|
<template> |
|
<div |
|
class="moveable-panel" |
|
ref="moveablePanelRef" |
|
:style="{ |
|
width: w + 'px', |
|
height: h ? h + 'px' : 'auto', |
|
left: x + 'px', |
|
top: y + 'px', |
|
}" |
|
> |
|
<template v-if="title"> |
|
<div class="header" @mousedown="$event => startMove($event)"> |
|
<div class="title">{{title}}</div> |
|
<div class="close-btn" @click="emit('close')"><IconClose /></div> |
|
</div> |
|
|
|
<div class="content"> |
|
<slot></slot> |
|
</div> |
|
</template> |
|
|
|
<div v-else class="content" @mousedown="$event => startMove($event)"> |
|
<slot></slot> |
|
</div> |
|
|
|
<div class="resizer" v-if="resizeable" @mousedown="$event => startResize($event)"></div> |
|
</div> |
|
</template> |
|
|
|
<script lang="ts" setup> |
|
import { computed, onMounted, ref } from 'vue' |
|
|
|
const props = withDefaults(defineProps<{ |
|
width: number |
|
height: number |
|
minWidth?: number |
|
minHeight?: number |
|
maxWidth?: number |
|
maxHeight?: number |
|
left?: number |
|
top?: number |
|
title?: string |
|
moveable?: boolean |
|
resizeable?: boolean |
|
}>(), { |
|
minWidth: 20, |
|
minHeight: 20, |
|
maxWidth: 500, |
|
maxHeight: 500, |
|
left: 10, |
|
top: 10, |
|
title: '', |
|
moveable: true, |
|
resizeable: false, |
|
}) |
|
|
|
const emit = defineEmits<{ |
|
(event: 'close'): void |
|
}>() |
|
|
|
const x = ref(0) |
|
const y = ref(0) |
|
const w = ref(0) |
|
const h = ref(0) |
|
const moveablePanelRef = ref<HTMLElement>() |
|
const realHeight = computed(() => { |
|
if (!h.value) { |
|
return moveablePanelRef.value?.clientHeight || 0 |
|
} |
|
return h.value |
|
}) |
|
|
|
onMounted(() => { |
|
if (props.left >= 0) x.value = props.left |
|
else x.value = document.body.clientWidth + props.left - props.width |
|
|
|
if (props.top >= 0) y.value = props.top |
|
else y.value = document.body.clientHeight + props.top - (props.height || realHeight.value) |
|
|
|
w.value = props.width |
|
h.value = props.height |
|
}) |
|
|
|
const startMove = (e: MouseEvent) => { |
|
if (!props.moveable) return |
|
|
|
let isMouseDown = true |
|
|
|
const windowWidth = document.body.clientWidth |
|
const clientHeight = document.body.clientHeight |
|
|
|
const startPageX = e.pageX |
|
const startPageY = e.pageY |
|
|
|
const originLeft = x.value |
|
const originTop = y.value |
|
|
|
document.onmousemove = e => { |
|
if (!isMouseDown) return |
|
|
|
const moveX = e.pageX - startPageX |
|
const moveY = e.pageY - startPageY |
|
|
|
let left = originLeft + moveX |
|
let top = originTop + moveY |
|
|
|
if (left < 0) left = 0 |
|
if (top < 0) top = 0 |
|
if (left + w.value > windowWidth) left = windowWidth - w.value |
|
if (top + realHeight.value > clientHeight) top = clientHeight - realHeight.value |
|
|
|
x.value = left |
|
y.value = top |
|
} |
|
document.onmouseup = () => { |
|
isMouseDown = false |
|
|
|
document.onmousemove = null |
|
document.onmouseup = null |
|
} |
|
} |
|
|
|
const startResize = (e: MouseEvent) => { |
|
if (!props.resizeable) return |
|
|
|
let isMouseDown = true |
|
|
|
const startPageX = e.pageX |
|
const startPageY = e.pageY |
|
|
|
const originWidth = w.value |
|
const originHeight = h.value |
|
|
|
document.onmousemove = e => { |
|
if (!isMouseDown) return |
|
|
|
const moveX = e.pageX - startPageX |
|
const moveY = e.pageY - startPageY |
|
|
|
let width = originWidth + moveX |
|
let height = originHeight + moveY |
|
|
|
if (width < props.minWidth) width = props.minWidth |
|
if (height < props.minHeight) height = props.minHeight |
|
if (width > props.maxWidth) width = props.maxWidth |
|
if (height > props.maxHeight) height = props.maxHeight |
|
|
|
w.value = width |
|
h.value = height |
|
} |
|
document.onmouseup = () => { |
|
isMouseDown = false |
|
|
|
document.onmousemove = null |
|
document.onmouseup = null |
|
} |
|
} |
|
</script> |
|
|
|
<style lang="scss" scoped> |
|
.moveable-panel { |
|
position: fixed; |
|
background-color: #fff; |
|
box-shadow: $boxShadow; |
|
border: 1px solid $borderColor; |
|
border-radius: $borderRadius; |
|
display: flex; |
|
flex-direction: column; |
|
z-index: 999; |
|
} |
|
.resizer { |
|
width: 10px; |
|
height: 10px; |
|
position: absolute; |
|
bottom: 0; |
|
right: 0; |
|
cursor: se-resize; |
|
|
|
&::after { |
|
content: ""; |
|
position: absolute; |
|
bottom: -4px; |
|
right: -4px; |
|
transform: rotate(45deg); |
|
transform-origin: center; |
|
width: 0; |
|
height: 0; |
|
border: 6px solid transparent; |
|
border-left-color: #e1e1e1; |
|
} |
|
} |
|
.header { |
|
height: 40px; |
|
display: flex; |
|
align-items: center; |
|
border-bottom: 1px solid #f0f0f0; |
|
cursor: move; |
|
} |
|
.title { |
|
flex: 1; |
|
font-size: 13px; |
|
padding-left: 10px; |
|
} |
|
.close-btn { |
|
width: 40px; |
|
height: 40px; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
color: #666; |
|
font-size: 13px; |
|
cursor: pointer; |
|
} |
|
.content { |
|
flex: 1; |
|
padding: 10px; |
|
overflow: auto; |
|
} |
|
</style> |