import _ from 'lodash' import localforage from 'localforage' import type EntityManager from './EntityManager' import { markRaw, reactive, ref } from 'vue' import type Viewport from '@/core/engine/Viewport.ts' import { getQueryParams, setQueryParam } from '@/utils/webutils.ts' // 差异类型定义 interface DataDiff { added: ItemJson[] removed: ItemJson[] updated: { before: ItemJson; after: ItemJson }[] } // 历史记录项 interface HistoryStep { diff: DataDiff timestamp: number } /** * 数据状态管理器 * 他能够对数据进行载入, 保存草稿, 载入草稿, 数据增删改查,撤销、重做等操作 * 最终他会将数据的变化同步到 EntityManager 中 * 主要功能包括: * 1. 管理场景数据的读取、保存, 以及临时保存、临时读取等功能 * 2. 管理撤销、重做功能 * 3. 交互控制组件通过如下步骤修改数据 * - 1. 调用 beginStateUpdate 开始修改数据 * - 2. 直接修改 vdata 数据 * - 3. 调用 endStateUpdate 完成数据修改 * 4. 内部如果进行了撤销、还原等操作,会通过 syncDataState() 方法将 vdata 数据与 EntityManager 进行同步 * 5. 注意,如果正在读取中,需要设置 isLoading = true,外部需要等待加载完成后再进行操作 * * 主要难点: * - 单张地图数据量可能超过 10000 个对象, 需要高效的管理数据状态 * * // 用例1, 修改 * stateManager.beginStateUpdate();stateManager.vdata.items[1].tf[0] = [-10, 0, 4];stateManager.endStateUpdate(); * stateManager.undo() * stateManager.redo() * * // 用例2 添加 * stateManager.beginStateUpdate();stateManager.vdata.items[3].dt.center.push('p5');stateManager.vdata.items.push({ id: 'p5', t: 'measure', tf: [[-6.0, 0, 8], [0, 0, 0], [0.25, 0.1, 0.25]], dt: { center: ['p4'] } });stateManager.endStateUpdate(); * stateManager.undo(); * stateManager.redo(); * * // 用例3 删除 * stateManager.beginStateUpdate(); stateManager.vdata.items.splice(3, 1); stateManager.endStateUpdate(); * stateManager.undo() * stateManager.redo() * * // 用例4 多连线 * stateManager.beginStateUpdate();stateManager.vdata.items[0].dt.center.push('p3');stateManager.vdata.items[2].dt.center.push('p1');stateManager.endStateUpdate(); * stateManager.undo() * * // 用例5 删除线 * stateManager.beginStateUpdate();stateManager.vdata.items[0].dt.center=[];stateManager.vdata.items[1].dt.center.splice(0,1);stateManager.endStateUpdate(); * stateManager.undo() */ export default class StateManager { /** * 唯一场景标识符, 用于做临时存储的 key */ readonly id: string /** * 实体对象, 用于同步当前场景的状态 */ readonly entityManager: EntityManager /** * 是否发生了变化,通知外部是否需要保存数据 */ readonly isChanged = ref(false) /** * 是否正在加载数据,通知外部是否需要等待加载完成 */ readonly isLoading = ref(false) readonly storeKey: string /** * 当前场景数据 */ vdata: VData /** * 使用循环缓冲区存储历史记录 */ private historySteps: HistoryStep[] = [] private historyIndex = -1 private maxHistorySteps = 20 // 标记是否有未保存的更改 private pendingChanges = false private isAutoSavingPaused = false private autoSaveTimer: number | null = null private isUpdating = false // 变化追踪器 private readonly changeTracker: DataDiff = { added: [], removed: [], updated: [] } /** * 数据快照(用于差异计算) */ private lastStateDict = new Map() /** * @param id 唯一场景标识符, 用于做临时存储的 key * @param viewport 视口对象, 用于获取、同步当前场景的状态 * @param maxHistorySteps 最大回撤步数 默认为 20 */ constructor(id: string, viewport: Viewport, maxHistorySteps = 20) { this.id = id this.storeKey = `-tmp-yvan-lcc-${this.id}` this.entityManager = viewport.entityManager this.maxHistorySteps = maxHistorySteps // this.historySteps = Array.from({ length: this.maxHistorySteps }, () => null) this.historySteps = [] this.pendingChanges = false this.isAutoSavingPaused = false } /** * 开始用户操作(创建数据快照) */ beginStateUpdate(): void { this.lastStateDict = new Map(this.vdata.items.map(item => [item.id, _.cloneDeep(item)])) this.changeTracker.added.length = 0 this.changeTracker.removed.length = 0 this.changeTracker.updated.length = 0 this.isUpdating = true } /** * 结束用户操作(计算差异并保存) */ endStateUpdate(option = { autoSave: true }): void { this.calculateDiff() this.saveStep() this.syncDataState(this.changeTracker) this.isChanged.value = true this.pendingChanges = true // 标记有需要保存的更改 this.isUpdating = false if (option.autoSave) { this.startAutoSave() // 触发自动保存 } } // 差异反转方法 private invertDiff(diff: DataDiff): DataDiff { return { added: diff.removed, removed: diff.added, updated: diff.updated.map(u => ({ before: u.after, after: u.before })) } } // 计算当前状态与快照的差异 private calculateDiff() { const currentMap = new Map(this.vdata.items.map(item => [item.id, item])) const added: ItemJson[] = [] const removed: ItemJson[] = [] const updated: { before: ItemJson; after: ItemJson }[] = [] // 检查删除 & 更新 for (const [id, item] of this.lastStateDict.entries()) { if (!currentMap.has(id)) { removed.push(item) } else if (!_.isEqual(item, currentMap.get(id))) { updated.push({ before: item, after: currentMap.get(id) }) } } // 检查新增 for (const [id, item] of currentMap.entries()) { if (!this.lastStateDict.has(id)) { added.push(item) } } this.changeTracker.added = added this.changeTracker.removed = removed this.changeTracker.updated = updated } /** * 保存差异到历史记录 */ private saveStep() { const { added, removed, updated } = this.changeTracker if (added.length === 0 && removed.length === 0 && updated.length === 0) return // 深拷贝差异对象 const clonedDiff = { added: _.cloneDeep(added), removed: _.cloneDeep(removed), updated: _.cloneDeep(updated) } const step: HistoryStep = { diff: clonedDiff, timestamp: Date.now() } if (this.historySteps.length >= this.maxHistorySteps) { this.historySteps.shift() } this.historySteps.push(step) this.historyIndex = this.historySteps.length - 1 this.pendingChanges = true } /** * 将当前数据 与 entityManager 进行同步, 对比出不同的部分,分别进行更新 * - 调用 entityManager.beginEntityUpdate() 开始更新 * - 调用 entityManager.createOrUpdateEntity(vdataItem) 添加场景中新的实体 * - 调用 entityManager.deleteEntity(id) 删除场景中的实体 * - 调用 entityManager.endEntityUpdate() 结束更新场景 */ syncDataState(diff: DataDiff): void { // 没有变化时跳过同步 if ( diff.added.length === 0 && diff.removed.length === 0 && diff.updated.length === 0 ) { return } console.log('[StateManager] 同步数据状态,变更:', diff) this.entityManager.beginEntityUpdate() for (const item of diff.added) { this.entityManager.createOrUpdateEntity(item) } for (const { after } of diff.updated) { this.entityManager.createOrUpdateEntity(after) } // 最后处理删除 for (const item of diff.removed) { this.entityManager.deleteEntity(item.id) } this.entityManager.endEntityUpdate() // 从实体管理器中获取最新数据 const updatedItems = this.entityManager.getAllEntities() this.vdata.items = Array.from(updatedItems.values()) } /** * 撤销 */ undo() { if (!this.undoEnabled()) return const step = this.historySteps[this.historyIndex] if (!step) return // 生成和应用反转差异 const reverseDiff = this.invertDiff(step.diff) this.applyDiff(reverseDiff) this.syncDataState(reverseDiff) this.historyIndex-- this.isChanged.value = true this.pendingChanges = true system.msg('撤销完成') this.startAutoSave() } /** * 重做 */ redo() { if (!this.redoEnabled()) return this.historyIndex++ const step = this.historySteps[this.historyIndex] if (!step) return this.applyDiff(step.diff) this.syncDataState(step.diff) this.isChanged.value = true this.pendingChanges = true system.msg('重做完成') this.startAutoSave() } startAutoSave() { this.isAutoSavingPaused = false if (this.autoSaveTimer !== null) { window.clearTimeout(this.autoSaveTimer) this.autoSaveTimer = null } // 设置新的定时器,在 5 秒后尝试保存 this.queueAutoSave() } stopAutoSave() { if (this.autoSaveTimer !== null) { window.clearTimeout(this.autoSaveTimer) this.autoSaveTimer = null } this.isAutoSavingPaused = true } private queueAutoSave() { if (this.autoSaveTimer !== null || this.isAutoSavingPaused) return if (!this.pendingChanges) return this.autoSaveTimer = window.setTimeout(async () => { this.autoSaveTimer = null if (this.isUpdating) { const checkInterval = setInterval(() => { if (!this.isUpdating) { clearInterval(checkInterval) if (!this.isAutoSavingPaused) { this.queueAutoSave() } } else { console.log('wait for entityManager to finish updating...') } }, 200) return } await this.saveToLocalstore() }, 5000) } /** * 从外部加载数据 */ async load(data: Partial) { this.isLoading.value = true this.historySteps = Array.from({ length: this.maxHistorySteps }, () => null) this.historyIndex = -1 try { // 停止自动保存,避免在加载过程中触发 this.stopAutoSave() // 直接替换数组引用(避免响应式开销) //@ts-ignore this.vdata = { id: this.id, isChanged: false, ...data } // 同步到视口 this.fullSync() this.isChanged.value = false // 强制保存一次初始状态 await this.saveToLocalstore() console.log('[StateManager] 加载完成,共', data.items.length, '个对象') } finally { this.isLoading.value = false } } // 差异应用方法 private applyDiff(diff: DataDiff) { const { added, removed, updated } = diff // 删除 const removedIds = new Set(removed.map(item => item.id)) _.remove(this.vdata.items, item => removedIds.has(item.id)) // 新增 const addedIds = new Set(added.map(i => i.id)) _.remove(this.vdata.items, item => addedIds.has(item.id)) this.vdata.items.push(...added) // 更新 const updateMap = new Map(updated.map(u => [u.after.id, u.after])) for (const [key, after] of updateMap) { const idx = _.findIndex(this.vdata.items, (item => item.id === key)) if (idx >= 0) { Object.assign(this.vdata.items[idx], after) } else { this.vdata.items.push(after) } } } private fullSync() { this.entityManager.beginEntityUpdate() this.vdata.items.forEach(item => { this.entityManager.createOrUpdateEntity(item) }) this.entityManager.endEntityUpdate() // 初始状态作为第一步 this.beginStateUpdate() this.endStateUpdate({ autoSave: false }) this.pendingChanges = false } undoEnabled() { return this.historyIndex >= 0 } redoEnabled() { const nextIndex = (this.historyIndex + 1) % this.maxHistorySteps return !!this.historySteps[nextIndex] } /** * 保存数据到外部 */ async save(): Promise { return _.cloneDeep(this.vdata) } /** * 保存到本地存储 浏览器indexDb(防止数据丢失) */ async saveToLocalstore() { await localforage.setItem(this.storeKey, this.vdata) console.log('[StateManager] 数据已保存草稿:' + this.storeKey + ', 共', this.vdata.items.length, '个对象') } /** * 从本地存储还原数据 */ async loadFromLocalstore(): Promise { try { this.isLoading.value = true const saved: VData = await localforage.getItem(this.storeKey) if (saved) { this.vdata = { ...saved } this.isChanged.value = saved.isChanged || false this.fullSync() // 同步到视口 console.log('[StateManager] 从本地存储恢复', this.vdata.items.length, '个对象') return true } return false } catch (error) { console.error('[StateManager] 从本地存储加载失败:', error) return false } finally { this.isLoading.value = false } } /** * 删除本地存储 */ async removeLocalstore() { try { await localforage.removeItem(this.storeKey) console.log('[StateManager] 本地存储已清除') } catch (error) { console.error('[StateManager] 清除本地存储失败:', error) } } /** * 销毁资源 */ destroy() { // 清理引用 delete this.vdata delete this.historySteps } /** * 尝试从草稿中读取数据 */ static async tryLoadCatalogFromLocalstore(): Promise<{ success: boolean, catalogCode?: string, catalog?: Catalog, stateManagerId?: string }> { // 获取 url 中的 stateManagerId // 从 localforage 中读取草稿数据 const stateManagerId = getQueryParams()?.get('store') if (stateManagerId) { const storeKey = `-tmp-yvan-lcc-${stateManagerId}` const saved: VData = await localforage.getItem(storeKey) if (saved && saved.catalog) { const catalogCode = saved.catalogCode || '' if (catalogCode) { return { success: true, catalogCode, catalog: saved.catalog, stateManagerId } } } } return { success: false } } }