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