diff --git a/devui/popover/index.ts b/devui/popover/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8325a36c012a26349dc8ea457c7281712437fb8c --- /dev/null +++ b/devui/popover/index.ts @@ -0,0 +1,17 @@ +import type { App } from 'vue' +import Popover from './src/popover' + +Popover.install = function (app: App): void { + app.component(Popover.name, Popover) +} + +export { Popover } + +export default { + title: 'Popover 悬浮提示', + category: '反馈', + status: '开发中', // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + app.use(Popover as any); + } +} diff --git a/devui/popover/src/popover.scss b/devui/popover/src/popover.scss new file mode 100644 index 0000000000000000000000000000000000000000..60f8e1d335f7ad328ba68cfecd0e0c2dea8ed063 --- /dev/null +++ b/devui/popover/src/popover.scss @@ -0,0 +1,323 @@ +@import '../../style/theme/color'; +@import '../../style//theme//corner'; +@import '../../style//theme/font'; +@import '../../style//theme//animation'; + +@mixin some-display { + display: inline-flex; +} + +.devui-popover { + --devui-popover-margin: -8px; + --devui-popover-width: -6px; + --devui-popover-offset: 8px; + + position: relative; + @include some-display; + + &::after { + content: ''; + width: 12px; + display: none; + height: 12px; + transform: rotate(45deg); + position: absolute; + z-index: 1060; + background-color: $devui-feedback-overlay-bg; + } + + &.devui-popover-isVisible { + .devui-popover-content { + @include some-display; + } + + &::after { + @include some-display; + } + } + + .devui-popover-content { + position: absolute; + display: none; + padding: 5px 14px; + align-items: center; + flex-wrap: wrap; + white-space: nowrap; + border-radius: $devui-border-radius-feedback; + color: $devui-feedback-overlay-text; + background-color: $devui-feedback-overlay-bg; + font-size: $devui-font-size-sm; + + &.is-icon { + flex-wrap: nowrap; + } + + .devui-popover-icon { + margin-right: 5px; + } + } +} + +//动画效果 +.devui-popover { + &.devui-popover-animation { + $devui-popover-animation-wait:0s; + @mixin some-animation { + animation: some-animation 0.2s $devui-animation-linear $devui-popover-animation-wait 1 forwards; + } + + .devui-popover-content { + opacity: 0; + @include some-animation; + } + + &::after { + $devui-popover-animation-wait:0.1s; + + opacity: 0; + @include some-animation; + } + } +} + +@keyframes some-animation { + 0% { + @include some-display; + } + + 100% { + opacity: 1; + } +} + +//left等方向 +.devui-popover { + @mixin left-postion--content { + left: var(--devui-popover-margin); + top: 0; + transform: translate(-100%, 0); + } + @mixin left-postion--after { + left: var(--devui-popover-margin); + top: 50%; + margin-top: var(--devui-popover-width); + margin-left: var(--devui-popover-width); + } + + &.left { + .devui-popover-content { + @include left-postion--content(); + + top: 50%; + transform: translate(-100%, -50%); + } + + &::after { + @include left-postion--after; + } + } + + &.left-top { + .devui-popover-content { + @include left-postion--content; + } + + &::after { + @include left-postion--after; + + top: var(--devui-popover-offset); + margin-top: auto; + } + } + + &.left-bottom { + .devui-popover-content { + @include left-postion--content; + + top: auto; + bottom: 0; + } + + &::after { + @include left-postion--after; + + bottom: var(--devui-popover-offset); + margin-top: auto; + } + } +} + +//top等方向 +.devui-popover { + @mixin top-postion--content { + top: var(--devui-popover-margin); + left: 0; + transform: translate(0, -100%); + } + @mixin top-postion--after { + top: var(--devui-popover-margin); + margin-top: var(--devui-popover-width); + margin-left: var(--devui-popover-width); + } + + &.top { + .devui-popover-content { + @include top-postion--content; + + left: 50%; + transform: translate(-50%, -100%); + } + + &::after { + left: 50%; + @include top-postion--after; + } + } + + &.top-left { + .devui-popover-content { + @include top-postion--content; + } + + &::after { + @include top-postion--after; + + left: var(--devui-popover-offset); + margin-left: auto; + margin-right: var(--devui-popover-width); + } + } + + &.top-right { + .devui-popover-content { + @include top-postion--content; + + left: auto; + right: 0; + } + + &::after { + @include top-postion--after; + + right: var(--devui-popover-offset); + } + } +} + +//right等方向 +.devui-popover { + @mixin right-postion--content { + right: var(--devui-popover-margin); + top: 50%; + transform: translate(100%, 0); + } + @mixin right-postion--after { + right: var(--devui-popover-margin); + margin-right: var(--devui-popover-width); + top: var(--devui-popover-offset); + } + + &.right { + .devui-popover-content { + @include right-postion--content; + + transform: translate(100%, -50%); + } + + &::after { + @include right-postion--after; + + top: 50%; + margin-top: var(--devui-popover-width); + } + } + + &.right-top { + .devui-popover-content { + @include right-postion--content; + + top: 0; + } + + &::after { + @include right-postion--after; + } + } + + &.right-bottom { + .devui-popover-content { + @include right-postion--content; + + bottom: 0; + top: auto; + } + + &::after { + @include right-postion--after; + + bottom: var(--devui-popover-offset); + } + } +} + +//bottom等方向 +.devui-popover { + @mixin bottom-postion--content { + left: 0; + bottom: 0; + margin-bottom: var(--devui-popover-width); + transform: translate(0, 100%); + } + @mixin bottom-postion--after { + left: var(--devui-popover-offset); + bottom: var(--devui-popover-margin); + margin-bottom: var(--devui-popover-width); + margin-right: var(--devui-popover-width); + } + + &.bottom { + .devui-popover-content { + @include bottom-postion--content; + + left: 50%; + bottom: 0; + transform: translate(-50%, 100%); + } + + &::after { + @include bottom-postion--after; + + left: 50%; + margin-right: auto; + margin-left: var(--devui-popover-width); + } + } + + &.bottom-left { + .devui-popover-content { + @include bottom-postion--content; + } + + &::after { + @include bottom-postion--after; + } + } + + &.bottom-right { + .devui-popover-content { + @include bottom-postion--content; + + left: auto; + right: 0; + } + + &::after { + @include bottom-postion--after; + + left: auto; + margin-right: auto; + right: var(--devui-popover-offset); + margin-left: var(--devui-popover-width); + } + } +} diff --git a/devui/popover/src/popover.tsx b/devui/popover/src/popover.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9fbd12ca37fc6c2f7baf8f0fec25b79c698b773b --- /dev/null +++ b/devui/popover/src/popover.tsx @@ -0,0 +1,121 @@ +import { defineComponent, toRefs, ref, CSSProperties, reactive } from 'vue' +import clickoutsideDirective from '../../shared/devui-directive/clickoutside' +import './popover.scss' + +type positionType = 'top' | 'right' | 'bottom' | 'left' +type triggerType = 'click' | 'hover' +type popType = 'success' | 'error' | 'warning' | 'info' | 'default' +const popTypeClass = { + success: { name: 'right-o', color: 'rgb(61, 204, 166)' }, + error: { name: 'error-o', color: 'rgb(249, 95, 91)' }, + info: { name: 'info-o', color: 'rgb(81, 112, 255)' }, + warning: { name: 'warning-o', color: 'rgb(254, 204, 85)' }, + default: {} +} + +export default defineComponent({ + name: 'DPopover', + directives: { + clickoutside: clickoutsideDirective + }, + + props: { + visible: { + type: Boolean, + default: false + }, + position: { + type: String as () => positionType, + default: 'bottom' + }, + content: { + type: String, + default: 'default' + }, + + trigger: { + type: String as () => triggerType, + default: 'click', + }, + + zIndex: { + type: Number as () => CSSProperties, + default: 1060 + }, + popType: { + type: String as () => popType, + default: 'default' + }, + showAnimation: { + type: Boolean, + default: true + }, + mouseEnterDelay: { + type: Number, + default: 150 + }, + mouseLeaveDelay: { + type: Number, + default: 100 + } + }, + + setup(props, ctx) { + let enter = null + let leave = null + const visible = ref(props.visible); + const { position, content, zIndex, trigger, popType, mouseEnterDelay, mouseLeaveDelay, showAnimation } = toRefs(props); + const isClick = trigger.value === 'click' + const iconType = reactive(popTypeClass[popType.value]) + const event = function () { + if (visible.value) { + visible.value = false; + return + } + visible.value = true + } + const onClick = isClick ? event : null; + const onMouseenter = isClick ? null : () => { + enter && clearTimeout(enter); + enter = setTimeout(() => { + visible.value = true + }, mouseEnterDelay.value) + } + const onMouseleave = isClick ? null : () => { + leave && clearTimeout(leave) + leave = setTimeout(() => { + visible.value = false + }, mouseLeaveDelay.value) + } + const hiddenContext = function () { + visible.value = false + } + + + return () => { + const { slots } = ctx; + const style: CSSProperties = { + zIndex: zIndex.value + } + + return ( +