import './index.scss' import { NCard } from 'naive-ui' import { Teleport, Transition } from 'vue' import interact from 'interactjs' import { cardProps } from 'naive-ui' import { unrefElement, completeSize, queryElements } from '@/utils' import type { VNode } from 'vue' import type { MaybeElement, MaybeRefOrGetter } from '@vueuse/core' import type { AnyFC } from '@/types' type RestrictRectOptions = Parameters[0] type Padding = { x: number y: number } export type DefaultPosition = | Padding | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center' | 'top-center' | 'bottom-center' const props = { ...cardProps, /** * * @description * 需要限制的区域位置。 * * @default body */ restrictionElement: { type: [String, HTMLElement, Function, Object] as PropType< string | HTMLElement | (() => VNode) | MaybeRefOrGetter >, default: 'body', }, /** * * @description * 是否启用拖拽。 * * @default true */ dad: { type: Boolean, default: true, }, /** * * @description * 自定义限制拖拽范围。 * * @default undefined */ restrictRectOptions: { type: Object as PropType, default: void 0, }, /** * * @description * 默认位置。 * * @default { x: 0, y: 0 } */ defaultPosition: { type: [Object, String] as PropType, default: () => ({ x: 0, y: 0, }), }, /** * * @description * 拖拽卡片宽度。 * * @default 600 */ width: { type: [String, Number] as PropType, default: 600, }, /** * * @description * 拖拽卡片 z-index。 * * @default undefined */ zIndex: { type: Number, default: void 0, }, /** * * @description * 是否启用动画。 * * @default false */ animation: { type: Boolean, default: false, }, /** * * @description * 默认的边距。 * 设置该属性后,卡片首次出现的位置会根据该属性进行偏移。 * * @default undefined */ padding: { type: Object as PropType, default: void 0, }, } export default defineComponent({ name: 'RDraggableCard', props, setup(props, { expose }) { const cardRef = useTemplateRef('cardRef') let interactInst: ReturnType | null = null const position = { x: 0, y: 0, } const CONTAINER_ID = 'r-draggable-card-container' const cssVars = computed(() => { return { '--r-draggable-card-width': completeSize(props.width), '--r-draggable-card-z-index': props.zIndex, } }) let isSetup = false const cacheProps = { defaultPosition: props.defaultPosition, dad: props.dad, } // 创建 DraggableCard 容器 const createDraggableCardContainer = () => { if (!document.getElementById(CONTAINER_ID)) { const container = document.createElement('div') container.id = CONTAINER_ID document.documentElement.appendChild(container) } } createDraggableCardContainer() // 获取 card, restrictionElement 的 dom 信息 const getDom = () => { const card = unrefElement(cardRef) const re = typeof props.restrictionElement === 'string' ? queryElements(props.restrictionElement) : props.restrictionElement let restrictionElement: HTMLElement | null = null if (Array.isArray(re)) { restrictionElement = re[0] } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any restrictionElement = unrefElement(re as any) as HTMLElement } return { card, restrictionElement, } } // 获取 container, card 的位置 const getPosition = (containerRect: DOMRect, cardRect: DOMRect) => { const { defaultPosition, padding } = props const { x: paddingX = 0, y: paddingY = 0 } = padding ?? {} // 默认的 body restrictionElement 的偏移量是 0 const { x: containerX, y: containerY, width: containerWidth, height: containerHeight, } = containerRect const { width: cardWidth, height: cardHeight } = cardRect if (typeof defaultPosition === 'string') { switch (defaultPosition) { case 'top-center': { const cx1 = (containerWidth - cardWidth) / 2 + containerX const cy1 = paddingY + containerY const cx2 = paddingX + cx1 const cy2 = cy1 return { x: cx2, y: cy2 } } case 'bottom-center': { const cx1 = (containerWidth - cardWidth) / 2 + containerX const cy1 = containerHeight - cardHeight - paddingY + containerY const cx2 = paddingX + cx1 const cy2 = cy1 return { x: cx2, y: cy2 } } case 'center': { const cx1 = (containerWidth - cardWidth) / 2 + containerX const cy1 = (containerHeight - cardHeight) / 2 + containerY const cx2 = paddingX + cx1 const cy2 = paddingY + cy1 return { x: cx2, y: cy2 } } case 'top-left': return { x: paddingX + containerX, y: paddingY + containerY } case 'top-right': return { x: containerWidth - cardWidth - paddingX + containerX, y: paddingY + containerY, } case 'bottom-left': return { x: paddingX + containerX, y: containerHeight - cardHeight - paddingY + containerY, } case 'bottom-right': return { x: containerWidth - cardWidth - paddingX + containerX, y: containerHeight - cardHeight - paddingY + containerY, } // 默认为左上角 default: return { x: paddingX + containerX, y: paddingY + containerY } } } else { const { x: defaultX, y: defaultY } = defaultPosition return { x: defaultX + containerX + paddingX, y: defaultY + containerY + paddingY, } } } // 初始化设置 card 的位置,并且根据配置启用拖拽 const setupDraggable = () => { const { card, restrictionElement } = getDom() if (!card) { return } const restrictionRect = restrictionElement?.getBoundingClientRect() const cardHeader = card.querySelector('.n-card-header') const restrictRectOptions = Object.assign( {}, { restriction: restrictionElement, endOnly: true, }, props.restrictRectOptions, ) if (restrictionRect && !isSetup) { // 计算偏移位置 const p = getPosition(restrictionRect, card.getBoundingClientRect()) // 设置初始位置 card.style.transform = `translate(${p.x}px, ${p.y}px)` position.x = p.x position.y = p.y } if (!props.dad) { return } interactInst = interact(card) .draggable({ inertia: true, autoScroll: true, allowFrom: cardHeader ? '.n-card-header' : '.n-card__content', modifiers: [interact.modifiers.restrictRect(restrictRectOptions)], listeners: { move: (event) => { card.setAttribute('can-drag', 'true') position.x += event.dx position.y += event.dy card.style.transform = `translate(${position.x}px, ${position.y}px)` }, }, }) .resizable(false) isSetup = true } // 取消拖拽 const resetDraggable = () => { interactInst?.unset() interactInst = null } // 更新拖拽 const refreshDraggableWhenPropsChange = (fn: AnyFC) => { isSetup = false fn() setupDraggable() } expose() watchEffect(() => { props.dad ? setupDraggable() : resetDraggable() if (props.defaultPosition !== cacheProps.defaultPosition) { refreshDraggableWhenPropsChange(() => { cacheProps.defaultPosition = props.defaultPosition }) } }) onMounted(() => { nextTick(() => { setupDraggable() }) }) return { cardRef, CONTAINER_ID, cssVars, } }, render() { const { $attrs, $slots, $props, CONTAINER_ID, cssVars, animation } = this return ( {animation ? ( {{ ...$slots }} ) : ( {{ ...$slots }} )} ) }, })