199 lines
4.5 KiB
Vue
199 lines
4.5 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
|
|
|
const props = defineProps({
|
|
initialPosition: {
|
|
type: Object,
|
|
default: () => ({ x: 0, y: 0 })
|
|
},
|
|
stick: {
|
|
type: String,
|
|
default: null,
|
|
validator: (value) => ['left', 'right', 'top', 'bottom', null].includes(value)
|
|
},
|
|
dragMode: {
|
|
type: String,
|
|
default: 'free',
|
|
validator: (value) => ['free', 'horizontal', 'vertical'].includes(value)
|
|
},
|
|
stickyDistance: {
|
|
type: Number,
|
|
default: 20
|
|
},
|
|
margin: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
draggable: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
zIndex: {
|
|
type: Number,
|
|
default: 1000
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['update:position'])
|
|
|
|
const containerRef = ref(null)
|
|
const position = ref(props.initialPosition)
|
|
const isDragging = ref(false)
|
|
const startPos = ref({ x: 0, y: 0 })
|
|
const windowSize = ref({ width: 0, height: 0 })
|
|
const elementSize = ref({ width: 0, height: 0 })
|
|
|
|
const transformStyle = computed(() => ({
|
|
transform: `translate3d(${position.value.x}px, ${position.value.y}px, 0)`,
|
|
zIndex: props.zIndex,
|
|
position: 'fixed',
|
|
top: '0',
|
|
left: '0', // 添加基准定位点
|
|
right: 'auto',
|
|
bottom: 'auto'
|
|
}))
|
|
|
|
const updateSizes = () => {
|
|
if (typeof window !== 'undefined') {
|
|
windowSize.value = {
|
|
width: window.innerWidth,
|
|
height: window.innerHeight
|
|
}
|
|
if (containerRef.value) {
|
|
elementSize.value = {
|
|
width: containerRef.value.offsetWidth,
|
|
height: containerRef.value.offsetHeight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleStick = (pos) => {
|
|
if (!props.stick) return pos
|
|
|
|
const { width, height } = elementSize.value
|
|
const { stickyDistance, margin } = props
|
|
const maxX = windowSize.value.width - width - margin
|
|
const maxY = windowSize.value.height - height - margin
|
|
|
|
const newPos = { ...pos }
|
|
|
|
switch (props.stick) {
|
|
case 'left':
|
|
if (pos.x < stickyDistance) newPos.x = margin
|
|
break
|
|
case 'right':
|
|
if (maxX - pos.x < stickyDistance) newPos.x = maxX
|
|
break
|
|
case 'top':
|
|
if (pos.y < stickyDistance) newPos.y = margin
|
|
break
|
|
case 'bottom':
|
|
if (maxY - pos.y < stickyDistance) newPos.y = maxY
|
|
break
|
|
}
|
|
|
|
return newPos
|
|
}
|
|
|
|
const constrainPosition = (pos) => {
|
|
const { width, height } = elementSize.value
|
|
const { margin } = props
|
|
const maxX = windowSize.value.width - width - margin
|
|
const maxY = windowSize.value.height - height - margin
|
|
|
|
return {
|
|
x: Math.min(Math.max(pos.x, margin), maxX),
|
|
y: Math.min(Math.max(pos.y, margin), maxY)
|
|
}
|
|
}
|
|
|
|
const handleStart = (event) => {
|
|
if (!props.draggable) return
|
|
|
|
const e = event.touches ? event.touches[0] : event
|
|
isDragging.value = true
|
|
startPos.value = {
|
|
x: e.clientX - position.value.x,
|
|
y: e.clientY - position.value.y
|
|
}
|
|
}
|
|
|
|
const handleMove = (event) => {
|
|
if (!isDragging.value) return
|
|
|
|
const e = event.touches ? event.touches[0] : event
|
|
let newPos = {
|
|
x: e.clientX - startPos.value.x,
|
|
y: e.clientY - startPos.value.y
|
|
}
|
|
|
|
if (props.dragMode === 'horizontal') {
|
|
newPos.y = position.value.y
|
|
} else if (props.dragMode === 'vertical') {
|
|
newPos.x = position.value.x
|
|
}
|
|
|
|
newPos = handleStick(newPos)
|
|
newPos = constrainPosition(newPos)
|
|
|
|
position.value = newPos
|
|
emit('update:position', newPos)
|
|
|
|
event.preventDefault()
|
|
}
|
|
|
|
const handleEnd = () => {
|
|
isDragging.value = false
|
|
}
|
|
|
|
onMounted(() => {
|
|
updateSizes()
|
|
window.addEventListener('resize', updateSizes)
|
|
document.addEventListener('mousemove', handleMove)
|
|
document.addEventListener('mouseup', handleEnd)
|
|
document.addEventListener('touchmove', handleMove, { passive: false })
|
|
document.addEventListener('touchend', handleEnd)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('resize', updateSizes)
|
|
document.removeEventListener('mousemove', handleMove)
|
|
document.removeEventListener('mouseup', handleEnd)
|
|
document.removeEventListener('touchmove', handleMove)
|
|
document.removeEventListener('touchend', handleEnd)
|
|
})
|
|
|
|
watch(() => props.initialPosition, (newPos) => {
|
|
position.value = newPos
|
|
}, {deep: true})
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
ref="containerRef"
|
|
class="drag-window"
|
|
:class="{ 'dragging': isDragging }"
|
|
:style="transformStyle"
|
|
@mousedown="handleStart"
|
|
@touchstart="handleStart"
|
|
>
|
|
<slot></slot>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.drag-window {
|
|
position: fixed;
|
|
touch-action: none;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
will-change: transform;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.drag-window.dragging {
|
|
transition: none;
|
|
cursor: move;
|
|
}
|
|
</style> |