/** * editorStore.ts - 编辑器状态管理 * * 使用Zustand管理块编辑器的状态,包括: * - 文档元数据 * - blocks数组 * - 选中状态 * - 加载/保存状态 * * @module stores/editorStore */ import { create } from 'zustand'; import pLimit from 'p-limit'; import hashSum from 'hash-sum'; import { message } from 'antd'; import type { DocumentBlock, PartialBlock, BlockUpdate, BlockType, TableBlock, } from '../types/editor'; import { blockService } from '../services/blockService'; import { normalizeTableBlock, serializeTableBlock } from '../utils/blockOperations'; // 并发保存限制器(最多同时进行 3 个请求,降低服务器压力) const saveConcurrencyLimit = pLimit(3); // 自动保存延迟时间(毫秒) const AUTO_SAVE_DELAY = 3000; // 3秒无操作后自动保存 // 最大重试次数 const MAX_RETRY_ATTEMPTS = 3; /** * 计算块内容的哈希值 * 用于检测内容是否真的发生了变化 */ function computeBlockHash(block: DocumentBlock): string { // 只计算关键内容字段的哈希,忽略 id、block_order 等元数据 const contentForHash = { type: block.type, content: block.content, style: block.style, word_style: block.word_style, level: block.level, }; return hashSum(contentForHash); } // ══════════════════════════════════════════════════════════════════════════════ // Store State Interface // ══════════════════════════════════════════════════════════════════════════════ interface EditorStore { // ── 文档状态 ──────────────────────────────────────────────────────────── documentId: string | null; documentTitle: string; blocks: DocumentBlock[]; selectedBlockId: string | null; // ── 加载/保存状态 ────────────────────────────────────────────────────── isLoading: boolean; isSaving: boolean; error: string | null; // ── 保存进度 ──────────────────────────────────────────────────────────── /** 正在保存的块数量 */ savingProgress: { current: number; total: number } | null; // ── 修改状态追踪 ──────────────────────────────────────────────────────── /** 文档是否已被修改(用于控制保存按钮状态) */ hasModified: boolean; /** 原始blocks快照(用于检测变化) */ originalBlocksSnapshot: string | null; /** 保存失败的块ID列表 */ failedBlocks: string[]; /** 被修改但未保存的块ID集合 */ dirtyBlocks: Set; /** 上次成功保存的快照 */ lastSavedSnapshot: string | null; /** 块内容哈希映射表(用于精确检测内容变化)*/ blockHashes: Map; // ── 保存控制 ──────────────────────────────────────────────────────────── /** 当前正在进行的保存 Promise */ currentSavePromise: Promise | null; /** 用于取消请求的 AbortController */ saveAbortController: AbortController | null; // ── 自动保存 ──────────────────────────────────────────────────────────── /** 自动保存定时器 */ autoSaveTimer: ReturnType | null; /** 是否启用自动保存 */ autoSaveEnabled: boolean; /** 上次保存时间戳 */ lastSaveTime: number | null; // ── 重试机制 ──────────────────────────────────────────────────────────── /** 块重试次数记录 */ retryAttempts: Map; // ── 操作方法 ──────────────────────────────────────────────────────────── /** * 加载文档 */ loadDocument: (documentId: string) => Promise; /** * 保存文档 */ saveDocument: () => Promise; /** * 重试保存失败的块 */ retryFailedBlocks: () => Promise; /** * 添加块 * @param block 块数据(部分字段) * @param afterBlockId 插入位置(在此块之后),不传则追加到末尾 */ addBlock: (block: PartialBlock, afterBlockId?: string) => void; /** * 更新块 * @param id 块ID * @param updates 更新数据 */ updateBlock: (id: string, updates: BlockUpdate) => void; /** * 标记文档已修改 */ markAsModified: () => void; /** * 标记文档已保存 */ markAsSaved: () => void; /** * 删除块 * @param id 块ID */ deleteBlock: (id: string) => Promise; /** * 移动块 * @param id 块ID * @param targetOrder 目标位置 */ moveBlock: (id: string, targetOrder: number) => void; /** * 选中块 * @param id 块ID */ selectBlock: (id: string | null) => void; /** * 根据ID获取块 */ getBlockById: (id: string) => DocumentBlock | undefined; /** * 根据类型获取块 */ getBlocksByType: (type: BlockType) => DocumentBlock[]; /** * 检查块是否为脏块(已修改未保存) */ isBlockDirty: (id: string) => boolean; /** * 获取所有脏块 */ getDirtyBlocks: () => DocumentBlock[]; /** * 保存单个block的更改 */ saveBlock: (id: string) => Promise; /** * 重新计算所有块的block_order(稀疏排序) */ recomputeBlockOrders: () => void; /** * 启用/禁用自动保存 */ setAutoSaveEnabled: (enabled: boolean) => void; /** * 触发自动保存(带防抖) */ triggerAutoSave: () => void; /** * 取消自动保存定时器 */ cancelAutoSave: () => void; /** * 重置状态 */ reset: () => void; } // ══════════════════════════════════════════════════════════════════════════════ // Utility Functions // ══════════════════════════════════════════════════════════════════════════════ /** * 生成块ID */ function generateBlockId(type: BlockType, index: number = 0): string { const typePrefix: Record = { heading: 'h', paragraph: 'p', table: 'tbl', image: 'img', toc: 'toc', }; return `block-${typePrefix[type]}-${Date.now()}-${index}`; } /** * 计算插入位置的block_order * 稀疏排序策略:在两个块之间找到中间值 */ function computeInsertOrder(prevOrder: number, nextOrder: number): number { const gap = nextOrder - prevOrder; if (gap > 1) { // 有间隙,直接取中间值 return Math.floor((prevOrder + nextOrder) / 2); } // 间隙不足,需要重排 return -1; } /** * 重新平衡block_order(稀疏排序,间隔100) */ function rebalanceOrders(blocks: DocumentBlock[]): DocumentBlock[] { const sorted = [...blocks].sort((a, b) => a.block_order - b.block_order); return sorted.map((block, index) => ({ ...block, block_order: index * 100, })); } // ══════════════════════════════════════════════════════════════════════════════ // Store Implementation // ══════════════════════════════════════════════════════════════════════════════ const initialState = { documentId: null, documentTitle: '', blocks: [], selectedBlockId: null, isLoading: false, isSaving: false, error: null, hasModified: false, originalBlocksSnapshot: null, failedBlocks: [] as string[], currentSavePromise: null, saveAbortController: null, dirtyBlocks: new Set(), lastSavedSnapshot: null, savingProgress: null, blockHashes: new Map(), autoSaveTimer: null, autoSaveEnabled: true, // 默认启用自动保存 lastSaveTime: null, retryAttempts: new Map(), }; export const useEditorStore = create((set, get) => ({ ...initialState, // ── loadDocument ──────────────────────────────────────────────────────── loadDocument: async (documentId: string) => { set({ isLoading: true, error: null }); try { const data = await blockService.getBlocks(documentId); // 规范化表格块的数据结构,确保与后端期望格式一致 const normalizedBlocks = data.blocks.map(block => { if (block.type === 'table') { return normalizeTableBlock(block as TableBlock); } return block; }); // 保存原始blocks快照,用于检测修改 const snapshot = JSON.stringify(normalizedBlocks); // 计算所有块的初始哈希值 const initialHashes = new Map(); normalizedBlocks.forEach(block => { initialHashes.set(block.id, computeBlockHash(block)); }); set({ documentId, documentTitle: '未命名文档', // 后端不返回title,使用默认值 blocks: normalizedBlocks, isLoading: false, hasModified: false, // 初始状态为未修改 originalBlocksSnapshot: snapshot, lastSavedSnapshot: snapshot, // 加载即为已保存状态 dirtyBlocks: new Set(), // 清空脏块集合 failedBlocks: [], // 清空失败块列表 blockHashes: initialHashes, // 初始化哈希表 }); } catch (error: any) { set({ error: error.message || '加载文档失败', isLoading: false, }); throw error; } }, // ── saveDocument ──────────────────────────────────────────────────────── saveDocument: async () => { const { documentId, blocks, dirtyBlocks, currentSavePromise, saveAbortController, retryAttempts } = get(); if (!documentId) { throw new Error('没有打开的文档'); } // 如果没有脏块,无需保存 if (dirtyBlocks.size === 0) { return; } // 如果已有保存请求在进行中,返回现有的 Promise(去重) if (currentSavePromise) { return currentSavePromise; } // 取消之前的请求(如果有) if (saveAbortController) { saveAbortController.abort(); } // 取消自动保存定时器 const { autoSaveTimer } = get(); if (autoSaveTimer) { clearTimeout(autoSaveTimer); set({ autoSaveTimer: null }); } // 创建新的 AbortController const newAbortController = new AbortController(); set({ isSaving: true, error: null, saveAbortController: newAbortController, savingProgress: null, // 重置进度 }); // 创建保存 Promise const savePromise = (async () => { try { // 获取需要保存的块(只保存脏块) const blocksToSave = blocks.filter(block => { // 必须在脏块列表中 if (!dirtyBlocks.has(block.id)) { return false; } // 跳过TOC块 if (block.type === 'toc') { return false; } // 跳过metadata中标记为readonly的块 const metadata = block.metadata as any; if (metadata?.readonly === true || metadata?.is_auto_generated === true) { return false; } return true; }); const totalBlocks = blocksToSave.length; // 初始化进度 set({ savingProgress: { current: 0, total: totalBlocks } }); // 已完成的请求计数 let completedCount = 0; // 使用并发限制器并行保存 const tasks = blocksToSave.map(block => saveConcurrencyLimit(async () => { // 获取该块的重试次数 const attempts = retryAttempts.get(block.id) || 0; // 序列化块内容(将富文本数组转为纯字符串,并提取样式到style字段) let contentToSave = block.content; let styleToSave = block.style; if (block.type === 'table') { const serializedTable = serializeTableBlock(block as TableBlock); contentToSave = serializedTable.content; } else if (block.type === 'heading' || block.type === 'paragraph') { // 对于标题和段落块,如果content是富文本数组,需要序列化 if (Array.isArray(block.content)) { // 提取纯文本 contentToSave = block.content.map(seg => seg.text).join(''); // 如果所有片段的样式一致,提取到块级style const allSegments = block.content; if (allSegments.length > 0) { const firstStyle = allSegments[0].style; const allSameStyle = allSegments.every(seg => JSON.stringify(seg.style) === JSON.stringify(firstStyle) ); if (allSameStyle && Object.keys(firstStyle).length > 0) { // 所有片段样式一致,合并到块级style styleToSave = { ...block.style, ...firstStyle }; } } } } try { const result = await blockService.updateBlock( documentId, block.id, { content: contentToSave as any, style: styleToSave, word_style: block.word_style, metadata: block.metadata, }, { signal: newAbortController.signal } ); // 保存成功,重置重试次数 const newRetryAttempts = new Map(get().retryAttempts); newRetryAttempts.delete(block.id); set({ retryAttempts: newRetryAttempts }); // 更新进度 completedCount++; set({ savingProgress: { current: completedCount, total: totalBlocks } }); return result; } catch (error: any) { // 保存失败,记录重试次数 if (attempts < MAX_RETRY_ATTEMPTS && error.name !== 'CanceledError') { const newRetryAttempts = new Map(get().retryAttempts); newRetryAttempts.set(block.id, attempts + 1); set({ retryAttempts: newRetryAttempts }); } throw error; } }) ); // 等待所有任务完成(使用 allSettled 容错) const results = await Promise.allSettled(tasks); // 统计成功和失败的块 const successIds: string[] = []; const failedIndices: number[] = []; results.forEach((result, index) => { if (result.status === 'fulfilled') { successIds.push(blocksToSave[index].id); } else { failedIndices.push(index); } }); // 从脏块集合中移除成功保存的块 const newDirtyBlocks = new Set(dirtyBlocks); successIds.forEach(id => newDirtyBlocks.delete(id)); if (failedIndices.length > 0) { // 有块保存失败 const failedBlockIds = failedIndices.map(i => blocksToSave[i].id); // 更新成功保存的块的哈希值 const newBlockHashes = new Map(get().blockHashes); successIds.forEach(id => { const block = blocks.find(b => b.id === id); if (block) { newBlockHashes.set(id, computeBlockHash(block)); } }); // 检查失败的块是否还可以重试 const needRetryBlocks = failedBlockIds.filter(id => { const attempts = get().retryAttempts.get(id) || 0; return attempts < MAX_RETRY_ATTEMPTS; }); set({ isSaving: false, failedBlocks: failedBlockIds, dirtyBlocks: newDirtyBlocks, blockHashes: newBlockHashes, hasModified: newDirtyBlocks.size > 0, error: `部分保存失败: ${successIds.length}/${totalBlocks} 个块保存成功`, currentSavePromise: null, saveAbortController: null, savingProgress: null, lastSaveTime: Date.now(), }); // 如果有需要重试的块,3秒后自动重试 if (needRetryBlocks.length > 0) { setTimeout(() => { get().retryFailedBlocks(); }, 3000); } throw new Error(`${failedIndices.length} 个块保存失败`); } else { // 全部保存成功 const snapshot = JSON.stringify(blocks); // 更新成功保存的块的哈希值 const newBlockHashes = new Map(get().blockHashes); successIds.forEach(id => { const block = blocks.find(b => b.id === id); if (block) { newBlockHashes.set(id, computeBlockHash(block)); } }); set({ isSaving: false, hasModified: false, originalBlocksSnapshot: snapshot, lastSavedSnapshot: snapshot, failedBlocks: [], dirtyBlocks: new Set(), blockHashes: newBlockHashes, currentSavePromise: null, saveAbortController: null, savingProgress: null, lastSaveTime: Date.now(), }); } } catch (error: any) { // 如果是请求取消,不算错误 if (error.name === 'CanceledError' || error.message?.includes('canceled')) { set({ isSaving: false, currentSavePromise: null, saveAbortController: null, savingProgress: null, }); return; } set({ error: error.message || '保存失败', isSaving: false, currentSavePromise: null, saveAbortController: null, savingProgress: null, }); throw error; } })(); // 保存 Promise 到 state set({ currentSavePromise: savePromise }); return savePromise; }, // ── retryFailedBlocks ─────────────────────────────────────────────────── retryFailedBlocks: async () => { const { documentId, blocks, failedBlocks, currentSavePromise, saveAbortController } = get(); if (!documentId) { throw new Error('没有打开的文档'); } if (failedBlocks.length === 0) { return; // 没有失败的块 } // 如果已有保存请求在进行中,返回现有的 Promise(去重) if (currentSavePromise) { return currentSavePromise; } // 取消之前的请求(如果有) if (saveAbortController) { saveAbortController.abort(); } // 创建新的 AbortController const newAbortController = new AbortController(); set({ isSaving: true, error: null, saveAbortController: newAbortController, savingProgress: null, }); // 创建保存 Promise const savePromise = (async () => { try { // 获取失败的块 const blocksToRetry = blocks.filter(block => failedBlocks.includes(block.id) ); const totalBlocks = blocksToRetry.length; // 初始化进度 set({ savingProgress: { current: 0, total: totalBlocks } }); // 已完成的请求计数 let completedCount = 0; // 使用并发限制器重试保存 const tasks = blocksToRetry.map(block => saveConcurrencyLimit(() => { // 序列化块内容(将富文本数组转为纯字符串,并提取样式到style字段) let contentToSave = block.content; let styleToSave = block.style; if (block.type === 'table') { const serializedTable = serializeTableBlock(block as TableBlock); contentToSave = serializedTable.content; } else if (block.type === 'heading' || block.type === 'paragraph') { // 对于标题和段落块,如果content是富文本数组,需要序列化 if (Array.isArray(block.content)) { // 提取纯文本 contentToSave = block.content.map(seg => seg.text).join(''); // 如果所有片段的样式一致,提取到块级style const allSegments = block.content; if (allSegments.length > 0) { const firstStyle = allSegments[0].style; const allSameStyle = allSegments.every(seg => JSON.stringify(seg.style) === JSON.stringify(firstStyle) ); if (allSameStyle && Object.keys(firstStyle).length > 0) { // 所有片段样式一致,合并到块级style styleToSave = { ...block.style, ...firstStyle }; } } } } return blockService.updateBlock( documentId, block.id, { content: contentToSave as any, style: styleToSave, word_style: block.word_style, metadata: block.metadata, }, { signal: newAbortController.signal } ).then(result => { // 更新进度 completedCount++; set({ savingProgress: { current: completedCount, total: totalBlocks } }); return result; }); }) ); const results = await Promise.allSettled(tasks); // 统计仍然失败的块 const stillFailedIds: string[] = []; const successIds: string[] = []; results.forEach((result, index) => { if (result.status === 'rejected') { stillFailedIds.push(blocksToRetry[index].id); } else { successIds.push(blocksToRetry[index].id); } }); // 从脏块集合中移除成功的块 const { dirtyBlocks } = get(); const newDirtyBlocks = new Set(dirtyBlocks); successIds.forEach(id => newDirtyBlocks.delete(id)); if (stillFailedIds.length > 0) { // 仍有块保存失败 // 更新成功保存的块的哈希值 const newBlockHashes = new Map(get().blockHashes); successIds.forEach(id => { const block = blocks.find(b => b.id === id); if (block) { newBlockHashes.set(id, computeBlockHash(block)); } }); set({ isSaving: false, failedBlocks: stillFailedIds, dirtyBlocks: newDirtyBlocks, blockHashes: newBlockHashes, // 更新成功块的哈希 hasModified: newDirtyBlocks.size > 0, error: `重试后仍有 ${stillFailedIds.length}/${totalBlocks} 个块保存失败`, currentSavePromise: null, saveAbortController: null, savingProgress: null, }); throw new Error(`${stillFailedIds.length} 个块保存失败`); } else { // 全部重试成功 const snapshot = JSON.stringify(blocks); // 更新所有成功保存的块的哈希值 const newBlockHashes = new Map(get().blockHashes); successIds.forEach(id => { const block = blocks.find(b => b.id === id); if (block) { newBlockHashes.set(id, computeBlockHash(block)); } }); set({ isSaving: false, hasModified: newDirtyBlocks.size > 0, originalBlocksSnapshot: snapshot, lastSavedSnapshot: snapshot, failedBlocks: [], dirtyBlocks: newDirtyBlocks, blockHashes: newBlockHashes, // 更新哈希表 currentSavePromise: null, saveAbortController: null, savingProgress: null, }); } } catch (error: any) { // 如果是请求取消,不算错误 if (error.name === 'CanceledError' || error.message?.includes('canceled')) { set({ isSaving: false, currentSavePromise: null, saveAbortController: null, savingProgress: null, }); return; } set({ error: error.message || '重试失败', isSaving: false, currentSavePromise: null, saveAbortController: null, savingProgress: null, }); throw error; } })(); // 保存 Promise 到 state set({ currentSavePromise: savePromise }); return savePromise; }, // ── addBlock ──────────────────────────────────────────────────────────── addBlock: async (partialBlock: PartialBlock, afterBlockId?: string) => { const { blocks, blockHashes, documentId } = get(); if (!documentId) { message.error('没有打开的文档'); return; } // 找到插入位置 const afterIndex = afterBlockId ? blocks.findIndex((b) => b.id === afterBlockId) : blocks.length - 1; // 计算block_order let newOrder: number; if (blocks.length === 0) { // 空文档,从0开始 newOrder = 0; } else if (afterIndex === -1 || afterIndex === blocks.length - 1) { // 追加到末尾 const lastBlock = blocks[blocks.length - 1]; newOrder = lastBlock ? lastBlock.block_order + 100 : 0; } else { // 插入到中间 const prevOrder = blocks[afterIndex].block_order; const nextOrder = blocks[afterIndex + 1].block_order; newOrder = computeInsertOrder(prevOrder, nextOrder); if (newOrder === -1) { // 需要重排 message.error('需要重排block_order,功能待实现'); return; } } // 先在本地创建临时块(使用临时ID) const tempId = generateBlockId(partialBlock.type); const tempBlock: DocumentBlock = { id: tempId, block_order: newOrder, level: partialBlock.level ?? 0, index: partialBlock.index ?? 0, word_style: partialBlock.word_style ?? 'Normal', style: partialBlock.style ?? {}, metadata: partialBlock.metadata ?? {}, ...partialBlock, } as DocumentBlock; // 立即添加到本地状态(乐观更新) const newBlocks = [...blocks, tempBlock].sort( (a, b) => a.block_order - b.block_order ); set({ blocks: newBlocks, }); // 调用后端API创建块 try { const response: any = await blockService.createBlock(documentId, { type: partialBlock.type, level: partialBlock.level ?? 0, index: partialBlock.index ?? 0, // 添加index字段 content: partialBlock.content, word_style: partialBlock.word_style ?? 'Normal', style: partialBlock.style ?? {}, metadata: partialBlock.metadata ?? {}, after_block_id: afterBlockId || null, }); // 后端返回 { blockId: string },我们需要使用这个ID更新临时块 const realBlockId = response.blockId || response.data?.blockId; if (!realBlockId) { throw new Error('后端未返回blockId'); } // 用后端返回的真实ID替换临时ID const { blocks: currentBlocks } = get(); const updatedBlocks = currentBlocks.map(b => { if (b.id === tempId) { const realBlock = { ...b, id: realBlockId }; return realBlock; } return b; }); // 找到更新后的真实块 const realBlock = updatedBlocks.find(b => b.id === realBlockId); if (realBlock) { // 添加到哈希表(新创建的块认为是已保存状态) const newBlockHashes = new Map(blockHashes); newBlockHashes.set(realBlockId, computeBlockHash(realBlock)); set({ blocks: updatedBlocks, blockHashes: newBlockHashes, }); } else { throw new Error('无法找到创建的块'); } } catch (error: any) { // 创建失败,回滚本地状态 const { blocks: currentBlocks } = get(); const rolledBackBlocks = currentBlocks.filter(b => b.id !== tempId); set({ blocks: rolledBackBlocks, }); message.error(error.message || '创建块失败'); throw error; } }, // ── updateBlock ───────────────────────────────────────────────────────── updateBlock: (id: string, updates: BlockUpdate) => { const { blocks, dirtyBlocks, blockHashes, autoSaveEnabled } = get(); const newBlocks = blocks.map((block) => block.id === id ? { ...block, ...updates } as DocumentBlock : block ); // 获取更新后的块 const updatedBlock = newBlocks.find(b => b.id === id); if (!updatedBlock) { return; // 块不存在,跳过 } // 计算新的哈希值 const newHash = computeBlockHash(updatedBlock); const originalHash = blockHashes.get(id); // 比较哈希值,只有真正改变时才标记为脏块 const newDirtyBlocks = new Set(dirtyBlocks); if (originalHash && newHash === originalHash) { // 内容与原始版本相同,从脏块中移除(如果存在) newDirtyBlocks.delete(id); } else if (newHash !== originalHash) { // 内容真正发生了变化,标记为脏块 newDirtyBlocks.add(id); } set({ blocks: newBlocks, dirtyBlocks: newDirtyBlocks, hasModified: newDirtyBlocks.size > 0, }); // 触发自动保存 if (autoSaveEnabled && newDirtyBlocks.size > 0) { get().triggerAutoSave(); } }, // ── deleteBlock ───────────────────────────────────────────────────────── deleteBlock: async (id: string) => { const { blocks, dirtyBlocks, blockHashes, documentId } = get(); if (!documentId) { message.error('没有打开的文档'); return; } // 找到要删除的块 const blockToDelete = blocks.find(b => b.id === id); if (!blockToDelete) { return; } // 先从本地状态删除(乐观更新) const newBlocks = blocks.filter((block) => block.id !== id); // 删除块时,也需要从脏块集合中移除(如果存在) const newDirtyBlocks = new Set(dirtyBlocks); newDirtyBlocks.delete(id); // 从哈希表中移除 const newBlockHashes = new Map(blockHashes); const wasOriginalBlock = blockHashes.has(id); newBlockHashes.delete(id); // 立即更新本地状态(不标记为已修改,因为我们会直接调用后端API) set({ blocks: newBlocks, dirtyBlocks: newDirtyBlocks, blockHashes: newBlockHashes, }); try { // 1. 调用后端API删除块 await blockService.deleteBlock(documentId, id); // 2. 删除成功后,重新计算并更新所有块的 block_order // 按照当前顺序重新分配 block_order(使用稀疏排序,间隔100) const sortedBlocks = [...newBlocks].sort((a, b) => a.block_order - b.block_order); const blocksNeedUpdate: Array<{block: DocumentBlock; newOrder: number}> = []; sortedBlocks.forEach((block, index) => { const expectedOrder = index * 100; if (block.block_order !== expectedOrder) { blocksNeedUpdate.push({ block, newOrder: expectedOrder, }); } }); // 3. 如果有块需要更新顺序,批量调用 PUT API 更新 if (blocksNeedUpdate.length > 0) { // 并发调用 PUT API 更新所有受影响的块 const updatePromises = blocksNeedUpdate.map(({ block, newOrder }) => { // 对于TOC块,只更新block_order,不传递其他字段 // 因为后端对TOC块有严格的验证限制 if (block.type === 'toc') { return blockService.updateBlock(documentId, block.id, { block_order: newOrder, }); } // 非TOC块:传递完整的数据 return blockService.updateBlock(documentId, block.id, { content: block.content as any, style: block.style, word_style: block.word_style, metadata: block.metadata, block_order: newOrder, }); }); await Promise.all(updatePromises); // 4. 更新本地状态中的 block_order,并更新这些块的哈希值 const { blocks: currentBlocks, blockHashes: currentBlockHashes } = get(); const updatedBlocks = currentBlocks.map(b => { const update = blocksNeedUpdate.find(u => u.block.id === b.id); if (update) { return { ...b, block_order: update.newOrder }; } return b; }); // 更新这些块的哈希值,因为后端已经保存了 const updatedBlockHashes = new Map(currentBlockHashes); updatedBlocks.forEach(block => { if (blocksNeedUpdate.some(u => u.block.id === block.id)) { updatedBlockHashes.set(block.id, computeBlockHash(block)); } }); // 5. 更新本地状态,不标记为已修改(因为后端已经同步) set({ blocks: updatedBlocks, blockHashes: updatedBlockHashes, hasModified: false, // 所有操作都已同步到后端 }); } else { // 没有块需要更新顺序(可能删除的是最后一个块) set({ hasModified: false }); // 标记为已同步 } } catch (error: any) { // 删除或更新失败,回滚本地状态 // 恢复被删除的块 if (blockToDelete) { const { blocks: currentBlocks } = get(); const restoredBlocks = [...currentBlocks, blockToDelete].sort( (a, b) => a.block_order - b.block_order ); // 恢复哈希表 const restoredBlockHashes = new Map(get().blockHashes); if (wasOriginalBlock) { restoredBlockHashes.set(id, computeBlockHash(blockToDelete)); } set({ blocks: restoredBlocks, blockHashes: restoredBlockHashes, }); } message.error(error.message || '删除块失败'); throw error; } }, // ── moveBlock ─────────────────────────────────────────────────────────── moveBlock: (id: string, targetOrder: number) => { const { blocks } = get(); const newBlocks = blocks.map((block) => block.id === id ? { ...block, block_order: targetOrder } : block ); set({ blocks: newBlocks }); }, // ── selectBlock ───────────────────────────────────────────────────────── selectBlock: (id: string | null) => { set({ selectedBlockId: id }); }, // ── getBlockById ──────────────────────────────────────────────────────── getBlockById: (id: string) => { const { blocks } = get(); return blocks.find((block) => block.id === id); }, // ── getBlocksByType ───────────────────────────────────────────────────── getBlocksByType: (type: BlockType) => { const { blocks } = get(); return blocks.filter((block) => block.type === type); }, // ── isBlockDirty ──────────────────────────────────────────────────────── isBlockDirty: (id: string) => { const { dirtyBlocks } = get(); return dirtyBlocks.has(id); }, // ── getDirtyBlocks ────────────────────────────────────────────────────── getDirtyBlocks: () => { const { blocks, dirtyBlocks } = get(); return blocks.filter(block => dirtyBlocks.has(block.id)); }, // ── saveBlock ─────────────────────────────────────────────────────────── saveBlock: async (id: string) => { const { documentId, blocks, dirtyBlocks, blockHashes } = get(); if (!documentId) { throw new Error('没有打开的文档'); } const block = blocks.find(b => b.id === id); if (!block) { throw new Error('块不存在'); } // 检查是否为只读块(如TOC块) if (block.type === 'toc') { // TOC块是只读的,跳过保存 return; } // 检查metadata中的readonly标志(使用类型安全的方式) const metadata = block.metadata as any; if (metadata?.readonly === true || metadata?.is_auto_generated === true) { // 只读块,跳过保存 return; } // **关键修复**: 在保存前先计算当前块的哈希值,并与原始哈希比较 // 如果哈希值相同,说明内容没有真正变化,跳过保存 const currentHash = computeBlockHash(block); const originalHash = blockHashes.get(id); if (originalHash && currentHash === originalHash) { // 从脏块集合中移除(可能是误标记) const newDirtyBlocks = new Set(dirtyBlocks); newDirtyBlocks.delete(id); set({ dirtyBlocks: newDirtyBlocks, hasModified: newDirtyBlocks.size > 0, }); return; } set({ isSaving: true, error: null }); try { // 序列化表格块的content(将富文本数组转为纯字符串) let contentToSave = block.content; if (block.type === 'table') { const serializedTable = serializeTableBlock(block as TableBlock); contentToSave = serializedTable.content; } await blockService.updateBlock(documentId, id, { type: block.type, level: block.level, content: contentToSave as any, // 类型断言:不同block类型的content类型不同 style: block.style, word_style: block.word_style, metadata: block.metadata, }); // **关键修复**: 保存成功后,需要重新获取最新的blocks状态 // 因为在保存期间可能又有新的更新 const { blocks: latestBlocks, dirtyBlocks: latestDirtyBlocks, blockHashes: latestBlockHashes } = get(); const latestBlock = latestBlocks.find(b => b.id === id); if (latestBlock) { // 保存成功后,从脏块集合中移除该块 const newDirtyBlocks = new Set(latestDirtyBlocks); newDirtyBlocks.delete(id); // **关键修复**: 使用保存成功时的块内容计算哈希值 const newBlockHashes = new Map(latestBlockHashes); newBlockHashes.set(id, computeBlockHash(latestBlock)); set({ isSaving: false, dirtyBlocks: newDirtyBlocks, blockHashes: newBlockHashes, hasModified: newDirtyBlocks.size > 0, }); } else { // 块已被删除 set({ isSaving: false }); } } catch (error: any) { // 保存失败,保持脏块标记 set({ error: error.message || '保存块失败', isSaving: false, }); throw error; } }, // ── markAsModified ────────────────────────────────────────────────────── markAsModified: () => { set({ hasModified: true }); }, // ── markAsSaved ───────────────────────────────────────────────────────── markAsSaved: () => { const { blocks } = get(); const snapshot = JSON.stringify(blocks); set({ hasModified: false, originalBlocksSnapshot: snapshot, lastSavedSnapshot: snapshot, dirtyBlocks: new Set(), // 清空脏块集合 }); }, // ── recomputeBlockOrders ──────────────────────────────────────────────── recomputeBlockOrders: () => { const { blocks } = get(); const newBlocks = rebalanceOrders(blocks); set({ blocks: newBlocks }); }, // ── setAutoSaveEnabled ────────────────────────────────────────────────── setAutoSaveEnabled: (enabled: boolean) => { set({ autoSaveEnabled: enabled }); if (!enabled) { // 禁用时取消现有的自动保存定时器 get().cancelAutoSave(); } }, // ── triggerAutoSave ───────────────────────────────────────────────────── triggerAutoSave: () => { const { autoSaveTimer, autoSaveEnabled, dirtyBlocks } = get(); // 如果自动保存未启用或没有脏块,直接返回 if (!autoSaveEnabled || dirtyBlocks.size === 0) { return; } // 清除现有的定时器 if (autoSaveTimer) { clearTimeout(autoSaveTimer); } // 设置新的定时器 const newTimer = setTimeout(() => { get().saveDocument(); }, AUTO_SAVE_DELAY); set({ autoSaveTimer: newTimer }); }, // ── cancelAutoSave ────────────────────────────────────────────────────── cancelAutoSave: () => { const { autoSaveTimer } = get(); if (autoSaveTimer) { clearTimeout(autoSaveTimer); set({ autoSaveTimer: null }); } }, // ── reset ─────────────────────────────────────────────────────────────── reset: () => { // 清除自动保存定时器 const { autoSaveTimer } = get(); if (autoSaveTimer) { clearTimeout(autoSaveTimer); } set(initialState); }, })); export default useEditorStore;