feat: 优化批处理和历史记录管理
This commit is contained in:
@@ -1,13 +1,44 @@
|
||||
import { setValue, getValue } from '@/utils'
|
||||
|
||||
// 最大历史记录长度
|
||||
const MAX_HISTORY_SIZE = 1000
|
||||
|
||||
const undoStack: any[] = shallowReactive([])
|
||||
const redoStack: any[] = shallowReactive([])
|
||||
|
||||
/** 所有 useUndoRedo() 共用,避免面板 batch 和工具栏 undo 各过各的 */
|
||||
let activeBatch: { target: any; key: string; oldValue: any; newValue: any }[] | null = null
|
||||
|
||||
export function useUndoRedo() {
|
||||
// 是否可以撤销
|
||||
const canUndo = computed(() => undoStack.length > 0)
|
||||
// 是否可以重做
|
||||
const canRedo = computed(() => redoStack.length > 0)
|
||||
|
||||
// 开始批处理(先提交上一批,避免 focus 重入把未提交记录丢掉)
|
||||
function startBatch() {
|
||||
if (activeBatch?.length) {
|
||||
pushRecord(activeBatch)
|
||||
}
|
||||
activeBatch = []
|
||||
}
|
||||
|
||||
// 提交批处理
|
||||
function commitBatch() {
|
||||
if (activeBatch?.length) {
|
||||
pushRecord(activeBatch)
|
||||
}
|
||||
activeBatch = null
|
||||
}
|
||||
|
||||
// 记录,如果超出最大历史记录长度,删除最早的记录
|
||||
function pushRecord(record: any) {
|
||||
undoStack.push(record)
|
||||
if (undoStack.length >= MAX_HISTORY_SIZE) {
|
||||
undoStack.shift()
|
||||
}
|
||||
}
|
||||
|
||||
// 应用更改
|
||||
function applyChange(target: any, key: string, newValue: any) {
|
||||
const oldValue = getValue(target, key)
|
||||
@@ -19,7 +50,19 @@ export function useUndoRedo() {
|
||||
oldValue,
|
||||
newValue,
|
||||
}
|
||||
undoStack.push(record)
|
||||
|
||||
if (activeBatch) {
|
||||
// 如果当前处于批处理模式,检查是否已经存在相同的记录,如果存在则更新新值,否则添加新的记录
|
||||
const _record = activeBatch.find((r) => r.target === target && r.key === key)
|
||||
if (_record) {
|
||||
_record.newValue = newValue
|
||||
} else {
|
||||
activeBatch.push(record)
|
||||
}
|
||||
} else {
|
||||
pushRecord([record])
|
||||
}
|
||||
|
||||
setValue(target, key, newValue)
|
||||
// 清空重做栈
|
||||
redoStack.length = 0
|
||||
@@ -27,24 +70,30 @@ export function useUndoRedo() {
|
||||
|
||||
// 撤销
|
||||
function undo() {
|
||||
const record = undoStack.pop()
|
||||
if (!record) return
|
||||
// 输入框还没 blur 时,先把当前编辑收成一条,再撤销
|
||||
commitBatch()
|
||||
const records = undoStack.pop()
|
||||
if (!records) return
|
||||
|
||||
;[...records].reverse().forEach((record: any) => {
|
||||
const { target, key, oldValue } = record
|
||||
|
||||
setValue(target, key, oldValue)
|
||||
redoStack.push(record)
|
||||
})
|
||||
|
||||
redoStack.push(records)
|
||||
}
|
||||
|
||||
// 重做
|
||||
function redo() {
|
||||
const record = redoStack.pop()
|
||||
if (!record) return
|
||||
commitBatch()
|
||||
const records = redoStack.pop()
|
||||
if (!records) return
|
||||
|
||||
records.forEach((record: any) => {
|
||||
const { target, key, newValue } = record
|
||||
|
||||
setValue(target, key, newValue)
|
||||
undoStack.push(record)
|
||||
})
|
||||
pushRecord(records)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -53,5 +102,7 @@ export function useUndoRedo() {
|
||||
applyChange,
|
||||
undo,
|
||||
redo,
|
||||
startBatch,
|
||||
commitBatch,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useUndoRedo } from '@/composables/useUndoRedo'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import type { OnDrag, OnResize } from 'vue3-moveable'
|
||||
|
||||
/**
|
||||
* Moveable 单选 / 组拖拽与缩放
|
||||
*/
|
||||
export const useMoveable = () => {
|
||||
export const useMoveable = (moveableRef: any) => {
|
||||
const editorStore = useEditorStore()
|
||||
const { applyChange, startBatch, commitBatch } = useUndoRedo()
|
||||
|
||||
/** 从节点 DOM 反查 store 节点 */
|
||||
const getNodeByTarget = (target: HTMLElement) => {
|
||||
@@ -14,16 +16,38 @@ export const useMoveable = () => {
|
||||
return editorStore.findNodeById(nodeId)
|
||||
}
|
||||
|
||||
// 当节点布局发生变化时,更新 moveable 的选框位置和大小
|
||||
watch(
|
||||
() => editorStore.nodes.map((node) => node.layout),
|
||||
() => {
|
||||
// 手动更新
|
||||
moveableRef.value?.updateRect(undefined, true)
|
||||
},
|
||||
{
|
||||
flush: 'post', // 在 DOM 更新后再更新
|
||||
},
|
||||
)
|
||||
|
||||
function onStart() {
|
||||
startBatch()
|
||||
}
|
||||
|
||||
function onEnd() {
|
||||
commitBatch()
|
||||
}
|
||||
|
||||
/** 拖拽节点 */
|
||||
const onDrag = (e: OnDrag) => {
|
||||
console.log('拖拽事件', e)
|
||||
e.target.style.setProperty('left', `${e.left}px`)
|
||||
e.target.style.setProperty('top', `${e.top}px`)
|
||||
const node = getNodeByTarget(e.target as HTMLElement)
|
||||
console.log('拖拽节点', node)
|
||||
|
||||
if (!node) return
|
||||
node.layout.x = e.left
|
||||
node.layout.y = e.top
|
||||
applyChange(node, 'layout', {
|
||||
...node.layout,
|
||||
x: e.left,
|
||||
y: e.top,
|
||||
})
|
||||
}
|
||||
|
||||
/** 缩放节点 */
|
||||
@@ -34,6 +58,12 @@ export const useMoveable = () => {
|
||||
if (!node) return
|
||||
node.layout.width = e.width
|
||||
node.layout.height = e.height
|
||||
applyChange(node, 'layout', {
|
||||
...node.layout,
|
||||
width: e.width,
|
||||
height: e.height,
|
||||
})
|
||||
|
||||
// 处理左侧圆点的拖拽,改变节点位置
|
||||
onDrag(e.drag)
|
||||
}
|
||||
@@ -53,5 +83,7 @@ export const useMoveable = () => {
|
||||
onResize,
|
||||
onDragGroup,
|
||||
onResizeGroup,
|
||||
onStart,
|
||||
onEnd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const canvasRootRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const { RULER_THICK, scale, lines, rulerCanvasSize, viewportSize, palette, onZoomChange } =
|
||||
useCanvasRuler(moveableRef, canvasRootRef)
|
||||
const { onDrag, onResize, onDragGroup, onResizeGroup } = useMoveable()
|
||||
const { onDrag, onResize, onDragGroup, onResizeGroup, onStart, onEnd } = useMoveable(moveableRef)
|
||||
const { selectedTarget, onSelect, onClearSelect, onSelectEnd } = useSelection(stageRef, moveableRef)
|
||||
|
||||
/** 设计稿白底:宽高和背景色立刻跟 store */
|
||||
@@ -155,9 +155,17 @@ const onCommand = (command: keyof typeof commendMap) => {
|
||||
:resizable="true"
|
||||
:origin="false"
|
||||
@drag="onDrag"
|
||||
@drag-start="onStart"
|
||||
@drag-end="onEnd"
|
||||
@drag-group="onDragGroup"
|
||||
@drag-group-start="onStart"
|
||||
@drag-group-end="onEnd"
|
||||
@resize="onResize"
|
||||
@resize-start="onStart"
|
||||
@resize-end="onEnd"
|
||||
@resize-group="onResizeGroup"
|
||||
@resize-group-start="onStart"
|
||||
@resize-group-end="onEnd"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useUndoRedo } from '@/composables/useUndoRedo'
|
||||
|
||||
defineProps(['setters', 'formData'])
|
||||
|
||||
const { applyChange } = useUndoRedo()
|
||||
const { applyChange, startBatch, commitBatch } = useUndoRedo()
|
||||
|
||||
const componentMap: Record<string, any> = {
|
||||
input: ElInput,
|
||||
@@ -22,6 +22,8 @@ const componentMap: Record<string, any> = {
|
||||
:is="componentMap[setter.type]"
|
||||
:model-value="getValue(formData, setter.key)"
|
||||
@update:model-value="(value: any) => applyChange(formData, setter.key, value)"
|
||||
@focus="startBatch()"
|
||||
@blur="commitBatch()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
+21
-8
@@ -1,6 +1,9 @@
|
||||
import type { MaterialSchema } from '@/schema/materials'
|
||||
import type { PageSchema } from '@/schema/page'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useUndoRedo } from '@/composables/useUndoRedo'
|
||||
|
||||
const { applyChange, commitBatch } = useUndoRedo()
|
||||
|
||||
export const useEditorStore = defineStore('editor', () => {
|
||||
// 面板显示状态
|
||||
@@ -34,9 +37,16 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
return nodes.value.find((node) => node.id === selectedNodeId.value) || null
|
||||
})
|
||||
|
||||
function setNodes(newNodes: MaterialSchema[]) {
|
||||
// 先收掉属性面板未提交的输入,避免和整表替换叠成一次撤销
|
||||
commitBatch()
|
||||
applyChange(nodes, 'value', newNodes)
|
||||
}
|
||||
|
||||
/* 添加节点 */
|
||||
function addNode(node: MaterialSchema) {
|
||||
nodes.value.push(node)
|
||||
// 记录操作到撤销栈
|
||||
setNodes([...nodes.value, node])
|
||||
}
|
||||
|
||||
/* 选中节点 */
|
||||
@@ -72,27 +82,30 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
|
||||
// 删除节点
|
||||
function removeNode(node: MaterialSchema) {
|
||||
nodes.value = nodes.value.filter((n) => n.id !== node.id)
|
||||
setNodes(nodes.value.filter((n) => n.id !== node.id))
|
||||
|
||||
selectedNodeIds.value = selectedNodeIds.value.filter((id) => id !== node.id)
|
||||
}
|
||||
|
||||
// 置顶节点
|
||||
function moveTop(node: MaterialSchema) {
|
||||
const index = nodes.value.findIndex((n) => n.id === node.id)
|
||||
nodes.value.splice(index, 1)
|
||||
nodes.value.unshift(node)
|
||||
const splicedNodes = nodes.value.splice(index, 1)
|
||||
|
||||
setNodes([node, ...splicedNodes])
|
||||
}
|
||||
|
||||
// 置顶节点
|
||||
// 置底节点
|
||||
function moveBottom(node: MaterialSchema) {
|
||||
const index = nodes.value.findIndex((n) => n.id === node.id)
|
||||
nodes.value.splice(index, 1)
|
||||
nodes.value.push(node)
|
||||
const splicedNodes = nodes.value.splice(index, 1)
|
||||
|
||||
setNodes([...splicedNodes, node])
|
||||
}
|
||||
|
||||
// 锁定节点
|
||||
function toggleLock(node: MaterialSchema) {
|
||||
node.locked = !node.locked
|
||||
applyChange(node, 'locked', !node.locked)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+6
-2
@@ -12,6 +12,10 @@ export function getValue(target: any, key: string) {
|
||||
export function setValue(target: any, key: string, value: any) {
|
||||
const keys = key.split('.')
|
||||
const lastKey = keys.pop()!
|
||||
const _target = getValue(target, keys.join('.'))
|
||||
_target[lastKey] = value
|
||||
|
||||
if (keys.length) {
|
||||
target = getValue(target, keys.join('.'))
|
||||
}
|
||||
|
||||
target[lastKey] = value
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user