feat:撤销功能
This commit is contained in:
@@ -1,6 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<xn-form-container title="编辑项目阶段" :width="700" v-model:open="open" :destroy-on-close="true" @close="onClose">
|
<xn-form-container title="编辑项目阶段" :width="700" v-model:open="open" :destroy-on-close="true" @close="onClose">
|
||||||
<div class="stage-form">
|
<div class="stage-form">
|
||||||
|
<div class="stage-form__toolbar">
|
||||||
|
<a-space>
|
||||||
|
<a-tooltip title="撤销上一步修改">
|
||||||
|
<a-button :disabled="!canUndo" @click="undo">撤销</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip title="恢复撤销的修改">
|
||||||
|
<a-button :disabled="!canRedo" @click="redo">恢复</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
<div class="stage-form__header">
|
<div class="stage-form__header">
|
||||||
<div>阶段名称</div>
|
<div>阶段名称</div>
|
||||||
<div>阶段序号</div>
|
<div>阶段序号</div>
|
||||||
@@ -32,7 +42,7 @@
|
|||||||
import { createVNode } from 'vue'
|
import { createVNode } from 'vue'
|
||||||
import { Modal } from 'ant-design-vue'
|
import { Modal } from 'ant-design-vue'
|
||||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep, isEqual } from 'lodash-es'
|
||||||
// 抽屉状态
|
// 抽屉状态
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
const emit = defineEmits({ successful: null })
|
const emit = defineEmits({ successful: null })
|
||||||
@@ -42,10 +52,56 @@
|
|||||||
const visibleStages = computed(() => formData.value.filter((item) => !item.isDeleted))
|
const visibleStages = computed(() => formData.value.filter((item) => !item.isDeleted))
|
||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
|
|
||||||
|
// 撤销/恢复(类似 WPS / Ctrl+Z / Ctrl+Y)
|
||||||
|
const undoStack = ref([])
|
||||||
|
const redoStack = ref([])
|
||||||
|
const isRestoring = ref(false)
|
||||||
|
let historyTimer = null
|
||||||
|
const MAX_HISTORY = 50
|
||||||
|
|
||||||
|
const snapshotOf = (list) => cloneDeep(Array.isArray(list) ? list : [])
|
||||||
|
const initHistory = () => {
|
||||||
|
undoStack.value = [snapshotOf(formData.value)]
|
||||||
|
redoStack.value = []
|
||||||
|
}
|
||||||
|
const commitHistory = () => {
|
||||||
|
if (isRestoring.value) return
|
||||||
|
const next = snapshotOf(formData.value)
|
||||||
|
const last = undoStack.value[undoStack.value.length - 1]
|
||||||
|
if (last && isEqual(last, next)) return
|
||||||
|
undoStack.value.push(next)
|
||||||
|
if (undoStack.value.length > MAX_HISTORY) undoStack.value.shift()
|
||||||
|
redoStack.value = []
|
||||||
|
}
|
||||||
|
const canUndo = computed(() => undoStack.value.length > 1)
|
||||||
|
const canRedo = computed(() => redoStack.value.length > 0)
|
||||||
|
const applySnapshot = (snap) => {
|
||||||
|
isRestoring.value = true
|
||||||
|
formData.value = snapshotOf(snap)
|
||||||
|
nextTick(() => {
|
||||||
|
isRestoring.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const undo = () => {
|
||||||
|
if (!canUndo.value) return
|
||||||
|
const current = snapshotOf(formData.value)
|
||||||
|
const prev = undoStack.value[undoStack.value.length - 2]
|
||||||
|
redoStack.value.push(current)
|
||||||
|
undoStack.value.pop()
|
||||||
|
applySnapshot(prev)
|
||||||
|
}
|
||||||
|
const redo = () => {
|
||||||
|
if (!canRedo.value) return
|
||||||
|
const current = snapshotOf(formData.value)
|
||||||
|
const next = redoStack.value.pop()
|
||||||
|
undoStack.value.push(current)
|
||||||
|
applySnapshot(next)
|
||||||
|
}
|
||||||
|
|
||||||
const createEmptyStage = () => ({
|
const createEmptyStage = () => ({
|
||||||
stageId: '',
|
stageId: '',
|
||||||
stageName: '',
|
stageName: '',
|
||||||
projectId: projectId,
|
projectId: projectId.value,
|
||||||
stageSeq: visibleStages.value.length + 1,
|
stageSeq: visibleStages.value.length + 1,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
_key: `${Date.now()}-${Math.random()}`
|
_key: `${Date.now()}-${Math.random()}`
|
||||||
@@ -69,15 +125,19 @@
|
|||||||
if (!formData.value.length) {
|
if (!formData.value.length) {
|
||||||
formData.value.push(createEmptyStage())
|
formData.value.push(createEmptyStage())
|
||||||
}
|
}
|
||||||
|
initHistory()
|
||||||
}
|
}
|
||||||
// 关闭抽屉
|
// 关闭抽屉
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
formData.value = []
|
formData.value = []
|
||||||
open.value = false
|
open.value = false
|
||||||
|
undoStack.value = []
|
||||||
|
redoStack.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const addRow = () => {
|
const addRow = () => {
|
||||||
formData.value.push(createEmptyStage())
|
formData.value.push(createEmptyStage())
|
||||||
|
commitHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeRow = (stage) => {
|
const removeRow = (stage) => {
|
||||||
@@ -85,8 +145,22 @@
|
|||||||
if (!visibleStages.value.length) {
|
if (!visibleStages.value.length) {
|
||||||
formData.value.push(createEmptyStage())
|
formData.value.push(createEmptyStage())
|
||||||
}
|
}
|
||||||
|
commitHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 输入编辑用 watch 做快照(做个轻量防抖,避免每个字符都入栈)
|
||||||
|
watch(
|
||||||
|
formData,
|
||||||
|
() => {
|
||||||
|
if (isRestoring.value) return
|
||||||
|
if (historyTimer) clearTimeout(historyTimer)
|
||||||
|
historyTimer = setTimeout(() => {
|
||||||
|
commitHistory()
|
||||||
|
}, 300)
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
const submitStages = () => {
|
const submitStages = () => {
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
const submitList = formData.value.map(({ _key, ...rest }) => rest)
|
const submitList = formData.value.map(({ _key, ...rest }) => rest)
|
||||||
@@ -128,6 +202,10 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
.stage-form__toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
.stage-form__header,
|
.stage-form__header,
|
||||||
.stage-form__row {
|
.stage-form__row {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user