165 lines
5.3 KiB
Vue
165 lines
5.3 KiB
Vue
<template>
|
||
<div class="element-wrapper" :class="{ active }" :style="wrapperStyle" @pointerdown.stop="startDrag" @click.stop="emit('select', element.id)">
|
||
<slot />
|
||
<template v-if="active && resizable">
|
||
<span v-for="handle in handles" :key="handle" class="resize-handle" :class="`handle-${handle}`" @pointerdown.stop="startResize($event, handle)" />
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed } from 'vue';
|
||
import { calcDragRect, calcResizeRect } from '../core/dragResize';
|
||
import type { NativeElement } from '../core/types';
|
||
const PX_PER_MM = 3.7795275591;
|
||
|
||
const props = defineProps<{
|
||
element: NativeElement;
|
||
active: boolean;
|
||
scale: number;
|
||
gridSize: number;
|
||
pageWidth: number;
|
||
pageHeight: number;
|
||
movable?: boolean;
|
||
resizable?: boolean;
|
||
dragBounds?: {
|
||
minX: number;
|
||
maxX: number;
|
||
minY: number;
|
||
maxY: number;
|
||
};
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update', payload: { id: string; patch: Partial<NativeElement> }): void;
|
||
(e: 'select', id: string): void;
|
||
(e: 'dragging', payload: { id: string; rect: { x: number; y: number; w: number; h: number }; active: boolean }): void;
|
||
}>();
|
||
|
||
const handles = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
|
||
|
||
const wrapperStyle = computed(() => ({
|
||
position: 'absolute',
|
||
left: `${props.element.x}mm`,
|
||
top: `${props.element.y}mm`,
|
||
width: `${props.element.w}mm`,
|
||
height: `${props.element.h}mm`,
|
||
zIndex: props.element.zIndex,
|
||
outline: props.active ? '1px solid #1677ff' : '1px dashed transparent',
|
||
userSelect: 'none',
|
||
}));
|
||
const movable = computed(() => props.movable !== false);
|
||
const resizable = computed(() => props.resizable !== false);
|
||
|
||
function clampByBounds(rect: { x: number; y: number; w: number; h: number }) {
|
||
const bounds = props.dragBounds;
|
||
if (!bounds) return rect;
|
||
return {
|
||
...rect,
|
||
x: Math.max(bounds.minX, Math.min(bounds.maxX, rect.x)),
|
||
y: Math.max(bounds.minY, Math.min(bounds.maxY, rect.y)),
|
||
};
|
||
}
|
||
|
||
function startDrag(event: PointerEvent) {
|
||
emit('select', props.element.id);
|
||
if ((event.target as HTMLElement)?.classList.contains('resize-handle')) return;
|
||
if (!movable.value) return;
|
||
const start = { x: props.element.x, y: props.element.y, w: props.element.w, h: props.element.h };
|
||
const startX = event.clientX;
|
||
const startY = event.clientY;
|
||
const onMove = (moveEvent: PointerEvent) => {
|
||
// 鼠标位移是 px,画布坐标是 mm,这里必须做单位换算才会跟手
|
||
const deltaX = (moveEvent.clientX - startX) / props.scale / PX_PER_MM;
|
||
const deltaY = (moveEvent.clientY - startY) / props.scale / PX_PER_MM;
|
||
const next = calcDragRect(start, { width: props.pageWidth, height: props.pageHeight }, deltaX, deltaY, props.gridSize);
|
||
const bounded = clampByBounds(next);
|
||
emit('update', { id: props.element.id, patch: bounded });
|
||
emit('dragging', { id: props.element.id, rect: bounded, active: true });
|
||
};
|
||
const onUp = () => {
|
||
window.removeEventListener('pointermove', onMove);
|
||
window.removeEventListener('pointerup', onUp);
|
||
emit('dragging', { id: props.element.id, rect: { x: props.element.x, y: props.element.y, w: props.element.w, h: props.element.h }, active: false });
|
||
};
|
||
window.addEventListener('pointermove', onMove);
|
||
window.addEventListener('pointerup', onUp);
|
||
}
|
||
|
||
function startResize(event: PointerEvent, direction: any) {
|
||
emit('select', props.element.id);
|
||
if (!resizable.value) return;
|
||
const start = { x: props.element.x, y: props.element.y, w: props.element.w, h: props.element.h };
|
||
const startX = event.clientX;
|
||
const startY = event.clientY;
|
||
const onMove = (moveEvent: PointerEvent) => {
|
||
// 鼠标位移是 px,画布尺寸是 mm,缩放时同样要按单位换算
|
||
const deltaX = (moveEvent.clientX - startX) / props.scale / PX_PER_MM;
|
||
const deltaY = (moveEvent.clientY - startY) / props.scale / PX_PER_MM;
|
||
const next = calcResizeRect(direction, start, { width: props.pageWidth, height: props.pageHeight }, deltaX, deltaY, props.gridSize);
|
||
emit('update', { id: props.element.id, patch: next });
|
||
};
|
||
const onUp = () => {
|
||
window.removeEventListener('pointermove', onMove);
|
||
window.removeEventListener('pointerup', onUp);
|
||
};
|
||
window.addEventListener('pointermove', onMove);
|
||
window.addEventListener('pointerup', onUp);
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.element-wrapper {
|
||
box-sizing: border-box;
|
||
|
||
.resize-handle {
|
||
position: absolute;
|
||
width: 6px;
|
||
height: 6px;
|
||
background: #1677ff;
|
||
border-radius: 50%;
|
||
margin: -3px;
|
||
}
|
||
.handle-nw {
|
||
left: 0;
|
||
top: 0;
|
||
cursor: nwse-resize;
|
||
}
|
||
.handle-n {
|
||
left: 50%;
|
||
top: 0;
|
||
cursor: ns-resize;
|
||
}
|
||
.handle-ne {
|
||
left: 100%;
|
||
top: 0;
|
||
cursor: nesw-resize;
|
||
}
|
||
.handle-e {
|
||
left: 100%;
|
||
top: 50%;
|
||
cursor: ew-resize;
|
||
}
|
||
.handle-se {
|
||
left: 100%;
|
||
top: 100%;
|
||
cursor: nwse-resize;
|
||
}
|
||
.handle-s {
|
||
left: 50%;
|
||
top: 100%;
|
||
cursor: ns-resize;
|
||
}
|
||
.handle-sw {
|
||
left: 0;
|
||
top: 100%;
|
||
cursor: nesw-resize;
|
||
}
|
||
.handle-w {
|
||
left: 0;
|
||
top: 50%;
|
||
cursor: ew-resize;
|
||
}
|
||
}
|
||
</style>
|