diff --git a/packages/devui-vue/devui-cli/commands/build.js b/packages/devui-vue/devui-cli/commands/build.js index 337e15b34d..5e0ae1ddb3 100644 --- a/packages/devui-vue/devui-cli/commands/build.js +++ b/packages/devui-vue/devui-cli/commands/build.js @@ -16,7 +16,7 @@ const baseConfig = defineConfig({ }); const rollupOptions = { - external: ['vue', 'vue-router', '@vueuse/core'], + external: ['vue', 'vue-router', '@vueuse/core', '@floating-ui/dom'], output: { globals: { vue: 'Vue', diff --git a/packages/devui-vue/devui/badge/index.ts b/packages/devui-vue/devui/badge/index.ts index 8657a733b7..d6a4e971d7 100644 --- a/packages/devui-vue/devui/badge/index.ts +++ b/packages/devui-vue/devui/badge/index.ts @@ -1,17 +1,14 @@ -import type { App } from 'vue' -import Badge from './src/badge' +import type { App } from 'vue'; +import Badge from './src/badge'; +export * from './src/badge-types'; -Badge.install = function (app: App) { - app.component(Badge.name, Badge) -} - -export { Badge } +export { Badge }; export default { title: 'Badge 徽标', category: '数据展示', status: '100%', install(app: App): void { - app.use(Badge as any) - } -} + app.component(Badge.name, Badge); + }, +}; diff --git a/packages/devui-vue/devui/badge/src/badge-types.ts b/packages/devui-vue/devui/badge/src/badge-types.ts index b0a5ba2fdf..ee212e5311 100644 --- a/packages/devui-vue/devui/badge/src/badge-types.ts +++ b/packages/devui-vue/devui/badge/src/badge-types.ts @@ -1,41 +1,41 @@ -import type { PropType, ExtractPropTypes } from 'vue' +import type { PropType, ExtractPropTypes } from 'vue'; -type BadgeStatusType = PropType<'danger' | 'warning' | 'waiting' | 'success' | 'info'> -type BadgePositionType = PropType<'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'> +export type BadgeStatusType = 'danger' | 'warning' | 'waiting' | 'success' | 'info'; +export type BadgePositionType = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; -const badgeStatusType = ['danger', 'warning', 'waiting', 'success', 'info'] -const badgePositionType = ['top-left', 'top-right', 'bottom-left', 'bottom-right'] +const badgeStatusType = ['danger', 'warning', 'waiting', 'success', 'info']; +const badgePositionType = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; export const badgeProps = { count: { - type: [Number, String] + type: [Number, String], }, maxCount: { type: Number, - default: 99 + default: 99, }, showDot: { type: Boolean, - default: false + default: false, }, status: { - type: String as BadgeStatusType, - validator: (val: string) => badgeStatusType.includes(val) + type: String as PropType, + validator: (val: string): boolean => badgeStatusType.includes(val), }, - badgePos: { - type: String as BadgePositionType, + position: { + type: String as PropType, default: 'top-right', - validator: (val: string) => badgePositionType.includes(val) + validator: (val: string): boolean => badgePositionType.includes(val), }, - offsetXY: { - type: Array + offset: { + type: Array as PropType>, }, bgColor: { - type: String + type: String, }, textColor: { - type: String - } -} + type: String, + }, +}; -export type BadgeProps = ExtractPropTypes +export type BadgeProps = ExtractPropTypes; diff --git a/packages/devui-vue/devui/badge/src/badge.tsx b/packages/devui-vue/devui/badge/src/badge.tsx index ad6024bd19..a4b428eca6 100644 --- a/packages/devui-vue/devui/badge/src/badge.tsx +++ b/packages/devui-vue/devui/badge/src/badge.tsx @@ -1,65 +1,60 @@ -import './badge.scss' - -import { defineComponent, computed } from 'vue' -import { badgeProps, BadgeProps } from './badge-types' +import { defineComponent, computed } from 'vue'; +import { badgeProps, BadgeProps } from './badge-types'; +import './badge.scss'; export default defineComponent({ name: 'DBadge', props: badgeProps, - emits: [], setup(props: BadgeProps, ctx) { const className = computed(() => { - const base = 'devui-badge-content' + const base = 'devui-badge-content'; return [ base, props.showDot ? `${base}-dot` : `${base}-count`, props.status && `${base}-${props.status}`, - ctx.slots.default && props.badgePos && `${base}-${props.badgePos}`, - ctx.slots.default && `${base}-fixed` - ].join(' ') - }) + ctx.slots.default && props.position && `${base}-${props.position}`, + ctx.slots.default && `${base}-fixed`, + ].join(' '); + }); const style = computed(() => { const styleMap = { bgColor: 'background', - textColor: 'color' - } - const ret = Object.keys(styleMap).reduce((ret, key) => { - if (props[key]) { - ret[styleMap[key]] = props[key] - } - return ret - }, {}) - // 偏移量 - if (ctx.slots.default && props.offsetXY) { - const [x, y]: Array = props.offsetXY as Array - const [yName, xName] = (props.badgePos as string).split('-') - ret[yName] = y + 'px' - ret[xName] = x + 'px' + textColor: 'color', + }; + const ret = Object.keys(styleMap).reduce((result, key) => { + props[key] && (result[styleMap[key]] = props[key]); + return result; + }, {}); + if (ctx.slots.default && props.offset) { + const [x, y]: Array = props.offset; + const [yName, xName] = props.position.split('-'); + ret[yName] = y + 'px'; + ret[xName] = x + 'px'; } - return ret - }) + return ret; + }); const text = computed(() => { if (props.showDot) { - return + return; } if (typeof props.count === 'number' && typeof props.maxCount === 'number') { - return props.count > props.maxCount ? `${props.maxCount}+` : props.count + return props.count > props.maxCount ? `${props.maxCount}+` : props.count; } - return props.count - }) + return props.count; + }); return () => { return ( -
+
{ctx.slots.default?.()}
{text.value}
- ) - } - } -}) + ); + }; + }, +}); diff --git a/packages/devui-vue/devui/overlay/index.ts b/packages/devui-vue/devui/overlay/index.ts index 7c9592b712..b18849933c 100644 --- a/packages/devui-vue/devui/overlay/index.ts +++ b/packages/devui-vue/devui/overlay/index.ts @@ -1,25 +1,18 @@ -import type { App } from 'vue' -import {FixedOverlay} from './src/fixed-overlay'; -import {FlexibleOverlay } from './src/flexible-overlay'; -import {inBrowser} from '../shared/util/common-var'; +import type { App } from 'vue'; +import { FixedOverlay } from './src/fixed-overlay'; +import { FlexibleOverlay } from './src/flexible-overlay'; +import { inBrowser } from '../shared/util/common-var'; +export * from './src/overlay-types'; -FlexibleOverlay.install = function(app: App) { - app.component(FlexibleOverlay.name, FlexibleOverlay); -} - -FixedOverlay.install = function(app: App) { - app.component(FixedOverlay.name, FixedOverlay); -} - -export { FlexibleOverlay, FixedOverlay } +export { FlexibleOverlay, FixedOverlay }; export default { title: 'Overlay 遮罩层', category: '通用', status: '100%', install(app: App): void { - app.use(FixedOverlay as any); - app.use(FlexibleOverlay as any); + app.component(FixedOverlay.name, FixedOverlay); + app.component(FlexibleOverlay.name, FlexibleOverlay); if (inBrowser && !document.getElementById('d-overlay-anchor')) { const overlayAnchor = document.createElement('div'); @@ -28,7 +21,7 @@ export default { overlayAnchor.style.left = '0'; overlayAnchor.style.top = '0'; overlayAnchor.style.zIndex = '1000'; - document.body.appendChild(overlayAnchor); + document.body.appendChild(overlayAnchor); } - } -} + }, +}; diff --git a/packages/devui-vue/devui/overlay/src/fixed-overlay.tsx b/packages/devui-vue/devui/overlay/src/fixed-overlay.tsx index cda32fbcb2..0bb70d9b31 100644 --- a/packages/devui-vue/devui/overlay/src/fixed-overlay.tsx +++ b/packages/devui-vue/devui/overlay/src/fixed-overlay.tsx @@ -9,29 +9,17 @@ export const FixedOverlay = defineComponent({ props: fixedOverlayProps, emits: overlayEmits, setup(props: FixedOverlayProps, ctx) { - const { - backgroundClass, - overlayClass, - handleBackdropClick, - handleOverlayBubbleCancel - } = useOverlayLogic(props, ctx); + const { backgroundClass, overlayClass, handleBackdropClick, handleOverlayBubbleCancel } = useOverlayLogic(props, ctx); return () => ( -
-
- {renderSlot(ctx.slots, 'default')} + {props.visible && ( +
+
+ {renderSlot(ctx.slots, 'default')} +
-
+ )} ); }, diff --git a/packages/devui-vue/devui/overlay/src/flexible-overlay.scss b/packages/devui-vue/devui/overlay/src/flexible-overlay.scss new file mode 100644 index 0000000000..c5965590b8 --- /dev/null +++ b/packages/devui-vue/devui/overlay/src/flexible-overlay.scss @@ -0,0 +1,17 @@ +@import '../../styles-var/devui-var.scss'; + +.devui-flexible-overlay { + position: fixed; + border-radius: $devui-border-radius; + background-color: $devui-connected-overlay-bg; + box-shadow: $devui-shadow-length-connected-overlay $devui-shadow; + z-index: 1000; + + &-arrow { + position: absolute; + width: 8px; + height: 8px; + transform: rotate(45deg); + background-color: inherit; + } +} diff --git a/packages/devui-vue/devui/overlay/src/flexible-overlay.tsx b/packages/devui-vue/devui/overlay/src/flexible-overlay.tsx index fc7ff9b97f..6ca3d5ac60 100644 --- a/packages/devui-vue/devui/overlay/src/flexible-overlay.tsx +++ b/packages/devui-vue/devui/overlay/src/flexible-overlay.tsx @@ -1,263 +1,22 @@ -import { - CSSProperties, - defineComponent, - isRef, - onMounted, - reactive, - ref, - renderSlot, - toRef, - watch, - computed -} from 'vue'; -import { CommonOverlay } from './common-overlay'; -import { OriginOrDomRef, flexibleOverlayProps, FlexibleOverlayProps, Point, Origin, ConnectionPosition, overlayEmits } from './overlay-types'; -import { useOverlayLogic } from './utils'; +import { defineComponent } from 'vue'; +import { flexibleOverlayProps, FlexibleOverlayProps } from './overlay-types'; +import { useOverlay } from './use-flexible-overlay'; +import './flexible-overlay.scss'; -import { getElement, isComponent } from '../../shared/util/dom'; - -/** - * 弹性的 Overlay,用于连接固定的和相对点 - */ export const FlexibleOverlay = defineComponent({ name: 'DFlexibleOverlay', + inheritAttrs: false, props: flexibleOverlayProps, - emits: overlayEmits, - setup(props: FlexibleOverlayProps, ctx) { - // lift cycle - const overlayRef = ref(null); - const positionedStyle = reactive({ position: 'absolute' }); - onMounted(async () => { - const handleRectChange = (position: ConnectionPosition, rect: DOMRect, origin: Origin) => { - // TODO: add optimize for throttle - const point = calculatePosition(position, rect, origin); - - // set the current position style's value. - // the current position style is a 'ref'. - positionedStyle.left = `${point.x}px`; - positionedStyle.top = `${point.y}px`; - }; - - const locationElements = computed(() => { - // 获取面板 - const overlay = overlayRef.value; - // 获取原点 - const origin = getOrigin(props.origin); - if (!overlay || !origin) { - return; - } - return { origin, overlay }; - }); - - const visibleRef = toRef(props, 'visible'); - const positionRef = toRef(props, 'position'); - - watch([locationElements, visibleRef, positionRef], async ([locationElements, visible, position], ov, onInvalidate) => { - if (!visible || !locationElements) { - return; - } - const { origin, overlay } = locationElements; - handleRectChange(position, overlay.getBoundingClientRect(), origin); - const unsubscriptions = [ - subscribeLayoutEvent(() => handleRectChange(position, overlay.getBoundingClientRect(), origin)), - subscribeOverlayResize(overlay, (entries) => handleRectChange(position, entries[0].contentRect, origin)), - subscribeOriginResize(origin, () => handleRectChange(position, overlay.getBoundingClientRect(), origin)), - ]; - onInvalidate(() => { - unsubscriptions.forEach(fn => fn()); - }); - }); - }); - - const { - backgroundClass, - overlayClass, - handleBackdropClick, - handleOverlayBubbleCancel - } = useOverlayLogic(props,ctx); - - return () => ( - -
-
- {renderSlot(ctx.slots, 'default')} -
+ emits: ['update:modelValue', 'positionChange'], + setup(props: FlexibleOverlayProps, { slots, attrs, emit }) { + const { arrowRef, overlayRef } = useOverlay(props, emit); + + return () => + props.modelValue && ( +
+ {slots.default?.()} + {props.showArrow &&
}
- - ); + ); }, }); - - -/** - * 获取原点,可能是 Element 或者 Rect - * @param {OriginOrDomRef} origin - * @returns {Origin} - */ -function getOrigin(origin: OriginOrDomRef): Origin { - // Check for Element so SVG elements are also supported. - if (origin instanceof Element) { - return origin; - } - - if (isRef(origin)) { - return getElement(origin.value); - } - - if (isComponent(origin)) { - return getElement(origin); - } - - // is point { x: number, y: number, width: number, height: number } - return origin; -} - -/** - * 计算坐标系 - * @param {ConnectionPosition} position - * @param {HTMLElement | DOMRect} panelOrRect - * @param {Origin} origin - * @returns - */ -function calculatePosition( - position: ConnectionPosition, - rect: DOMRect, - origin: Origin -): Point { - // get overlay rect - const originRect = getOriginRect(origin); - - // calculate the origin point - const originPoint = getOriginRelativePoint(originRect, position); - - // calculate the overlay anchor point - return getOverlayPoint(originPoint, rect, position); -} - -/** - * 返回原点元素的 ClientRect - * @param origin - * @returns {DOMRect} - */ -function getOriginRect(origin: Origin): DOMRect { - if (origin instanceof Element) { - return origin.getBoundingClientRect(); - } - // Origin is point - const width = origin.width || 0; - const height = origin.height || 0; - - // If the origin is a point, return a client rect as if it was a 0x0 element at the point. - return { - top: origin.y, - bottom: origin.y + height, - left: origin.x, - right: origin.x + width, - height, - width, - } as DOMRect; -} - -/** - * 获取遮罩层的左上角坐标 - * @param {Point} originPoint - * @param {DOMRect} rect - * @param {ConnectionPosition} position - * @returns - */ -function getOverlayPoint( - originPoint: Point, - rect: DOMRect, - position: ConnectionPosition -): Point { - let x: number; - const { width, height } = rect; - if (position.overlayX == 'center') { - x = originPoint.x - width / 2; - } else { - x = position.overlayX == 'left' ? originPoint.x : originPoint.x - width; - } - - let y: number; - if (position.overlayY == 'center') { - y = originPoint.y - height / 2; - } else { - y = position.overlayY == 'top' ? originPoint.y : originPoint.y - height; - } - - return { x, y }; -} - -/** - * 获取原点相对于 position 的坐标 (x, y) - * @param originRect - * @param position - * @returns - */ -function getOriginRelativePoint( - originRect: ClientRect, - position: ConnectionPosition -): Point { - let x: number; - if (position.originX == 'center') { - x = originRect.left + originRect.width / 2; - } else { - const startX = originRect.left; - const endX = originRect.right; - x = position.originX == 'left' ? startX : endX; - } - - let y: number; - if (position.originY == 'center') { - y = originRect.top + originRect.height / 2; - } else { - y = position.originY == 'top' ? originRect.top : originRect.bottom; - } - - return { x, y }; -} - -/** - * 订阅 layout 变化事件 - * @param event - */ -function subscribeLayoutEvent(event: (e?: Event) => void) { - window.addEventListener('scroll', event, true); - window.addEventListener('resize', event); - window.addEventListener('orientationchange', event); - return () => { - window.removeEventListener('scroll', event, true); - window.removeEventListener('resize', event); - window.removeEventListener('orientationchange', event); - } -} - -function subscribeOverlayResize(overlay: Element, callback: (entries: ResizeObserverEntry[]) => void) { - if (overlay instanceof Element) { - const resizeObserver = new ResizeObserver(callback); - resizeObserver.observe(overlay); - return () => resizeObserver.disconnect(); - } - return () => { }; -} - -function subscribeOriginResize(origin: any, callback: () => void) { - if (origin instanceof Element) { - // Only when the style changing, you can change the position. - const observer = new MutationObserver(callback); - observer.observe(origin, { attributeFilter: ['style'] }); - return () => observer.disconnect(); - } - return () => { }; -} - diff --git a/packages/devui-vue/devui/overlay/src/flexible-utils.ts b/packages/devui-vue/devui/overlay/src/flexible-utils.ts new file mode 100644 index 0000000000..8504d88eb0 --- /dev/null +++ b/packages/devui-vue/devui/overlay/src/flexible-utils.ts @@ -0,0 +1,10 @@ +export function getScrollParent(element: HTMLElement): HTMLElement | (Window & typeof globalThis) { + const overflowRegex = /(auto|scroll|hidden)/; + for (let parent = element; (parent = parent.parentElement); parent.parentElement !== document.body) { + const style = window.getComputedStyle(parent); + if (overflowRegex.test(style.overflow + style.overflowX + style.overflowY)) { + return parent; + } + } + return window; +} diff --git a/packages/devui-vue/devui/overlay/src/overlay-types.ts b/packages/devui-vue/devui/overlay/src/overlay-types.ts index d4fc30fd67..52365888e3 100644 --- a/packages/devui-vue/devui/overlay/src/overlay-types.ts +++ b/packages/devui-vue/devui/overlay/src/overlay-types.ts @@ -1,4 +1,4 @@ -import { ExtractPropTypes, PropType, StyleValue, ComponentPublicInstance, Ref } from 'vue'; +import type { ExtractPropTypes, PropType, StyleValue, Ref } from 'vue'; export const overlayProps = { visible: { @@ -6,32 +6,31 @@ export const overlayProps = { }, backgroundBlock: { type: Boolean, - default: false + default: false, }, backgroundClass: { type: String, - default: '' + default: '', }, backgroundStyle: { - type: [String, Object] as PropType + type: [String, Object] as PropType, }, onBackdropClick: { type: Function, }, backdropClose: { type: Boolean, - default: true + default: true, }, hasBackdrop: { type: Boolean, - default: true + default: true, }, } as const; export const overlayEmits = ['update:visible', 'backdropClick'] as ['update:visible', 'backdropClick']; export type OverlayProps = ExtractPropTypes; - export const fixedOverlayProps = { ...overlayProps, overlayStyle: { @@ -41,67 +40,58 @@ export const fixedOverlayProps = { }; export type FixedOverlayProps = ExtractPropTypes; +export type Placement = + | 'top' + | 'right' + | 'bottom' + | 'left' + | 'top-start' + | 'top-end' + | 'right-start' + | 'right-end' + | 'bottom-start' + | 'bottom-end' + | 'left-start' + | 'left-end'; + +export type Alignment = 'start' | 'end'; +export type OffsetOptions = { mainAxis?: number; crossAxis?: number }; export const flexibleOverlayProps = { + modelValue: { + type: Boolean, + default: false, + }, origin: { - type: Object as PropType, + type: Object as PropType, require: true, }, position: { - type: Object as PropType, - default: (): ConnectionPosition => ({ - originX: 'left', - originY: 'top', - overlayX: 'left', - overlayY: 'top', - }), + type: Array as PropType>, + default: ['bottom'], }, - ...overlayProps, -} - - - - -export interface ClientRect { - bottom: number - readonly height: number - left: number - right: number - top: number - readonly width: number -} - -export interface Point { - x: number - y: number -} - -export interface Rect { - x: number - y: number - width?: number - height?: number -} - -export type Origin = Element | Rect; + offset: { + type: [Number, Object] as PropType, + default: 8, + }, + align: { + type: String as PropType | null, + default: null, + }, + showArrow: { + type: Boolean, + default: false, + }, + isArrowCenter: { + type: Boolean, + default: true, + }, +}; -type HorizontalConnectionPos = 'left' | 'center' | 'right'; -type VerticalConnectionPos = 'top' | 'center' | 'bottom'; +export type Point = { x?: number; y?: number }; -export interface ConnectionPosition { - originX: HorizontalConnectionPos - originY: VerticalConnectionPos - overlayX: HorizontalConnectionPos - overlayY: VerticalConnectionPos -} +export type UseOverlayFn = { arrowRef: Ref; overlayRef: Ref }; -export type OriginOrDomRef = - | Element - | ComponentPublicInstance - | Ref - | Rect - | null; +export type EmitEventFn = (event: 'positionChange' | 'update:modelValue', result?: unknown) => void; export type FlexibleOverlayProps = ExtractPropTypes; - - diff --git a/packages/devui-vue/devui/overlay/src/use-flexible-overlay.ts b/packages/devui-vue/devui/overlay/src/use-flexible-overlay.ts new file mode 100644 index 0000000000..c4e8f5c49f --- /dev/null +++ b/packages/devui-vue/devui/overlay/src/use-flexible-overlay.ts @@ -0,0 +1,90 @@ +import { ref, unref, watch, nextTick, onUnmounted } from 'vue'; +import { FlexibleOverlayProps, Placement, Point, UseOverlayFn, EmitEventFn } from './overlay-types'; +import { arrow, autoPlacement, computePosition, offset } from '@floating-ui/dom'; +import { getScrollParent } from './flexible-utils'; + +function adjustArrowPosition(isArrowCenter: boolean, point: Point, placement: Placement, originRect: any): Point { + let { x, y } = point; + if (!isArrowCenter) { + const { width, height } = originRect; + if (x && placement.includes('start')) { + x = 12; + } + if (x && placement.includes('end')) { + x = Math.round(width - 24); + } + if (y && placement.includes('start')) { + y = 10; + } + if (y && placement.includes('end')) { + y = height - 14; + } + } + + return { x, y }; +} + +export function useOverlay(props: FlexibleOverlayProps, emit: EmitEventFn): UseOverlayFn { + const overlayRef = ref(); + const arrowRef = ref(); + const updateArrowPosition = (arrowEl: HTMLElement, placement: Placement, point: Point, overlayEl: HTMLElement) => { + const { x, y } = adjustArrowPosition(props.isArrowCenter, point, placement, overlayEl.getBoundingClientRect()); + const staticSide = { + top: 'bottom', + right: 'left', + bottom: 'top', + left: 'right', + }[placement.split('-')[0]]; + Object.assign(arrowEl.style, { + left: x ? `${x}px` : '', + top: y ? `${y}px` : '', + right: '', + bottom: '', + [staticSide]: '-4px', + }); + }; + const updatePosition = async () => { + const hostEl = props.origin; + const overlayEl = unref(overlayRef.value); + const arrowEl = unref(arrowRef.value); + const middleware = [ + offset(props.offset), + autoPlacement({ + alignment: props.align, + allowedPlacements: props.position, + }), + ]; + props.showArrow && middleware.push(arrow({ element: arrowEl })); + const { x, y, placement, middlewareData } = await computePosition(hostEl, overlayEl, { + strategy: 'fixed', + middleware, + }); + emit('positionChange', placement); + Object.assign(overlayEl.style, { top: `${y}px`, left: `${x}px` }); + props.showArrow && updateArrowPosition(arrowEl, placement, middlewareData.arrow, overlayEl); + }; + watch( + () => props.modelValue, + () => { + const originParent = getScrollParent(props.origin); + if (props.modelValue && props.origin) { + nextTick(updatePosition); + originParent.addEventListener('scroll', updatePosition); + originParent !== window && window.addEventListener('scroll', updatePosition); + window.addEventListener('resize', updatePosition); + } else { + originParent.removeEventListener('scroll', updatePosition); + originParent !== window && window.removeEventListener('scroll', updatePosition); + window.removeEventListener('resize', updatePosition); + } + } + ); + onUnmounted(() => { + const originParent = getScrollParent(props.origin); + originParent.removeEventListener('scroll', updatePosition); + originParent !== window && window.removeEventListener('scroll', updatePosition); + window.removeEventListener('resize', updatePosition); + }); + + return { arrowRef, overlayRef }; +} diff --git a/packages/devui-vue/docs/components/badge/index.md b/packages/devui-vue/docs/components/badge/index.md index 41f8007189..5597e7fe30 100644 --- a/packages/devui-vue/docs/components/badge/index.md +++ b/packages/devui-vue/docs/components/badge/index.md @@ -2,7 +2,7 @@ 图标右上角的圆形徽标数字。 -### 何时使用 +#### 何时使用 出现在图标右上角或列表项右方,通过不同的状态色加数字提示用户有消息需要处理时。 @@ -12,34 +12,51 @@ ```vue + + ``` ::: ### 点状徽章 -:::demo 点状徽章类型,当有包裹元素且 showDot 参数为 true 时为点状徽章,默认在右上角展示小点不显示数目。 +:::demo 点状徽章类型,当有包裹元素且 `show-dot` 参数为 true 时为点状徽章,默认在右上角展示小点不显示数目。 ```vue + + ``` ::: @@ -50,41 +67,57 @@ ```vue + + ``` ::: ### 状态徽章 -:::demo 当徽章独立使用、不包裹任何元素且 showDot 参数为 true 时为状态徽章,不同状态展示不同色点。 +:::demo 当徽章独立使用、不包裹任何元素且 `show-dot` 参数为 true 时为状态徽章,不同状态展示不同色点。 ```vue ``` @@ -93,20 +126,16 @@ ### 徽章位置 -:::demo 通过 badgePos 参数设置徽章位置。 +:::demo 通过 `position` 参数设置徽章位置。 ```vue @@ -116,64 +145,45 @@ ### 自定义 -:::demo 通过 bgColor 参数设置徽章展示状态色(此时 status 参数设置的徽章状态色失效),通过 offsetXY 参数可设置相对于 badgePos 的徽章偏移量。通过 textColor、bgColor 自定义文字、背景颜色。 +:::demo 通过 `bg-color` 参数设置徽章展示状态色(此时 `status` 参数设置的徽章状态色失效),通过 `offset` 参数可设置相对于 position 的徽章偏移量。通过 ` text-color``、bgColor ` 自定义文字、背景颜色。 ```vue ``` ::: -### API - -| 参数 | 类型 | 默认 | 说明 | -| :-------: | :-----------------: | :---------: | :--------------------------------------------------------------------------------------------------------------------------- | -| count | `Number` | -- | 可选,设置基本徽章和计数徽章中显示的数目 | -| maxCount | `Number` | 99 | 可选,设置基本徽章和计数徽章最大可显示数目,当 count > maxCount 时显示 maxCount+ | -| showDot | `Boolean` | false | 可选,true 时为点状徽章(有包裹)或状态徽章(无包裹),false 时为基本徽章(有包裹)或计数徽章(无包裹) | -| status | `BadgeStatusType` | -- | 可选,状态色 danger\| warning \| waiting \| success \| info | -| badgePos | `BadgePositionType` | 'top-right' | 可选,徽标位置 top-left\| top-right \| bottom-left \| bottom-right | -| bgColor | `String` | -- | 可选,自定义徽标色,此时 status 参数设置的徽章状态色失效 | -| textColor | `String` | -- | 可选, 可自定义徽标文字颜色 | -| offsetXY | `[number, number] ` | -- | 可选,可选,有包裹时徽标位置偏移量,格式为[x,y],单位为 px。x 为相对 right 或 left 的偏移量,y 为相对 top 或 bottom 的偏移量 | - - +### d-badge 参数 + +| 参数 | 类型 | 默认 | 说明 | +| ---------- | ------------------- | ----------- | :--------------------------------------------------------------------------------------------------------------------- | +| count | `Number` | -- | 可选,设置基本徽章和计数徽章中显示的数目 | +| max-count | `Number` | 99 | 可选,设置基本徽章和计数徽章最大可显示数目,当 count > `max-count` 时显示 `max-count+` | +| show-dot | `Boolean` | false | 可选,true 时为点状徽章(有包裹)或状态徽章(无包裹),false 时为基本徽章(有包裹)或计数徽章(无包裹) | +| status | `BadgeStatusType` | -- | 可选,状态色 danger\| warning \| waiting \| success \| info | +| position | `BadgePositionType` | 'top-right' | 可选,徽标位置 top-left\| top-right \| bottom-left \| bottom-right | +| bg-color | `String` | -- | 可选,自定义徽标色,此时 status 参数设置的徽章状态色失效 | +| text-color | `String` | -- | 可选, 可自定义徽标文字颜色 | +| offset | `[number, number]` | -- | 可选,有包裹时徽标位置偏移量,格式为[x,y],单位为 px。x 为相对 right 或 left 的偏移量,y 为相对 top 或 bottom 的偏移量 | + +### BadgeStatusType 类型 + +```typescript +type BadgeStatusType = 'danger' | 'warning' | 'waiting' | 'success' | 'info'; +``` + +### BadgePositionType 类型 + +```typescript +type BadgePositionType = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; +``` diff --git a/packages/devui-vue/docs/components/overlay/index.md b/packages/devui-vue/docs/components/overlay/index.md index 2c1d9eba1d..db9c2b8a5f 100644 --- a/packages/devui-vue/docs/components/overlay/index.md +++ b/packages/devui-vue/docs/components/overlay/index.md @@ -1,152 +1,86 @@ # 遮罩层 + 遮罩层属于基础组件,用于构建独立于当前页面布局的组件。 -### 何时使用 + +#### 何时使用 + 当你需要全局弹窗,或者需要元素跟随功能,便可以使用该组件。 + ### 固定遮罩层 :::demo ```vue ``` + ::: +### 自适应位置 -### 弹性遮罩层 +:::demo 跟随起点元素移动,并且在遇到边界时根据`position`参数指定的可选位置自动调整。 -:::demo ```vue + + ``` ::: @@ -205,57 +139,45 @@ export default defineComponent({ } +### d-fixed-overlay 参数 + +| 参数 | 类型 | 默认 | 说明 | +| ----------------- | -------------------------- | ----- | ---------------------------------------------------------------------------- | +| visible | `boolean` | false | 可选,遮罩层是否可见 | +| onUpdate:visible | `(value: boolean) => void` | -- | 可选,遮罩层取消可见事件 | +| background-block | `boolean` | false | 可选,如果为 true,背景不能滚动 | +| background-class | `string` | -- | 可选,背景的样式类 | +| background-style | `StyleValue` | -- | 可选,背景的样式 | +| on-backdrop-click | `() => void` | -- | 可选,点击背景触发的事件 | +| backdrop-close | `boolean` | false | 可选,如果为 true,点击背景将触发 `onUpdate:visible`,默认参数是 false | +| has-backdrop | `boolean` | true | 可选,如果为 false,背景元素的 `point-event` 会设为 `none`,且不显示默认背景 | +| overlay-style | `CSSProperties` | -- | 可选,遮罩层的样式 | + +### d-flexible-overlay 参数 + +| 参数 | 类型 | 默认 | 说明 | +| ---------- | ---------------------- | ---------- | ------------------------------------------------------ | +| v-model | `boolean` | false | 可选,控制是否显示 | +| origin | `HTMLElement` | -- | 必选,你必须指定起点元素才能让遮罩层与该元素连接在一起 | +| position | `Placement[]` | ['bottom'] | 可选,指定显示位置 | +| align | `start \| end \| null` | null | 可选,指定对对齐方式,默认居中对齐 | +| offset | `number` | 8 | 可选,指定与起点元素的间距 | +| show-arrow | `boolean` | false | 可选,是否显示箭头 | + +### Placement 类型 -### API -d-fixed-overlay 参数 - -| 参数 | 类型 | 默认 | 说明 | -| :--------------: | :------------------------: | :---: | :-------------------------------------------------------------------------- | -| visible | `boolean` | false | 可选,遮罩层是否可见 | -| onUpdate:visible | `(value: boolean) => void` | -- | 可选,遮罩层取消可见事件 | -| backgroundBlock | `boolean` | false | 可选,如果为 true,背景不能滚动 | -| backgroundClass | `string` | -- | 可选,背景的样式类 | -| backgroundStyle | `StyleValue` | -- | 可选,背景的样式 | -| onBackdropClick | `() => void` | -- | 可选,点击背景触发的事件 | -| backdropClose | `boolean` | false | 可选,如果为true,点击背景将触发 `onUpdate:visible`,默认参数是 false | -| hasBackdrop | `boolean` | true | 可选,如果为false,背景元素的 `point-event` 会设为 `none`,且不显示默认背景 | -| overlayStyle | `CSSProperties` | -- | 可选,遮罩层的样式 | - -d-flexible-overlay 参数 - -| 参数 | 类型 | 默认 | 说明 | -| :--------------: | :------------------------------------------: | :---: | :-------------------------------------------------------------------------- | -| visible | `boolean` | false | 可选,遮罩层是否可见 | -| onUpdate:visible | `(value: boolean) => void` | -- | 可选,遮罩层取消可见事件 | -| backgroundBlock | `boolean` | false | 可选,如果为 true,背景不能滚动 | -| backgroundClass | `string` | -- | 可选,背景的样式类 | -| backgroundStyle | `StyleValue` | -- | 可选,背景的样式 | -| onBackdropClick | `() => void` | -- | 可选,点击背景触发的事件 | -| backdropClose | `boolean` | false | 可选,如果为true,点击背景将触发 `onUpdate:visible`,参数是 false | -| hasBackdrop | `boolean` | true | 可选,如果为false,背景元素的 `point-event` 会设为 `none`,且不显示默认背景 | -| origin | `Element \| ComponentPublicInstance \| Rect` | false | 必选,你必须指定起点元素才能让遮罩层与该元素连接在一起 | -| position | `ConnectionPosition` | false | 可选,指定遮罩层与原点的连接点 | - -Rect 数据结构 ```typescript -interface Rect { - x: number - y: number - width?: number - height?: number -} -``` - -ConnectionPosition 数据结构 -```typescript -type HorizontalConnectionPos = 'left' | 'center' | 'right'; -type VerticalConnectionPos = 'top' | 'center' | 'bottom'; - -export interface ConnectionPosition { - originX: HorizontalConnectionPos - originY: VerticalConnectionPos - overlayX: HorizontalConnectionPos - overlayY: VerticalConnectionPos -} +type Placement = + | 'top' + | 'right' + | 'bottom' + | 'left' + | 'top-start' + | 'top-end' + | 'right-start' + | 'right-end' + | 'bottom-start' + | 'bottom-end' + | 'left-start' + | 'left-end'; ``` - diff --git a/packages/devui-vue/package.json b/packages/devui-vue/package.json index a12e943acf..5e47b4b5e3 100644 --- a/packages/devui-vue/package.json +++ b/packages/devui-vue/package.json @@ -34,6 +34,7 @@ }, "dependencies": { "@devui-design/icons": "^1.3.0", + "@floating-ui/dom": "^0.4.0", "@types/lodash-es": "^4.17.4", "@vueuse/core": "^7.7.1", "async-validator": "^4.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9dbe5a10c0..4ddffe848f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,6 +100,7 @@ importers: '@devui-design/icons': ^1.3.0 '@docsearch/css': ^3.0.0-alpha.50 '@docsearch/js': ^3.0.0-alpha.50 + '@floating-ui/dom': ^0.4.0 '@types/chalk': ^2.2.0 '@types/commander': ^2.12.2 '@types/jest': ^26.0.23 @@ -142,6 +143,7 @@ importers: vue-tsc: ^0.2.2 dependencies: '@devui-design/icons': 1.3.0 + '@floating-ui/dom': 0.4.0 '@types/lodash-es': 4.17.6 '@vueuse/core': 7.7.1_vue@3.2.31 async-validator: 4.0.7 @@ -1876,6 +1878,16 @@ packages: - supports-color dev: true + /@floating-ui/core/0.6.0: + resolution: {integrity: sha512-chJj27Tj4q6EUgRaR5m5Va+h+N5yFMP/s3CagVMS9Ug3482jsjZMvS2kuwQQAdLV+/1zCf5uZiN1mksaLerQoA==} + dev: false + + /@floating-ui/dom/0.4.0: + resolution: {integrity: sha512-sfbZCY30r2u9f6h2wBtFtvhlSLP2nUuwsj7feTvZLtxhbrVd+/43jrW1d2OHWfeqx8oUTbCQdO5Z7td9DffOkg==} + dependencies: + '@floating-ui/core': 0.6.0 + dev: false + /@francoischalifour/autocomplete-core/1.0.0-alpha.28: resolution: {integrity: sha512-rL9x+72btViw+9icfBKUJjZj87FgjFrD2esuTUqtj4RAX3s4AuVZiN8XEsfjQBSc6qJk31cxlvqZHC/BIyYXgg==} dev: true