From ca81246b03843378799287e982fba9f27cfcfc7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=A5=94=E8=B7=91=E7=9A=84=E9=9D=A2=E6=9D=A1?=
<1262327911@qq.com>
Date: Mon, 15 Jun 2026 11:02:19 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=A1=86=E9=80=89?=
=?UTF-8?q?=E4=B8=8E=E6=8B=96=E6=8B=BD=E5=90=B8=E9=99=84=E6=96=B9=E6=A1=88?=
=?UTF-8?q?=EF=BC=8C=E6=8F=90=E9=AB=98=E6=80=A7=E8=83=BD=EF=BC=8C=E6=94=AF?=
=?UTF-8?q?=E6=8C=81=E7=BB=84=E4=BB=B6=E6=8B=96=E6=8B=BD=E5=88=B0=E7=94=BB?=
=?UTF-8?q?=E5=B8=83=E5=A4=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../components/EditAlignLine/index.ts | 3 -
.../components/EditAlignLine/index.vue | 285 ------------------
.../components/EditRange/index.vue | 8 +-
.../components/EditSelect/index.ts | 3 -
.../components/EditSelect/index.vue | 113 -------
.../chart/ContentEdit/hooks/useDrag.hook.ts | 247 ++++++++++++---
src/views/chart/ContentEdit/index.vue | 1 -
src/views/preview/utils/style.ts | 1 +
8 files changed, 210 insertions(+), 451 deletions(-)
delete mode 100644 src/views/chart/ContentEdit/components/EditAlignLine/index.ts
delete mode 100644 src/views/chart/ContentEdit/components/EditAlignLine/index.vue
delete mode 100644 src/views/chart/ContentEdit/components/EditSelect/index.ts
delete mode 100644 src/views/chart/ContentEdit/components/EditSelect/index.vue
diff --git a/src/views/chart/ContentEdit/components/EditAlignLine/index.ts b/src/views/chart/ContentEdit/components/EditAlignLine/index.ts
deleted file mode 100644
index d07271c8..00000000
--- a/src/views/chart/ContentEdit/components/EditAlignLine/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import EditAlignLine from './index.vue'
-
-export { EditAlignLine }
diff --git a/src/views/chart/ContentEdit/components/EditAlignLine/index.vue b/src/views/chart/ContentEdit/components/EditAlignLine/index.vue
deleted file mode 100644
index bfa30006..00000000
--- a/src/views/chart/ContentEdit/components/EditAlignLine/index.vue
+++ /dev/null
@@ -1,285 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/views/chart/ContentEdit/components/EditRange/index.vue b/src/views/chart/ContentEdit/components/EditRange/index.vue
index e8034055..f10054c2 100644
--- a/src/views/chart/ContentEdit/components/EditRange/index.vue
+++ b/src/views/chart/ContentEdit/components/EditRange/index.vue
@@ -3,10 +3,6 @@
-
-
-
-
@@ -18,9 +14,7 @@ import { useSizeStyle } from '../../hooks/useStyle.hook'
import { canvasModelIndex } from '@/settings/designSetting'
import { mousedownBoxSelect } from '../../hooks/useDrag.hook'
import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
-import { EditAlignLine } from '../EditAlignLine'
import { EditWatermark } from '../EditWatermark'
-import { EditSelect } from '../EditSelect'
const chartEditStore = useChartEditStore()
@@ -47,7 +41,7 @@ const rangeModelStyle = computed(() => {
position: relative;
transform-origin: left top;
background-size: cover;
- overflow: hidden;
+ overflow: visible;
@include fetch-border-color('hover-border-color');
@include fetch-bg-color('background-color2');
@include go(edit-range-model) {
diff --git a/src/views/chart/ContentEdit/components/EditSelect/index.ts b/src/views/chart/ContentEdit/components/EditSelect/index.ts
deleted file mode 100644
index 88b3334f..00000000
--- a/src/views/chart/ContentEdit/components/EditSelect/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import EditSelect from './index.vue'
-
-export { EditSelect }
diff --git a/src/views/chart/ContentEdit/components/EditSelect/index.vue b/src/views/chart/ContentEdit/components/EditSelect/index.vue
deleted file mode 100644
index 5526e8ed..00000000
--- a/src/views/chart/ContentEdit/components/EditSelect/index.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/views/chart/ContentEdit/hooks/useDrag.hook.ts b/src/views/chart/ContentEdit/hooks/useDrag.hook.ts
index 758680ff..be7a6365 100644
--- a/src/views/chart/ContentEdit/hooks/useDrag.hook.ts
+++ b/src/views/chart/ContentEdit/hooks/useDrag.hook.ts
@@ -5,7 +5,10 @@ import { ConfigType } from '@/packages/index.d'
import { CreateComponentType, CreateComponentGroupType, PickCreateComponentType } from '@/packages/index.d'
import { useContextMenu } from '@/views/chart/hooks/useContextMenu.hook'
import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
+import { useDesignStore } from '@/store/modules/designStore/designStore'
+import { useSettingStore } from '@/store/modules/settingStore/settingStore'
import { EditCanvasTypeEnum } from '@/store/modules/chartEditStore/chartEditStore.d'
+import { selectBoxIndex } from '@/settings/designSetting'
import { loadingStart, loadingFinish, loadingError, setComponentPosition, JSONParse } from '@/utils'
import { throttle, cloneDeep } from 'lodash'
@@ -73,6 +76,23 @@ export const mousedownBoxSelect = (e: MouseEvent, item?: CreateComponentType | C
mousedownHandleUnStop(e)
+ // 框选容器(.go-edit-range),框选框会 append 到这里
+ const container = e.currentTarget as HTMLElement
+ // 主题色(每次框选时读取,跟随主题切换)
+ const themeColor = useDesignStore().getAppTheme
+
+ // 创建框选框 DOM(鼠标抬起时销毁,不走 store 响应式,避免卡顿)
+ // 将主题色(hex)转为带透明度的背景色
+ const hexToRgba = (hex: string, alpha: number) => {
+ const r = parseInt(hex.slice(1, 3), 16)
+ const g = parseInt(hex.slice(3, 5), 16)
+ const b = parseInt(hex.slice(5, 7), 16)
+ return `rgba(${r},${g},${b},${alpha})`
+ }
+ const selectEl = document.createElement('div')
+ selectEl.style.cssText = `position:absolute;left:0;top:0;z-index:${selectBoxIndex};pointer-events:none;border:1px dashed ${themeColor};background:${hexToRgba(themeColor, 0.06)};`
+ container.appendChild(selectEl)
+
// 记录点击初始位置
const startOffsetX = e.offsetX
const startOffsetY = e.offsetY
@@ -82,8 +102,6 @@ export const mousedownBoxSelect = (e: MouseEvent, item?: CreateComponentType | C
// 记录缩放
const scale = chartEditStore.getEditCanvas.scale
- chartEditStore.setMousePosition(undefined, undefined, startOffsetX, startOffsetY)
-
// 移动框选
const mousemove = throttle((moveEvent: MouseEvent) => {
// 取消当前选中
@@ -93,7 +111,6 @@ export const mousedownBoxSelect = (e: MouseEvent, item?: CreateComponentType | C
// 这里先把相对值算好,不然组件无法获取 startScreenX 和 startScreenY 的值
const currX = startOffsetX + moveEvent.screenX - startScreenX
const currY = startOffsetY + moveEvent.screenY - startScreenY
- chartEditStore.setMousePosition(currX, currY)
// 计算框选的左上角和右下角
const selectAttr = {
@@ -130,11 +147,20 @@ export const mousedownBoxSelect = (e: MouseEvent, item?: CreateComponentType | C
selectAttr.y2 = startOffsetY
}
+ // 直接操作 DOM 绘制框选框,绕开 store 响应式
+ const left = Math.min(selectAttr.x1, selectAttr.x2)
+ const top = Math.min(selectAttr.y1, selectAttr.y2)
+ const width = Math.abs(selectAttr.x2 - selectAttr.x1)
+ const height = Math.abs(selectAttr.y2 - selectAttr.y1)
+ selectEl.style.left = `${left}px`
+ selectEl.style.top = `${top}px`
+ selectEl.style.width = `${width}px`
+ selectEl.style.height = `${height}px`
+
// 遍历组件
chartEditStore.getComponentList.forEach(item => {
if (!chartEditStore.getTargetChart.selectId.includes(item.id)) {
// 处理左上角
- let isSelect = false
const { x, y, w, h } = item.attr
const targetAttr = {
// 左上角
@@ -144,16 +170,15 @@ export const mousedownBoxSelect = (e: MouseEvent, item?: CreateComponentType | C
x2: x + w,
y2: y + h
}
- // 全包含则选中
+ // 部分相交即选中
if (
- targetAttr.x1 - selectAttr.x1 >= 0 &&
- targetAttr.y1 - selectAttr.y1 >= 0 &&
- targetAttr.x2 - selectAttr.x2 <= 0 &&
- targetAttr.y2 - selectAttr.y2 <= 0 &&
+ targetAttr.x1 < selectAttr.x2 &&
+ targetAttr.x2 > selectAttr.x1 &&
+ targetAttr.y1 < selectAttr.y2 &&
+ targetAttr.y2 > selectAttr.y1 &&
!item.status.lock &&
!item.status.hide
) {
- isSelect = true
chartEditStore.setTargetSelectChart(item.id, true)
}
}
@@ -166,6 +191,8 @@ export const mousedownBoxSelect = (e: MouseEvent, item?: CreateComponentType | C
mousemove.cancel()
chartEditStore.setEditCanvas(EditCanvasTypeEnum.IS_SELECT, false)
chartEditStore.setMousePosition(0, 0, 0, 0)
+ // 销毁框选框 DOM
+ selectEl.remove()
document.removeEventListener('mousemove', mousemove)
document.removeEventListener('mouseup', mouseup)
}
@@ -173,6 +200,128 @@ export const mousedownBoxSelect = (e: MouseEvent, item?: CreateComponentType | C
document.addEventListener('mouseup', mouseup)
}
+// ---- 对齐线 DOM 操作(纯命令式,不走 Vue 响应式)----
+
+let _alignEls: HTMLElement[] = []
+
+const _clearAlignLines = () => {
+ _alignEls.forEach(el => el.remove())
+ _alignEls = []
+}
+
+const _drawAlignLine = (
+ container: HTMLElement,
+ isRow: boolean,
+ fixedCoord: number,
+ spanStart: number,
+ spanEnd: number,
+ label: string,
+ color: string
+) => {
+ if (spanStart >= spanEnd) return
+ const el = document.createElement('div')
+ if (isRow) {
+ // 横线:固定 top,left~width 表示水平范围
+ el.style.cssText = `position:absolute;pointer-events:none;z-index:9999;background:${color};left:${spanStart}px;top:${fixedCoord}px;width:${spanEnd - spanStart}px;height:1px;`
+ } else {
+ // 竖线:固定 left,top~height 表示垂直范围
+ el.style.cssText = `position:absolute;pointer-events:none;z-index:9999;background:${color};left:${fixedCoord}px;top:${spanStart}px;width:1px;height:${spanEnd - spanStart}px;`
+ }
+ const tag = document.createElement('span')
+ tag.textContent = label
+ tag.style.cssText = `position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:${color};color:#fff;font-size:11px;line-height:1;padding:2px 5px;border-radius:2px;white-space:nowrap;`
+ el.appendChild(tag)
+ container.appendChild(el)
+ _alignEls.push(el)
+}
+
+type AlignRef = { id: string; attr: { x: number; y: number; w: number; h: number } }
+
+/**
+ * 吸附对齐:检测 sel 与所有 refs 的边界对齐,返回吸附后坐标并绘制对齐线。
+ * 对齐线只绘制两个对象之间的覆盖范围,中间显示吸附前偏差 px。
+ */
+const _runAlignSnap = (
+ container: HTMLElement,
+ selectId: string,
+ sel: { x: number; y: number; w: number; h: number },
+ refs: AlignRef[],
+ color: string,
+ minDist: number
+): { x: number; y: number } => {
+ _clearAlignLines()
+
+ const { x: sx, y: sy, w: sw, h: sh } = sel
+
+ type Hit = { coord: number; newOrig: number; delta: number; ref: AlignRef }
+ let bestX: Hit | null = null
+ let bestY: Hit | null = null
+
+ for (const ref of refs) {
+ if (ref.id === selectId) continue
+ const { x: cx, y: cy, w: cw, h: ch } = ref.attr
+ const cR = cx + cw, cB = cy + ch
+ const cCx = cx + cw / 2, cCy = cy + ch / 2
+
+ // X 对齐检测 → 竖线
+ // [sel边, ref边, 吸附后的新 sx]
+ const xChecks: [number, number, number][] = [
+ [sx, cx, cx], // 左-左
+ [sx, cCx, cCx], // 左-中
+ [sx, cR, cR], // 左-右
+ [sx+sw/2, cx, cx-sw/2], // 中-左
+ [sx+sw/2, cCx, cCx-sw/2], // 中-中
+ [sx+sw/2, cR, cR-sw/2], // 中-右
+ [sx+sw, cx, cx-sw], // 右-左
+ [sx+sw, cCx, cCx-sw], // 右-中
+ [sx+sw, cR, cR-sw], // 右-右
+ ]
+ for (const [a, b, newSx] of xChecks) {
+ const delta = Math.abs(a - b)
+ if (delta <= minDist && (!bestX || delta < bestX.delta)) {
+ bestX = { coord: b, newOrig: newSx, delta, ref }
+ }
+ }
+
+ // Y 对齐检测 → 横线
+ const yChecks: [number, number, number][] = [
+ [sy, cy, cy],
+ [sy, cCy, cCy],
+ [sy, cB, cB],
+ [sy+sh/2, cy, cy-sh/2],
+ [sy+sh/2, cCy, cCy-sh/2],
+ [sy+sh/2, cB, cB-sh/2],
+ [sy+sh, cy, cy-sh],
+ [sy+sh, cCy, cCy-sh],
+ [sy+sh, cB, cB-sh],
+ ]
+ for (const [a, b, newSy] of yChecks) {
+ const delta = Math.abs(a - b)
+ if (delta <= minDist && (!bestY || delta < bestY.delta)) {
+ bestY = { coord: b, newOrig: newSy, delta, ref }
+ }
+ }
+ }
+
+ const finalX = bestX ? Math.round(bestX.newOrig) : sx
+ const finalY = bestY ? Math.round(bestY.newOrig) : sy
+
+ if (bestX) {
+ const { y: ry, h: rh } = bestX.ref.attr
+ const spanStart = Math.min(finalY, ry)
+ const spanEnd = Math.max(finalY + sh, ry + rh)
+ _drawAlignLine(container, false, bestX.coord, spanStart, spanEnd, `${Math.round(bestX.delta)}px`, color)
+ }
+ if (bestY) {
+ const { x: rx, w: rw } = bestY.ref.attr
+ const spanStart = Math.min(finalX, rx)
+ const spanEnd = Math.max(finalX + sw, rx + rw)
+ _drawAlignLine(container, true, bestY.coord, spanStart, spanEnd, `${Math.round(bestY.delta)}px`, color)
+ }
+
+ return { x: finalX, y: finalY }
+}
+
// * 鼠标事件
export const useMouseHandle = () => {
// * Click 事件, 松开鼠标触发
@@ -212,8 +361,9 @@ export const useMouseHandle = () => {
if (e.buttons === MouseEventButton.RIGHT) return
const scale = chartEditStore.getEditCanvas.scale
- const canvasWidth = chartEditStore.getEditCanvasConfig.width
- const canvasHeight = chartEditStore.getEditCanvasConfig.height
+ // 读取对齐配置(在 mousedown 时快照,避免拖拽过程中反复读 store)
+ const themeColor = useDesignStore().getAppTheme
+ const minDist = useSettingStore().getChartAlignRange
// 记录图表初始位置和大小
const targetMap = new Map()
@@ -233,57 +383,76 @@ export const useMouseHandle = () => {
let prevComponentInstance: Array = []
chartEditStore.getTargetChart.selectId.forEach(id => {
if (!targetMap.has(id)) return
-
const index = chartEditStore.fetchTargetIndex(id)
- // 拿到初始位置数据
prevComponentInstance.push(cloneDeep(chartEditStore.getComponentList[index]))
})
// 记录初始位置
chartEditStore.setMousePosition(undefined, undefined, startX, startY)
+ // 对齐线直接绘制到画布容器
+ const alignContainer = document.querySelector('.go-edit-range') as HTMLElement | null
+
// 移动-计算偏移量
const mousemove = throttle((moveEvent: MouseEvent) => {
chartEditStore.setEditCanvas(EditCanvasTypeEnum.IS_DRAG, true)
chartEditStore.setMousePosition(moveEvent.screenX, moveEvent.screenY)
- // 当前偏移量,处理 scale 比例问题
- let offsetX = (moveEvent.screenX - startX) / scale
- let offsetY = (moveEvent.screenY - startY) / scale
+ const offsetX = (moveEvent.screenX - startX) / scale
+ const offsetY = (moveEvent.screenY - startY) / scale
- chartEditStore.getTargetChart.selectId.forEach(id => {
+ const currentSelectIds = chartEditStore.getTargetChart.selectId
+
+ if (currentSelectIds.length === 1) {
+ // 单选:做对齐吸附
+ const id = currentSelectIds[0]
if (!targetMap.has(id)) return
-
- const index = chartEditStore.fetchTargetIndex(id)
- // 拿到初始位置数据
const { x, y, w, h } = targetMap.get(id)
- const componentInstance = chartEditStore.getComponentList[index]
-
let currX = Math.round(x + offsetX)
let currY = Math.round(y + offsetY)
- // 要预留的距离
- const distance = 50
-
- // 基于左上角位置检测
- currX = currX < -w + distance ? -w + distance : currX
- currY = currY < -h + distance ? -h + distance : currY
-
- // 基于右下角位置检测
- currX = currX > canvasWidth - distance ? canvasWidth - distance : currX
- currY = currY > canvasHeight - distance ? canvasHeight - distance : currY
- if (componentInstance) {
- componentInstance.attr = Object.assign(componentInstance.attr, {
- x: currX,
- y: currY
+ if (alignContainer) {
+ // 构造参照列表(所有其他组件 + 画布)
+ // 直接引用 attr,避免每帧构造新对象
+ const refs: AlignRef[] = (chartEditStore.getComponentList as Array).map(c => ({
+ id: c.id,
+ attr: c.attr as { x: number; y: number; w: number; h: number }
+ }))
+ refs.push({
+ id: '__canvas__',
+ attr: { x: 0, y: 0, w: chartEditStore.getEditCanvasConfig.width, h: chartEditStore.getEditCanvasConfig.height }
})
+ const snapped = _runAlignSnap(alignContainer, id, { x: currX, y: currY, w, h }, refs, themeColor, minDist)
+ currX = snapped.x
+ currY = snapped.y
}
- })
- return
+
+ const index = chartEditStore.fetchTargetIndex(id)
+ const componentInstance = chartEditStore.getComponentList[index]
+ if (componentInstance) {
+ componentInstance.attr = Object.assign(componentInstance.attr, { x: currX, y: currY })
+ }
+ } else {
+ // 多选:无对齐,直接移动
+ _clearAlignLines()
+ currentSelectIds.forEach(id => {
+ if (!targetMap.has(id)) return
+ const index = chartEditStore.fetchTargetIndex(id)
+ const { x, y } = targetMap.get(id)
+ const componentInstance = chartEditStore.getComponentList[index]
+ if (componentInstance) {
+ componentInstance.attr = Object.assign(componentInstance.attr, {
+ x: Math.round(x + offsetX),
+ y: Math.round(y + offsetY)
+ })
+ }
+ })
+ }
}, 20)
const mouseup = () => {
try {
+ _clearAlignLines()
chartEditStore.setMousePosition(0, 0, 0, 0)
chartEditStore.setEditCanvas(EditCanvasTypeEnum.IS_DRAG, false)
// 加入历史栈
diff --git a/src/views/chart/ContentEdit/index.vue b/src/views/chart/ContentEdit/index.vue
index 6e917f5f..975b8bfa 100644
--- a/src/views/chart/ContentEdit/index.vue
+++ b/src/views/chart/ContentEdit/index.vue
@@ -205,7 +205,6 @@ onMounted(() => {
@include background-image('background-point');
@include goId('chart-edit-content') {
- overflow: hidden;
height: 100%;
@extend .go-transition;
@include fetch-theme('box-shadow');
diff --git a/src/views/preview/utils/style.ts b/src/views/preview/utils/style.ts
index 945aa8c0..ac34e1ea 100644
--- a/src/views/preview/utils/style.ts
+++ b/src/views/preview/utils/style.ts
@@ -51,6 +51,7 @@ export const getEditCanvasConfigStyle = (canvas: EditCanvasConfigType) => {
}
return {
position: 'relative' as const,
+ overflow: 'hidden' as const,
width: canvas.width ? `${canvas.width || 100}px` : '100%',
height: canvas.height ? `${canvas.height}px` : '100%',
...computedBackground