Files
lingji-work-fe/src/views/components/drag/index.vue

460 lines
14 KiB
Vue
Raw Normal View History

2025-06-16 14:42:26 +08:00
<template>
<div class="app">
<DraggableResizable
v-for="comp in components"
:key="comp.id"
:id="comp.id"
:x="comp.x"
:y="comp.y"
:width="comp.width"
:height="comp.height"
:zIndex="comp.zIndex"
:snapDistance="snapDistance"
@drag="handleDrag"
@resize="handleResize"
:detectCollision="detectCollision"
:detectSnap="detectSnap"
:handleCollision="handleCollision"
:checkClosestComponent="getClosestComponent"
:setCurrentComponent="getCurrentComponent"
:directions="['right', 'bottom-right']"
/>
</div>
</template>
<script setup lang="ts">
//todo 1 增加布局切换的功能,且切换有过渡效果
//todo 2 吸附功能也需要增加过渡效果
//todo 3 resize的操作节点需要可配置并且允许用svg替代
//todo 4 拖拽区域需要可配置
//todo 5 增加drag预期位置显示并设置吸附距离达到吸附距离后立即移动到吸附后的位置并增加过渡效果
//todo 6 当预期位置侵占其他组件时,需要移动被侵占组件的位置
import DraggableResizable from './DraggableResizable.vue';
interface ComponentState {
id: string;
x: number;
y: number;
width: number;
height: number;
zIndex?: number | string;
}
interface SnapResult {
id?: string;
left: number;
top: number;
width: number;
height: number;
cx?: number;
cy?: number;
isSnap: boolean;
}
const snapDistance = 50;
const defaultComponents = [
{ id: 'comp1', x: 100, y: 100, width: 200, height: 200, zIndex: 1 },
{ id: 'comp2', x: 400, y: 100, width: 200, height: 200, zIndex: 1 },
{ id: 'comp3', x: 100, y: 400, width: 200, height: 200, zIndex: 1 },
{ id: 'comp4', x: 400, y: 400, width: 200, height: 200, zIndex: 1 },
];
const components = reactive<ComponentState[]>([]);
const currentComp = reactive<ComponentState>({
id: '',
x: 0,
y: 0,
width: 0,
height: 0,
});
const parentWidth = ref(0);
const parentHeight = ref(0);
const loadState = () => {
const savedState = localStorage.getItem('componentsState');
if (savedState) {
Object.assign(components, JSON.parse(savedState));
} else {
Object.assign(components, defaultComponents);
}
};
const saveState = () => {
localStorage.setItem('componentsState', JSON.stringify(components));
};
const handleDrag = (id: string, x: number, y: number) => {
const component = components.find((c) => c.id === id);
if (component) {
component.x = x;
component.y = y;
saveState();
}
};
const handleResize = (id: string, width: number, height: number) => {
const component = components.find((c) => c.id === id);
if (component) {
component.width = width;
component.height = height;
saveState();
}
};
type CollidedDirection = 'top' | 'right' | 'bottom' | 'left' | '';
/**
* 检测元素之间的碰撞
* @param id
* @param x
* @param y
* @param width
* @param height
*/
const detectCollision = (id: string, x: number, y: number, width: number, height: number) => {
const currentComponent = components.find((c) => c.id === id);
if (!currentComponent) return false;
let colliedComponent: ComponentState | null = null;
let collidedDirection: CollidedDirection = '';
const value = components.some((c) => {
if (c.id === id) return false;
colliedComponent = c;
return !(x + width <= c.x || x >= c.x + c.width || y + height <= c.y || y >= c.y + c.height);
});
if (value && colliedComponent) {
collidedDirection = getColliedDirection<ComponentState>(currentComponent, colliedComponent);
}
return { value, colliedComponent, collidedDirection };
};
/**
* 获取被碰撞组件的方向
* @param currentComponent
* @param colliedComponent
*/
const getColliedDirection = <T extends ComponentState>(currentComponent: T, colliedComponent: T): CollidedDirection => {
let collidedDirection: CollidedDirection = '';
if (currentComponent.x + currentComponent.width === colliedComponent.x) {
collidedDirection = 'left';
}
if (currentComponent.x === colliedComponent.x + colliedComponent.width) {
collidedDirection = 'right';
}
if (currentComponent.y + currentComponent.height === colliedComponent.y) {
collidedDirection = 'top';
}
if (currentComponent.y === colliedComponent.y + colliedComponent.height) {
collidedDirection = 'bottom';
}
return collidedDirection;
};
const getCurrentComponent = (currentComponent: {
width: string;
height: string;
top: string;
left: string;
id: string;
}): ComponentState => {
return Object.assign(currentComp, {
id: currentComponent.id,
x: parseInt(currentComponent.left),
y: parseInt(currentComponent.top),
width: parseInt(currentComponent.width),
height: parseInt(currentComponent.height),
});
};
/**
* 检测吸附
* @param id
* @param x
* @param y
* @param width
* @param height
* @param snapDistance
*/
const detectSnap = (id: string, x: number, y: number, width: number, height: number, snapDistance: number) => {
let snapResult: SnapResult = { left: x, top: y, width, height, cx: 0, cy: 0, isSnap: false };
const closestComponent = getClosestComponent(id, x, y, width, height);
// console.log('closestComponent', closestComponent);
//* 检测左边缘吸附
if (Math.abs(x - (closestComponent.x + closestComponent.width)) <= snapDistance) {
snapResult.left = closestComponent.x + closestComponent.width;
console.log('snap left', snapResult.left);
snapResult.isSnap = true;
}
//* 检测右边缘吸附
else if (Math.abs(x + width - closestComponent.x) <= snapDistance) {
snapResult.left = closestComponent.x - width;
console.log('snap right', snapResult.left);
snapResult.isSnap = true;
}
//* 检测上边缘吸附
else if (
Math.abs(y - (closestComponent.y + closestComponent.height)) <= snapDistance &&
(Math.abs(x + width - closestComponent.x) <= snapDistance || x + width >= closestComponent.x)
) {
snapResult.top = closestComponent.y + closestComponent.height;
console.log('snap top', snapResult.top);
snapResult.isSnap = true;
}
//* 检测下边缘吸附
else if (Math.abs(y + height - closestComponent.y) <= snapDistance) {
snapResult.top = closestComponent.y - height;
console.log('snap bottom', snapResult.top);
snapResult.isSnap = true;
} else {
snapResult.cx = closestComponent.x;
snapResult.cy = closestComponent.y;
}
//* 检测父元素边界吸附
if (x <= snapDistance) {
console.log('parent left');
snapResult.left = 0;
snapResult.isSnap = true;
}
if (y <= snapDistance) {
console.log('parent top');
snapResult.top = 0;
snapResult.isSnap = true;
}
if (parentWidth.value - (x + width) <= snapDistance) {
console.log('parent right');
snapResult.left = parentWidth.value - width;
snapResult.isSnap = true;
}
if (parentHeight.value - (y + height) <= snapDistance) {
console.log('parent bottom');
snapResult.top = parentHeight.value - height;
snapResult.isSnap = true;
}
// const result = adjustSnapResult(id, x, y, width, height, snapResult);
// Object.assign(snapResult, result);
return snapResult;
};
/**
* 调整吸附参数
* @param id
* @param x
* @param y
* @param width
* @param height
* @param snapResult
*/
const adjustSnapResult = (
id: string,
x: number,
y: number,
width: number,
height: number,
snapResult: SnapResult,
): SnapResult => {
const adjustPosition = adjustComponentPosition(id, x, y, width, height);
console.log('adjustPosition', adjustPosition, 'x', x, 'y', y);
console.log('currentComp', currentComp);
//* 如果调整值有负数,则直接返回原始坐标,不进行后续逻辑判断(暴力调整,暂不开启)
// if (adjustPosition.x < 0 || adjustPosition.y < 0) {
// snapResult.left = currentComp.x;
// snapResult.top = currentComp.y;
// return snapResult;
// }
//* 重新赋值,但需要避免不能将组件移动到负坐标上
if (adjustPosition.x !== x) {
if (adjustPosition.x <= 0) {
snapResult.left = x;
} else if (adjustPosition.x + width >= parentWidth.value) {
snapResult.left = parentWidth.value - width;
} else {
snapResult.left = adjustPosition.x;
}
snapResult.isSnap = true;
} else if (x < 0) {
snapResult.left = 0;
// snapResult.left = currentComp.x;
// snapResult.top = currentComp.y;
snapResult.isSnap = true;
}
if (adjustPosition.y !== y) {
snapResult.top = adjustPosition.y <= 0 ? 0 : adjustPosition.y;
snapResult.left = adjustPosition.x;
snapResult.isSnap = true;
}
//* 返回前最后一次检查
if (snapResult.left < 0 || snapResult.top < 0) {
snapResult.left = currentComp.x;
snapResult.top = currentComp.y;
}
return snapResult;
};
/**
* 计算距离最近的组件
* @param id
* @param x
* @param y
* @param width
* @param height
*/
const getClosestComponent = (id: string, x: number, y: number, width: number, height: number): ComponentState => {
const otherComponents = components.filter((comp) => comp.id !== id);
const calculateEdgeDistance = (
comp1: { x: number; y: number; width: number; height: number },
comp2: { x: number; y: number; width: number; height: number },
): number => {
const left = Math.abs(comp1.x - (comp2.x + comp2.width));
const right = Math.abs(comp1.x + comp1.width - comp2.x);
const top = Math.abs(comp1.y - (comp2.y + comp2.height));
const bottom = Math.abs(comp1.y + comp1.height - comp2.y);
const minX = Math.min(left, right);
const minY = Math.min(top, bottom);
return Math.min(minX, minY);
};
let closestComponent = otherComponents[0];
let shortestDistance = calculateEdgeDistance({ x, y, width, height }, closestComponent);
for (const component of otherComponents) {
const distance = calculateEdgeDistance({ x, y, width, height }, component);
if (distance < shortestDistance) {
shortestDistance = distance;
closestComponent = component;
}
}
return closestComponent;
};
const handleCollision = (id: string, x: number, y: number, width: number, height: number) => {
//* 仅检测碰撞,不移动其他组件
return detectCollision(id, x, y, width, height);
};
onMounted(() => {
parentWidth.value = document.querySelector('.app')!.clientWidth;
parentHeight.value = document.querySelector('.app')!.clientHeight;
loadState();
});
/**
* 是否与其他组件重叠
* @param comp1
* @param comp2
*/
const isOverlapping = (comp1: ComponentState, comp2: ComponentState): boolean => {
return !(
comp1.x >= comp2.x + comp2.width ||
comp1.x + comp1.width <= comp2.x ||
comp1.y >= comp2.y + comp2.height ||
comp1.y + comp1.height <= comp2.y
);
};
/**
* 调整当前组件位置
* @param id
* @param x
* @param y
* @param width
* @param height
*/
const adjustComponentPosition = (
id: string,
x: number,
y: number,
width: number,
height: number,
): { x: number; y: number } => {
const otherComponents = components.filter((comp) => comp.id !== id);
let adjustedX = x;
let adjustedY = y;
const adjustPosition = () => {
let adjusted = false;
for (const component of otherComponents) {
if (isOverlapping({ id, x: adjustedX, y: adjustedY, width, height }, component)) {
const right = component.x + component.width;
const bottom = component.y + component.height;
const leftOverlap = adjustedX < component.x;
const rightOverlap = adjustedX + width > component.x + component.width;
const topOverlap = adjustedY < component.y;
const bottomOverlap = adjustedY + height > component.y + component.height;
if (leftOverlap) {
console.log('leftOverlap');
adjustedX = component.x - width;
adjusted = true;
} else if (rightOverlap) {
console.log('rightOverlap', component);
if (component.x + component.width + width > parentWidth.value) {
console.log('rightOverlap overflow');
adjustedX = currentComp.x;
adjustedY = currentComp.y;
}
adjusted = true;
} else if (topOverlap) {
console.log('topOverlap');
adjustedY = component.y - height;
adjusted = true;
} else if (bottomOverlap) {
console.log('bottomOverlap');
adjustedY = bottom;
adjusted = true;
} else {
console.log('当前组件在其他组件内部');
}
//* 检查是否还是与其他组件重叠
let stillOverlapping = false;
for (const comp of otherComponents) {
if (isOverlapping({ id, x: adjustedX, y: adjustedY, width, height }, comp)) {
stillOverlapping = true;
break;
}
}
if (stillOverlapping) {
//* 恢复组件移动前的原始位置
console.log('stillOverlapping');
adjustedX = currentComp.x;
adjustedY = currentComp.y;
adjusted = true;
break;
}
}
}
return adjusted;
};
while (adjustPosition()) {
const prevX = adjustedX;
const prevY = adjustedY;
if (adjustPosition()) {
if (adjustedX === prevX && adjustedY === prevY) {
console.log('full overlap');
break;
}
}
}
return { x: adjustedX, y: adjustedY };
};
</script>
<style lang="less" scoped>
.app {
position: relative;
width: 100%;
height: 100%;
background-color: #f0f0f0;
overflow-x: hidden;
overflow-y: auto;
}
</style>