editorStore.ts 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293
  1. /**
  2. * editorStore.ts - 编辑器状态管理
  3. *
  4. * 使用Zustand管理块编辑器的状态,包括:
  5. * - 文档元数据
  6. * - blocks数组
  7. * - 选中状态
  8. * - 加载/保存状态
  9. *
  10. * @module stores/editorStore
  11. */
  12. import { create } from 'zustand';
  13. import pLimit from 'p-limit';
  14. import hashSum from 'hash-sum';
  15. import { message } from 'antd';
  16. import type {
  17. DocumentBlock,
  18. PartialBlock,
  19. BlockUpdate,
  20. BlockType,
  21. TableBlock,
  22. } from '../types/editor';
  23. import { blockService } from '../services/blockService';
  24. import { normalizeTableBlock, serializeTableBlock } from '../utils/blockOperations';
  25. // 并发保存限制器(最多同时进行 3 个请求,降低服务器压力)
  26. const saveConcurrencyLimit = pLimit(3);
  27. // 自动保存延迟时间(毫秒)
  28. const AUTO_SAVE_DELAY = 3000; // 3秒无操作后自动保存
  29. // 最大重试次数
  30. const MAX_RETRY_ATTEMPTS = 3;
  31. /**
  32. * 计算块内容的哈希值
  33. * 用于检测内容是否真的发生了变化
  34. */
  35. function computeBlockHash(block: DocumentBlock): string {
  36. // 只计算关键内容字段的哈希,忽略 id、block_order 等元数据
  37. const contentForHash = {
  38. type: block.type,
  39. content: block.content,
  40. style: block.style,
  41. word_style: block.word_style,
  42. level: block.level,
  43. };
  44. return hashSum(contentForHash);
  45. }
  46. // ══════════════════════════════════════════════════════════════════════════════
  47. // Store State Interface
  48. // ══════════════════════════════════════════════════════════════════════════════
  49. interface EditorStore {
  50. // ── 文档状态 ────────────────────────────────────────────────────────────
  51. documentId: string | null;
  52. documentTitle: string;
  53. blocks: DocumentBlock[];
  54. selectedBlockId: string | null;
  55. // ── 加载/保存状态 ──────────────────────────────────────────────────────
  56. isLoading: boolean;
  57. isSaving: boolean;
  58. error: string | null;
  59. // ── 保存进度 ────────────────────────────────────────────────────────────
  60. /** 正在保存的块数量 */
  61. savingProgress: { current: number; total: number } | null;
  62. // ── 修改状态追踪 ────────────────────────────────────────────────────────
  63. /** 文档是否已被修改(用于控制保存按钮状态) */
  64. hasModified: boolean;
  65. /** 原始blocks快照(用于检测变化) */
  66. originalBlocksSnapshot: string | null;
  67. /** 保存失败的块ID列表 */
  68. failedBlocks: string[];
  69. /** 被修改但未保存的块ID集合 */
  70. dirtyBlocks: Set<string>;
  71. /** 上次成功保存的快照 */
  72. lastSavedSnapshot: string | null;
  73. /** 块内容哈希映射表(用于精确检测内容变化)*/
  74. blockHashes: Map<string, string>;
  75. // ── 保存控制 ────────────────────────────────────────────────────────────
  76. /** 当前正在进行的保存 Promise */
  77. currentSavePromise: Promise<void> | null;
  78. /** 用于取消请求的 AbortController */
  79. saveAbortController: AbortController | null;
  80. // ── 自动保存 ────────────────────────────────────────────────────────────
  81. /** 自动保存定时器 */
  82. autoSaveTimer: ReturnType<typeof setTimeout> | null;
  83. /** 是否启用自动保存 */
  84. autoSaveEnabled: boolean;
  85. /** 上次保存时间戳 */
  86. lastSaveTime: number | null;
  87. // ── 重试机制 ────────────────────────────────────────────────────────────
  88. /** 块重试次数记录 */
  89. retryAttempts: Map<string, number>;
  90. // ── 操作方法 ────────────────────────────────────────────────────────────
  91. /**
  92. * 加载文档
  93. */
  94. loadDocument: (documentId: string) => Promise<void>;
  95. /**
  96. * 保存文档
  97. */
  98. saveDocument: () => Promise<void>;
  99. /**
  100. * 重试保存失败的块
  101. */
  102. retryFailedBlocks: () => Promise<void>;
  103. /**
  104. * 添加块
  105. * @param block 块数据(部分字段)
  106. * @param afterBlockId 插入位置(在此块之后),不传则追加到末尾
  107. */
  108. addBlock: (block: PartialBlock, afterBlockId?: string) => void;
  109. /**
  110. * 更新块
  111. * @param id 块ID
  112. * @param updates 更新数据
  113. */
  114. updateBlock: (id: string, updates: BlockUpdate) => void;
  115. /**
  116. * 标记文档已修改
  117. */
  118. markAsModified: () => void;
  119. /**
  120. * 标记文档已保存
  121. */
  122. markAsSaved: () => void;
  123. /**
  124. * 删除块
  125. * @param id 块ID
  126. */
  127. deleteBlock: (id: string) => Promise<void>;
  128. /**
  129. * 移动块
  130. * @param id 块ID
  131. * @param targetOrder 目标位置
  132. */
  133. moveBlock: (id: string, targetOrder: number) => void;
  134. /**
  135. * 选中块
  136. * @param id 块ID
  137. */
  138. selectBlock: (id: string | null) => void;
  139. /**
  140. * 根据ID获取块
  141. */
  142. getBlockById: (id: string) => DocumentBlock | undefined;
  143. /**
  144. * 根据类型获取块
  145. */
  146. getBlocksByType: (type: BlockType) => DocumentBlock[];
  147. /**
  148. * 检查块是否为脏块(已修改未保存)
  149. */
  150. isBlockDirty: (id: string) => boolean;
  151. /**
  152. * 获取所有脏块
  153. */
  154. getDirtyBlocks: () => DocumentBlock[];
  155. /**
  156. * 保存单个block的更改
  157. */
  158. saveBlock: (id: string) => Promise<void>;
  159. /**
  160. * 重新计算所有块的block_order(稀疏排序)
  161. */
  162. recomputeBlockOrders: () => void;
  163. /**
  164. * 启用/禁用自动保存
  165. */
  166. setAutoSaveEnabled: (enabled: boolean) => void;
  167. /**
  168. * 触发自动保存(带防抖)
  169. */
  170. triggerAutoSave: () => void;
  171. /**
  172. * 取消自动保存定时器
  173. */
  174. cancelAutoSave: () => void;
  175. /**
  176. * 重置状态
  177. */
  178. reset: () => void;
  179. }
  180. // ══════════════════════════════════════════════════════════════════════════════
  181. // Utility Functions
  182. // ══════════════════════════════════════════════════════════════════════════════
  183. /**
  184. * 生成块ID
  185. */
  186. function generateBlockId(type: BlockType, index: number = 0): string {
  187. const typePrefix: Record<BlockType, string> = {
  188. heading: 'h',
  189. paragraph: 'p',
  190. table: 'tbl',
  191. image: 'img',
  192. toc: 'toc',
  193. };
  194. return `block-${typePrefix[type]}-${Date.now()}-${index}`;
  195. }
  196. /**
  197. * 计算插入位置的block_order
  198. * 稀疏排序策略:在两个块之间找到中间值
  199. */
  200. function computeInsertOrder(prevOrder: number, nextOrder: number): number {
  201. const gap = nextOrder - prevOrder;
  202. if (gap > 1) {
  203. // 有间隙,直接取中间值
  204. return Math.floor((prevOrder + nextOrder) / 2);
  205. }
  206. // 间隙不足,需要重排
  207. return -1;
  208. }
  209. /**
  210. * 重新平衡block_order(稀疏排序,间隔100)
  211. */
  212. function rebalanceOrders(blocks: DocumentBlock[]): DocumentBlock[] {
  213. const sorted = [...blocks].sort((a, b) => a.block_order - b.block_order);
  214. return sorted.map((block, index) => ({
  215. ...block,
  216. block_order: index * 100,
  217. }));
  218. }
  219. // ══════════════════════════════════════════════════════════════════════════════
  220. // Store Implementation
  221. // ══════════════════════════════════════════════════════════════════════════════
  222. const initialState = {
  223. documentId: null,
  224. documentTitle: '',
  225. blocks: [],
  226. selectedBlockId: null,
  227. isLoading: false,
  228. isSaving: false,
  229. error: null,
  230. hasModified: false,
  231. originalBlocksSnapshot: null,
  232. failedBlocks: [] as string[],
  233. currentSavePromise: null,
  234. saveAbortController: null,
  235. dirtyBlocks: new Set<string>(),
  236. lastSavedSnapshot: null,
  237. savingProgress: null,
  238. blockHashes: new Map<string, string>(),
  239. autoSaveTimer: null,
  240. autoSaveEnabled: true, // 默认启用自动保存
  241. lastSaveTime: null,
  242. retryAttempts: new Map<string, number>(),
  243. };
  244. export const useEditorStore = create<EditorStore>((set, get) => ({
  245. ...initialState,
  246. // ── loadDocument ────────────────────────────────────────────────────────
  247. loadDocument: async (documentId: string) => {
  248. set({ isLoading: true, error: null });
  249. try {
  250. const data = await blockService.getBlocks(documentId);
  251. // 规范化表格块的数据结构,确保与后端期望格式一致
  252. const normalizedBlocks = data.blocks.map(block => {
  253. if (block.type === 'table') {
  254. return normalizeTableBlock(block as TableBlock);
  255. }
  256. return block;
  257. });
  258. // 保存原始blocks快照,用于检测修改
  259. const snapshot = JSON.stringify(normalizedBlocks);
  260. // 计算所有块的初始哈希值
  261. const initialHashes = new Map<string, string>();
  262. normalizedBlocks.forEach(block => {
  263. initialHashes.set(block.id, computeBlockHash(block));
  264. });
  265. set({
  266. documentId,
  267. documentTitle: '未命名文档', // 后端不返回title,使用默认值
  268. blocks: normalizedBlocks,
  269. isLoading: false,
  270. hasModified: false, // 初始状态为未修改
  271. originalBlocksSnapshot: snapshot,
  272. lastSavedSnapshot: snapshot, // 加载即为已保存状态
  273. dirtyBlocks: new Set<string>(), // 清空脏块集合
  274. failedBlocks: [], // 清空失败块列表
  275. blockHashes: initialHashes, // 初始化哈希表
  276. });
  277. } catch (error: any) {
  278. set({
  279. error: error.message || '加载文档失败',
  280. isLoading: false,
  281. });
  282. throw error;
  283. }
  284. },
  285. // ── saveDocument ────────────────────────────────────────────────────────
  286. saveDocument: async () => {
  287. const { documentId, blocks, dirtyBlocks, currentSavePromise, saveAbortController, retryAttempts } = get();
  288. if (!documentId) {
  289. throw new Error('没有打开的文档');
  290. }
  291. // 如果没有脏块,无需保存
  292. if (dirtyBlocks.size === 0) {
  293. return;
  294. }
  295. // 如果已有保存请求在进行中,返回现有的 Promise(去重)
  296. if (currentSavePromise) {
  297. return currentSavePromise;
  298. }
  299. // 取消之前的请求(如果有)
  300. if (saveAbortController) {
  301. saveAbortController.abort();
  302. }
  303. // 取消自动保存定时器
  304. const { autoSaveTimer } = get();
  305. if (autoSaveTimer) {
  306. clearTimeout(autoSaveTimer);
  307. set({ autoSaveTimer: null });
  308. }
  309. // 创建新的 AbortController
  310. const newAbortController = new AbortController();
  311. set({
  312. isSaving: true,
  313. error: null,
  314. saveAbortController: newAbortController,
  315. savingProgress: null, // 重置进度
  316. });
  317. // 创建保存 Promise
  318. const savePromise = (async () => {
  319. try {
  320. // 获取需要保存的块(只保存脏块)
  321. const blocksToSave = blocks.filter(block => {
  322. // 必须在脏块列表中
  323. if (!dirtyBlocks.has(block.id)) {
  324. return false;
  325. }
  326. // 跳过TOC块
  327. if (block.type === 'toc') {
  328. return false;
  329. }
  330. // 跳过metadata中标记为readonly的块
  331. const metadata = block.metadata as any;
  332. if (metadata?.readonly === true || metadata?.is_auto_generated === true) {
  333. return false;
  334. }
  335. return true;
  336. });
  337. const totalBlocks = blocksToSave.length;
  338. // 初始化进度
  339. set({ savingProgress: { current: 0, total: totalBlocks } });
  340. // 已完成的请求计数
  341. let completedCount = 0;
  342. // 使用并发限制器并行保存
  343. const tasks = blocksToSave.map(block =>
  344. saveConcurrencyLimit(async () => {
  345. // 获取该块的重试次数
  346. const attempts = retryAttempts.get(block.id) || 0;
  347. // 序列化块内容(将富文本数组转为纯字符串,并提取样式到style字段)
  348. let contentToSave = block.content;
  349. let styleToSave = block.style;
  350. if (block.type === 'table') {
  351. const serializedTable = serializeTableBlock(block as TableBlock);
  352. contentToSave = serializedTable.content;
  353. } else if (block.type === 'heading' || block.type === 'paragraph') {
  354. // 对于标题和段落块,如果content是富文本数组,需要序列化
  355. if (Array.isArray(block.content)) {
  356. // 提取纯文本
  357. contentToSave = block.content.map(seg => seg.text).join('');
  358. // 如果所有片段的样式一致,提取到块级style
  359. const allSegments = block.content;
  360. if (allSegments.length > 0) {
  361. const firstStyle = allSegments[0].style;
  362. const allSameStyle = allSegments.every(seg =>
  363. JSON.stringify(seg.style) === JSON.stringify(firstStyle)
  364. );
  365. if (allSameStyle && Object.keys(firstStyle).length > 0) {
  366. // 所有片段样式一致,合并到块级style
  367. styleToSave = { ...block.style, ...firstStyle };
  368. }
  369. }
  370. }
  371. }
  372. try {
  373. const result = await blockService.updateBlock(
  374. documentId,
  375. block.id,
  376. {
  377. content: contentToSave as any,
  378. style: styleToSave,
  379. word_style: block.word_style,
  380. metadata: block.metadata,
  381. },
  382. { signal: newAbortController.signal }
  383. );
  384. // 保存成功,重置重试次数
  385. const newRetryAttempts = new Map(get().retryAttempts);
  386. newRetryAttempts.delete(block.id);
  387. set({ retryAttempts: newRetryAttempts });
  388. // 更新进度
  389. completedCount++;
  390. set({ savingProgress: { current: completedCount, total: totalBlocks } });
  391. return result;
  392. } catch (error: any) {
  393. // 保存失败,记录重试次数
  394. if (attempts < MAX_RETRY_ATTEMPTS && error.name !== 'CanceledError') {
  395. const newRetryAttempts = new Map(get().retryAttempts);
  396. newRetryAttempts.set(block.id, attempts + 1);
  397. set({ retryAttempts: newRetryAttempts });
  398. }
  399. throw error;
  400. }
  401. })
  402. );
  403. // 等待所有任务完成(使用 allSettled 容错)
  404. const results = await Promise.allSettled(tasks);
  405. // 统计成功和失败的块
  406. const successIds: string[] = [];
  407. const failedIndices: number[] = [];
  408. results.forEach((result, index) => {
  409. if (result.status === 'fulfilled') {
  410. successIds.push(blocksToSave[index].id);
  411. } else {
  412. failedIndices.push(index);
  413. }
  414. });
  415. // 从脏块集合中移除成功保存的块
  416. const newDirtyBlocks = new Set(dirtyBlocks);
  417. successIds.forEach(id => newDirtyBlocks.delete(id));
  418. if (failedIndices.length > 0) {
  419. // 有块保存失败
  420. const failedBlockIds = failedIndices.map(i => blocksToSave[i].id);
  421. // 更新成功保存的块的哈希值
  422. const newBlockHashes = new Map(get().blockHashes);
  423. successIds.forEach(id => {
  424. const block = blocks.find(b => b.id === id);
  425. if (block) {
  426. newBlockHashes.set(id, computeBlockHash(block));
  427. }
  428. });
  429. // 检查失败的块是否还可以重试
  430. const needRetryBlocks = failedBlockIds.filter(id => {
  431. const attempts = get().retryAttempts.get(id) || 0;
  432. return attempts < MAX_RETRY_ATTEMPTS;
  433. });
  434. set({
  435. isSaving: false,
  436. failedBlocks: failedBlockIds,
  437. dirtyBlocks: newDirtyBlocks,
  438. blockHashes: newBlockHashes,
  439. hasModified: newDirtyBlocks.size > 0,
  440. error: `部分保存失败: ${successIds.length}/${totalBlocks} 个块保存成功`,
  441. currentSavePromise: null,
  442. saveAbortController: null,
  443. savingProgress: null,
  444. lastSaveTime: Date.now(),
  445. });
  446. // 如果有需要重试的块,3秒后自动重试
  447. if (needRetryBlocks.length > 0) {
  448. setTimeout(() => {
  449. get().retryFailedBlocks();
  450. }, 3000);
  451. }
  452. throw new Error(`${failedIndices.length} 个块保存失败`);
  453. } else {
  454. // 全部保存成功
  455. const snapshot = JSON.stringify(blocks);
  456. // 更新成功保存的块的哈希值
  457. const newBlockHashes = new Map(get().blockHashes);
  458. successIds.forEach(id => {
  459. const block = blocks.find(b => b.id === id);
  460. if (block) {
  461. newBlockHashes.set(id, computeBlockHash(block));
  462. }
  463. });
  464. set({
  465. isSaving: false,
  466. hasModified: false,
  467. originalBlocksSnapshot: snapshot,
  468. lastSavedSnapshot: snapshot,
  469. failedBlocks: [],
  470. dirtyBlocks: new Set<string>(),
  471. blockHashes: newBlockHashes,
  472. currentSavePromise: null,
  473. saveAbortController: null,
  474. savingProgress: null,
  475. lastSaveTime: Date.now(),
  476. });
  477. }
  478. } catch (error: any) {
  479. // 如果是请求取消,不算错误
  480. if (error.name === 'CanceledError' || error.message?.includes('canceled')) {
  481. set({
  482. isSaving: false,
  483. currentSavePromise: null,
  484. saveAbortController: null,
  485. savingProgress: null,
  486. });
  487. return;
  488. }
  489. set({
  490. error: error.message || '保存失败',
  491. isSaving: false,
  492. currentSavePromise: null,
  493. saveAbortController: null,
  494. savingProgress: null,
  495. });
  496. throw error;
  497. }
  498. })();
  499. // 保存 Promise 到 state
  500. set({ currentSavePromise: savePromise });
  501. return savePromise;
  502. },
  503. // ── retryFailedBlocks ───────────────────────────────────────────────────
  504. retryFailedBlocks: async () => {
  505. const { documentId, blocks, failedBlocks, currentSavePromise, saveAbortController } = get();
  506. if (!documentId) {
  507. throw new Error('没有打开的文档');
  508. }
  509. if (failedBlocks.length === 0) {
  510. return; // 没有失败的块
  511. }
  512. // 如果已有保存请求在进行中,返回现有的 Promise(去重)
  513. if (currentSavePromise) {
  514. return currentSavePromise;
  515. }
  516. // 取消之前的请求(如果有)
  517. if (saveAbortController) {
  518. saveAbortController.abort();
  519. }
  520. // 创建新的 AbortController
  521. const newAbortController = new AbortController();
  522. set({
  523. isSaving: true,
  524. error: null,
  525. saveAbortController: newAbortController,
  526. savingProgress: null,
  527. });
  528. // 创建保存 Promise
  529. const savePromise = (async () => {
  530. try {
  531. // 获取失败的块
  532. const blocksToRetry = blocks.filter(block =>
  533. failedBlocks.includes(block.id)
  534. );
  535. const totalBlocks = blocksToRetry.length;
  536. // 初始化进度
  537. set({ savingProgress: { current: 0, total: totalBlocks } });
  538. // 已完成的请求计数
  539. let completedCount = 0;
  540. // 使用并发限制器重试保存
  541. const tasks = blocksToRetry.map(block =>
  542. saveConcurrencyLimit(() => {
  543. // 序列化块内容(将富文本数组转为纯字符串,并提取样式到style字段)
  544. let contentToSave = block.content;
  545. let styleToSave = block.style;
  546. if (block.type === 'table') {
  547. const serializedTable = serializeTableBlock(block as TableBlock);
  548. contentToSave = serializedTable.content;
  549. } else if (block.type === 'heading' || block.type === 'paragraph') {
  550. // 对于标题和段落块,如果content是富文本数组,需要序列化
  551. if (Array.isArray(block.content)) {
  552. // 提取纯文本
  553. contentToSave = block.content.map(seg => seg.text).join('');
  554. // 如果所有片段的样式一致,提取到块级style
  555. const allSegments = block.content;
  556. if (allSegments.length > 0) {
  557. const firstStyle = allSegments[0].style;
  558. const allSameStyle = allSegments.every(seg =>
  559. JSON.stringify(seg.style) === JSON.stringify(firstStyle)
  560. );
  561. if (allSameStyle && Object.keys(firstStyle).length > 0) {
  562. // 所有片段样式一致,合并到块级style
  563. styleToSave = { ...block.style, ...firstStyle };
  564. }
  565. }
  566. }
  567. }
  568. return blockService.updateBlock(
  569. documentId,
  570. block.id,
  571. {
  572. content: contentToSave as any,
  573. style: styleToSave,
  574. word_style: block.word_style,
  575. metadata: block.metadata,
  576. },
  577. { signal: newAbortController.signal }
  578. ).then(result => {
  579. // 更新进度
  580. completedCount++;
  581. set({ savingProgress: { current: completedCount, total: totalBlocks } });
  582. return result;
  583. });
  584. })
  585. );
  586. const results = await Promise.allSettled(tasks);
  587. // 统计仍然失败的块
  588. const stillFailedIds: string[] = [];
  589. const successIds: string[] = [];
  590. results.forEach((result, index) => {
  591. if (result.status === 'rejected') {
  592. stillFailedIds.push(blocksToRetry[index].id);
  593. } else {
  594. successIds.push(blocksToRetry[index].id);
  595. }
  596. });
  597. // 从脏块集合中移除成功的块
  598. const { dirtyBlocks } = get();
  599. const newDirtyBlocks = new Set(dirtyBlocks);
  600. successIds.forEach(id => newDirtyBlocks.delete(id));
  601. if (stillFailedIds.length > 0) {
  602. // 仍有块保存失败
  603. // 更新成功保存的块的哈希值
  604. const newBlockHashes = new Map(get().blockHashes);
  605. successIds.forEach(id => {
  606. const block = blocks.find(b => b.id === id);
  607. if (block) {
  608. newBlockHashes.set(id, computeBlockHash(block));
  609. }
  610. });
  611. set({
  612. isSaving: false,
  613. failedBlocks: stillFailedIds,
  614. dirtyBlocks: newDirtyBlocks,
  615. blockHashes: newBlockHashes, // 更新成功块的哈希
  616. hasModified: newDirtyBlocks.size > 0,
  617. error: `重试后仍有 ${stillFailedIds.length}/${totalBlocks} 个块保存失败`,
  618. currentSavePromise: null,
  619. saveAbortController: null,
  620. savingProgress: null,
  621. });
  622. throw new Error(`${stillFailedIds.length} 个块保存失败`);
  623. } else {
  624. // 全部重试成功
  625. const snapshot = JSON.stringify(blocks);
  626. // 更新所有成功保存的块的哈希值
  627. const newBlockHashes = new Map(get().blockHashes);
  628. successIds.forEach(id => {
  629. const block = blocks.find(b => b.id === id);
  630. if (block) {
  631. newBlockHashes.set(id, computeBlockHash(block));
  632. }
  633. });
  634. set({
  635. isSaving: false,
  636. hasModified: newDirtyBlocks.size > 0,
  637. originalBlocksSnapshot: snapshot,
  638. lastSavedSnapshot: snapshot,
  639. failedBlocks: [],
  640. dirtyBlocks: newDirtyBlocks,
  641. blockHashes: newBlockHashes, // 更新哈希表
  642. currentSavePromise: null,
  643. saveAbortController: null,
  644. savingProgress: null,
  645. });
  646. }
  647. } catch (error: any) {
  648. // 如果是请求取消,不算错误
  649. if (error.name === 'CanceledError' || error.message?.includes('canceled')) {
  650. set({
  651. isSaving: false,
  652. currentSavePromise: null,
  653. saveAbortController: null,
  654. savingProgress: null,
  655. });
  656. return;
  657. }
  658. set({
  659. error: error.message || '重试失败',
  660. isSaving: false,
  661. currentSavePromise: null,
  662. saveAbortController: null,
  663. savingProgress: null,
  664. });
  665. throw error;
  666. }
  667. })();
  668. // 保存 Promise 到 state
  669. set({ currentSavePromise: savePromise });
  670. return savePromise;
  671. },
  672. // ── addBlock ────────────────────────────────────────────────────────────
  673. addBlock: async (partialBlock: PartialBlock, afterBlockId?: string) => {
  674. const { blocks, blockHashes, documentId } = get();
  675. if (!documentId) {
  676. message.error('没有打开的文档');
  677. return;
  678. }
  679. // 找到插入位置
  680. const afterIndex = afterBlockId
  681. ? blocks.findIndex((b) => b.id === afterBlockId)
  682. : blocks.length - 1;
  683. // 计算block_order
  684. let newOrder: number;
  685. if (blocks.length === 0) {
  686. // 空文档,从0开始
  687. newOrder = 0;
  688. } else if (afterIndex === -1 || afterIndex === blocks.length - 1) {
  689. // 追加到末尾
  690. const lastBlock = blocks[blocks.length - 1];
  691. newOrder = lastBlock ? lastBlock.block_order + 100 : 0;
  692. } else {
  693. // 插入到中间
  694. const prevOrder = blocks[afterIndex].block_order;
  695. const nextOrder = blocks[afterIndex + 1].block_order;
  696. newOrder = computeInsertOrder(prevOrder, nextOrder);
  697. if (newOrder === -1) {
  698. // 需要重排
  699. message.error('需要重排block_order,功能待实现');
  700. return;
  701. }
  702. }
  703. // 先在本地创建临时块(使用临时ID)
  704. const tempId = generateBlockId(partialBlock.type);
  705. const tempBlock: DocumentBlock = {
  706. id: tempId,
  707. block_order: newOrder,
  708. level: partialBlock.level ?? 0,
  709. index: partialBlock.index ?? 0,
  710. word_style: partialBlock.word_style ?? 'Normal',
  711. style: partialBlock.style ?? {},
  712. metadata: partialBlock.metadata ?? {},
  713. ...partialBlock,
  714. } as DocumentBlock;
  715. // 立即添加到本地状态(乐观更新)
  716. const newBlocks = [...blocks, tempBlock].sort(
  717. (a, b) => a.block_order - b.block_order
  718. );
  719. set({
  720. blocks: newBlocks,
  721. });
  722. // 调用后端API创建块
  723. try {
  724. const response: any = await blockService.createBlock(documentId, {
  725. type: partialBlock.type,
  726. level: partialBlock.level ?? 0,
  727. index: partialBlock.index ?? 0, // 添加index字段
  728. content: partialBlock.content,
  729. word_style: partialBlock.word_style ?? 'Normal',
  730. style: partialBlock.style ?? {},
  731. metadata: partialBlock.metadata ?? {},
  732. after_block_id: afterBlockId || null,
  733. });
  734. // 后端返回 { blockId: string },我们需要使用这个ID更新临时块
  735. const realBlockId = response.blockId || response.data?.blockId;
  736. if (!realBlockId) {
  737. throw new Error('后端未返回blockId');
  738. }
  739. // 用后端返回的真实ID替换临时ID
  740. const { blocks: currentBlocks } = get();
  741. const updatedBlocks = currentBlocks.map(b => {
  742. if (b.id === tempId) {
  743. const realBlock = { ...b, id: realBlockId };
  744. return realBlock;
  745. }
  746. return b;
  747. });
  748. // 找到更新后的真实块
  749. const realBlock = updatedBlocks.find(b => b.id === realBlockId);
  750. if (realBlock) {
  751. // 添加到哈希表(新创建的块认为是已保存状态)
  752. const newBlockHashes = new Map(blockHashes);
  753. newBlockHashes.set(realBlockId, computeBlockHash(realBlock));
  754. set({
  755. blocks: updatedBlocks,
  756. blockHashes: newBlockHashes,
  757. });
  758. } else {
  759. throw new Error('无法找到创建的块');
  760. }
  761. } catch (error: any) {
  762. // 创建失败,回滚本地状态
  763. const { blocks: currentBlocks } = get();
  764. const rolledBackBlocks = currentBlocks.filter(b => b.id !== tempId);
  765. set({
  766. blocks: rolledBackBlocks,
  767. });
  768. message.error(error.message || '创建块失败');
  769. throw error;
  770. }
  771. },
  772. // ── updateBlock ─────────────────────────────────────────────────────────
  773. updateBlock: (id: string, updates: BlockUpdate) => {
  774. const { blocks, dirtyBlocks, blockHashes, autoSaveEnabled } = get();
  775. const newBlocks = blocks.map((block) =>
  776. block.id === id ? { ...block, ...updates } as DocumentBlock : block
  777. );
  778. // 获取更新后的块
  779. const updatedBlock = newBlocks.find(b => b.id === id);
  780. if (!updatedBlock) {
  781. return; // 块不存在,跳过
  782. }
  783. // 计算新的哈希值
  784. const newHash = computeBlockHash(updatedBlock);
  785. const originalHash = blockHashes.get(id);
  786. // 比较哈希值,只有真正改变时才标记为脏块
  787. const newDirtyBlocks = new Set(dirtyBlocks);
  788. if (originalHash && newHash === originalHash) {
  789. // 内容与原始版本相同,从脏块中移除(如果存在)
  790. newDirtyBlocks.delete(id);
  791. } else if (newHash !== originalHash) {
  792. // 内容真正发生了变化,标记为脏块
  793. newDirtyBlocks.add(id);
  794. }
  795. set({
  796. blocks: newBlocks,
  797. dirtyBlocks: newDirtyBlocks,
  798. hasModified: newDirtyBlocks.size > 0,
  799. });
  800. // 触发自动保存
  801. if (autoSaveEnabled && newDirtyBlocks.size > 0) {
  802. get().triggerAutoSave();
  803. }
  804. },
  805. // ── deleteBlock ─────────────────────────────────────────────────────────
  806. deleteBlock: async (id: string) => {
  807. const { blocks, dirtyBlocks, blockHashes, documentId } = get();
  808. if (!documentId) {
  809. message.error('没有打开的文档');
  810. return;
  811. }
  812. // 找到要删除的块
  813. const blockToDelete = blocks.find(b => b.id === id);
  814. if (!blockToDelete) {
  815. return;
  816. }
  817. // 先从本地状态删除(乐观更新)
  818. const newBlocks = blocks.filter((block) => block.id !== id);
  819. // 删除块时,也需要从脏块集合中移除(如果存在)
  820. const newDirtyBlocks = new Set(dirtyBlocks);
  821. newDirtyBlocks.delete(id);
  822. // 从哈希表中移除
  823. const newBlockHashes = new Map(blockHashes);
  824. const wasOriginalBlock = blockHashes.has(id);
  825. newBlockHashes.delete(id);
  826. // 立即更新本地状态(不标记为已修改,因为我们会直接调用后端API)
  827. set({
  828. blocks: newBlocks,
  829. dirtyBlocks: newDirtyBlocks,
  830. blockHashes: newBlockHashes,
  831. });
  832. try {
  833. // 1. 调用后端API删除块
  834. await blockService.deleteBlock(documentId, id);
  835. // 2. 删除成功后,重新计算并更新所有块的 block_order
  836. // 按照当前顺序重新分配 block_order(使用稀疏排序,间隔100)
  837. const sortedBlocks = [...newBlocks].sort((a, b) => a.block_order - b.block_order);
  838. const blocksNeedUpdate: Array<{block: DocumentBlock; newOrder: number}> = [];
  839. sortedBlocks.forEach((block, index) => {
  840. const expectedOrder = index * 100;
  841. if (block.block_order !== expectedOrder) {
  842. blocksNeedUpdate.push({
  843. block,
  844. newOrder: expectedOrder,
  845. });
  846. }
  847. });
  848. // 3. 如果有块需要更新顺序,批量调用 PUT API 更新
  849. if (blocksNeedUpdate.length > 0) {
  850. // 并发调用 PUT API 更新所有受影响的块
  851. const updatePromises = blocksNeedUpdate.map(({ block, newOrder }) => {
  852. // 对于TOC块,只更新block_order,不传递其他字段
  853. // 因为后端对TOC块有严格的验证限制
  854. if (block.type === 'toc') {
  855. return blockService.updateBlock(documentId, block.id, {
  856. block_order: newOrder,
  857. });
  858. }
  859. // 非TOC块:传递完整的数据
  860. return blockService.updateBlock(documentId, block.id, {
  861. content: block.content as any,
  862. style: block.style,
  863. word_style: block.word_style,
  864. metadata: block.metadata,
  865. block_order: newOrder,
  866. });
  867. });
  868. await Promise.all(updatePromises);
  869. // 4. 更新本地状态中的 block_order,并更新这些块的哈希值
  870. const { blocks: currentBlocks, blockHashes: currentBlockHashes } = get();
  871. const updatedBlocks = currentBlocks.map(b => {
  872. const update = blocksNeedUpdate.find(u => u.block.id === b.id);
  873. if (update) {
  874. return { ...b, block_order: update.newOrder };
  875. }
  876. return b;
  877. });
  878. // 更新这些块的哈希值,因为后端已经保存了
  879. const updatedBlockHashes = new Map(currentBlockHashes);
  880. updatedBlocks.forEach(block => {
  881. if (blocksNeedUpdate.some(u => u.block.id === block.id)) {
  882. updatedBlockHashes.set(block.id, computeBlockHash(block));
  883. }
  884. });
  885. // 5. 更新本地状态,不标记为已修改(因为后端已经同步)
  886. set({
  887. blocks: updatedBlocks,
  888. blockHashes: updatedBlockHashes,
  889. hasModified: false, // 所有操作都已同步到后端
  890. });
  891. } else {
  892. // 没有块需要更新顺序(可能删除的是最后一个块)
  893. set({ hasModified: false }); // 标记为已同步
  894. }
  895. } catch (error: any) {
  896. // 删除或更新失败,回滚本地状态
  897. // 恢复被删除的块
  898. if (blockToDelete) {
  899. const { blocks: currentBlocks } = get();
  900. const restoredBlocks = [...currentBlocks, blockToDelete].sort(
  901. (a, b) => a.block_order - b.block_order
  902. );
  903. // 恢复哈希表
  904. const restoredBlockHashes = new Map(get().blockHashes);
  905. if (wasOriginalBlock) {
  906. restoredBlockHashes.set(id, computeBlockHash(blockToDelete));
  907. }
  908. set({
  909. blocks: restoredBlocks,
  910. blockHashes: restoredBlockHashes,
  911. });
  912. }
  913. message.error(error.message || '删除块失败');
  914. throw error;
  915. }
  916. },
  917. // ── moveBlock ───────────────────────────────────────────────────────────
  918. moveBlock: (id: string, targetOrder: number) => {
  919. const { blocks } = get();
  920. const newBlocks = blocks.map((block) =>
  921. block.id === id ? { ...block, block_order: targetOrder } : block
  922. );
  923. set({ blocks: newBlocks });
  924. },
  925. // ── selectBlock ─────────────────────────────────────────────────────────
  926. selectBlock: (id: string | null) => {
  927. set({ selectedBlockId: id });
  928. },
  929. // ── getBlockById ────────────────────────────────────────────────────────
  930. getBlockById: (id: string) => {
  931. const { blocks } = get();
  932. return blocks.find((block) => block.id === id);
  933. },
  934. // ── getBlocksByType ─────────────────────────────────────────────────────
  935. getBlocksByType: (type: BlockType) => {
  936. const { blocks } = get();
  937. return blocks.filter((block) => block.type === type);
  938. },
  939. // ── isBlockDirty ────────────────────────────────────────────────────────
  940. isBlockDirty: (id: string) => {
  941. const { dirtyBlocks } = get();
  942. return dirtyBlocks.has(id);
  943. },
  944. // ── getDirtyBlocks ──────────────────────────────────────────────────────
  945. getDirtyBlocks: () => {
  946. const { blocks, dirtyBlocks } = get();
  947. return blocks.filter(block => dirtyBlocks.has(block.id));
  948. },
  949. // ── saveBlock ───────────────────────────────────────────────────────────
  950. saveBlock: async (id: string) => {
  951. const { documentId, blocks, dirtyBlocks, blockHashes } = get();
  952. if (!documentId) {
  953. throw new Error('没有打开的文档');
  954. }
  955. const block = blocks.find(b => b.id === id);
  956. if (!block) {
  957. throw new Error('块不存在');
  958. }
  959. // 检查是否为只读块(如TOC块)
  960. if (block.type === 'toc') {
  961. // TOC块是只读的,跳过保存
  962. return;
  963. }
  964. // 检查metadata中的readonly标志(使用类型安全的方式)
  965. const metadata = block.metadata as any;
  966. if (metadata?.readonly === true || metadata?.is_auto_generated === true) {
  967. // 只读块,跳过保存
  968. return;
  969. }
  970. // **关键修复**: 在保存前先计算当前块的哈希值,并与原始哈希比较
  971. // 如果哈希值相同,说明内容没有真正变化,跳过保存
  972. const currentHash = computeBlockHash(block);
  973. const originalHash = blockHashes.get(id);
  974. if (originalHash && currentHash === originalHash) {
  975. // 从脏块集合中移除(可能是误标记)
  976. const newDirtyBlocks = new Set(dirtyBlocks);
  977. newDirtyBlocks.delete(id);
  978. set({
  979. dirtyBlocks: newDirtyBlocks,
  980. hasModified: newDirtyBlocks.size > 0,
  981. });
  982. return;
  983. }
  984. set({ isSaving: true, error: null });
  985. try {
  986. // 序列化表格块的content(将富文本数组转为纯字符串)
  987. let contentToSave = block.content;
  988. if (block.type === 'table') {
  989. const serializedTable = serializeTableBlock(block as TableBlock);
  990. contentToSave = serializedTable.content;
  991. }
  992. await blockService.updateBlock(documentId, id, {
  993. type: block.type,
  994. level: block.level,
  995. content: contentToSave as any, // 类型断言:不同block类型的content类型不同
  996. style: block.style,
  997. word_style: block.word_style,
  998. metadata: block.metadata,
  999. });
  1000. // **关键修复**: 保存成功后,需要重新获取最新的blocks状态
  1001. // 因为在保存期间可能又有新的更新
  1002. const { blocks: latestBlocks, dirtyBlocks: latestDirtyBlocks, blockHashes: latestBlockHashes } = get();
  1003. const latestBlock = latestBlocks.find(b => b.id === id);
  1004. if (latestBlock) {
  1005. // 保存成功后,从脏块集合中移除该块
  1006. const newDirtyBlocks = new Set(latestDirtyBlocks);
  1007. newDirtyBlocks.delete(id);
  1008. // **关键修复**: 使用保存成功时的块内容计算哈希值
  1009. const newBlockHashes = new Map(latestBlockHashes);
  1010. newBlockHashes.set(id, computeBlockHash(latestBlock));
  1011. set({
  1012. isSaving: false,
  1013. dirtyBlocks: newDirtyBlocks,
  1014. blockHashes: newBlockHashes,
  1015. hasModified: newDirtyBlocks.size > 0,
  1016. });
  1017. } else {
  1018. // 块已被删除
  1019. set({ isSaving: false });
  1020. }
  1021. } catch (error: any) {
  1022. // 保存失败,保持脏块标记
  1023. set({
  1024. error: error.message || '保存块失败',
  1025. isSaving: false,
  1026. });
  1027. throw error;
  1028. }
  1029. },
  1030. // ── markAsModified ──────────────────────────────────────────────────────
  1031. markAsModified: () => {
  1032. set({ hasModified: true });
  1033. },
  1034. // ── markAsSaved ─────────────────────────────────────────────────────────
  1035. markAsSaved: () => {
  1036. const { blocks } = get();
  1037. const snapshot = JSON.stringify(blocks);
  1038. set({
  1039. hasModified: false,
  1040. originalBlocksSnapshot: snapshot,
  1041. lastSavedSnapshot: snapshot,
  1042. dirtyBlocks: new Set<string>(), // 清空脏块集合
  1043. });
  1044. },
  1045. // ── recomputeBlockOrders ────────────────────────────────────────────────
  1046. recomputeBlockOrders: () => {
  1047. const { blocks } = get();
  1048. const newBlocks = rebalanceOrders(blocks);
  1049. set({ blocks: newBlocks });
  1050. },
  1051. // ── setAutoSaveEnabled ──────────────────────────────────────────────────
  1052. setAutoSaveEnabled: (enabled: boolean) => {
  1053. set({ autoSaveEnabled: enabled });
  1054. if (!enabled) {
  1055. // 禁用时取消现有的自动保存定时器
  1056. get().cancelAutoSave();
  1057. }
  1058. },
  1059. // ── triggerAutoSave ─────────────────────────────────────────────────────
  1060. triggerAutoSave: () => {
  1061. const { autoSaveTimer, autoSaveEnabled, dirtyBlocks } = get();
  1062. // 如果自动保存未启用或没有脏块,直接返回
  1063. if (!autoSaveEnabled || dirtyBlocks.size === 0) {
  1064. return;
  1065. }
  1066. // 清除现有的定时器
  1067. if (autoSaveTimer) {
  1068. clearTimeout(autoSaveTimer);
  1069. }
  1070. // 设置新的定时器
  1071. const newTimer = setTimeout(() => {
  1072. get().saveDocument();
  1073. }, AUTO_SAVE_DELAY);
  1074. set({ autoSaveTimer: newTimer });
  1075. },
  1076. // ── cancelAutoSave ──────────────────────────────────────────────────────
  1077. cancelAutoSave: () => {
  1078. const { autoSaveTimer } = get();
  1079. if (autoSaveTimer) {
  1080. clearTimeout(autoSaveTimer);
  1081. set({ autoSaveTimer: null });
  1082. }
  1083. },
  1084. // ── reset ───────────────────────────────────────────────────────────────
  1085. reset: () => {
  1086. // 清除自动保存定时器
  1087. const { autoSaveTimer } = get();
  1088. if (autoSaveTimer) {
  1089. clearTimeout(autoSaveTimer);
  1090. }
  1091. set(initialState);
  1092. },
  1093. }));
  1094. export default useEditorStore;