| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293 |
- /**
- * 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<string>;
- /** 上次成功保存的快照 */
- lastSavedSnapshot: string | null;
- /** 块内容哈希映射表(用于精确检测内容变化)*/
- blockHashes: Map<string, string>;
-
- // ── 保存控制 ────────────────────────────────────────────────────────────
- /** 当前正在进行的保存 Promise */
- currentSavePromise: Promise<void> | null;
- /** 用于取消请求的 AbortController */
- saveAbortController: AbortController | null;
-
- // ── 自动保存 ────────────────────────────────────────────────────────────
- /** 自动保存定时器 */
- autoSaveTimer: ReturnType<typeof setTimeout> | null;
- /** 是否启用自动保存 */
- autoSaveEnabled: boolean;
- /** 上次保存时间戳 */
- lastSaveTime: number | null;
-
- // ── 重试机制 ────────────────────────────────────────────────────────────
- /** 块重试次数记录 */
- retryAttempts: Map<string, number>;
-
- // ── 操作方法 ────────────────────────────────────────────────────────────
-
- /**
- * 加载文档
- */
- loadDocument: (documentId: string) => Promise<void>;
-
- /**
- * 保存文档
- */
- saveDocument: () => Promise<void>;
-
- /**
- * 重试保存失败的块
- */
- retryFailedBlocks: () => Promise<void>;
-
- /**
- * 添加块
- * @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<void>;
-
- /**
- * 移动块
- * @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<void>;
-
- /**
- * 重新计算所有块的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<BlockType, string> = {
- 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<string>(),
- lastSavedSnapshot: null,
- savingProgress: null,
- blockHashes: new Map<string, string>(),
- autoSaveTimer: null,
- autoSaveEnabled: true, // 默认启用自动保存
- lastSaveTime: null,
- retryAttempts: new Map<string, number>(),
- };
- export const useEditorStore = create<EditorStore>((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<string, string>();
- 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<string>(), // 清空脏块集合
- 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<string>(),
- 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<string>(), // 清空脏块集合
- });
- },
-
- // ── 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;
|