Przeglądaj źródła

feat(消息列表与编辑器状态管理): 优化消息列表组件,使用 React.memo 提升性能;在编辑器状态管理中添加历史编辑记录功能

Zhang Yice 1 miesiąc temu
rodzic
commit
1301f9cbd4

+ 4 - 2
src/components/ChatPanel/MessageList.tsx

@@ -60,7 +60,7 @@ export interface MessageListProps {
60
  * <MessageList />
60
  * <MessageList />
61
  * ```
61
  * ```
62
  */
62
  */
63
-const MessageList: React.FC<MessageListProps> = ({ className }) => {
63
+const MessageList: React.FC<MessageListProps> = React.memo(({ className }) => {
64
   const messages = useChatStore((state) => state.messages);
64
   const messages = useChatStore((state) => state.messages);
65
   const openDocumentPreview = useUIStore((state) => state.openDocumentPreview);
65
   const openDocumentPreview = useUIStore((state) => state.openDocumentPreview);
66
 
66
 
@@ -160,6 +160,8 @@ const MessageList: React.FC<MessageListProps> = ({ className }) => {
160
       <div ref={bottomRef} data-testid="message-list-bottom" aria-hidden="true" />
160
       <div ref={bottomRef} data-testid="message-list-bottom" aria-hidden="true" />
161
     </div>
161
     </div>
162
   );
162
   );
163
-};
163
+});
164
+
165
+MessageList.displayName = 'MessageList';
164
 
166
 
165
 export default MessageList;
167
 export default MessageList;

+ 48 - 1
src/stores/editorStore.ts

@@ -38,6 +38,7 @@ const AUTO_SAVE_DELAY = 3000; // 3秒无操作后自动保存
38
 // 最大重试次数
38
 // 最大重试次数
39
 const MAX_RETRY_ATTEMPTS = 3;
39
 const MAX_RETRY_ATTEMPTS = 3;
40
 const MAX_HISTORY_ENTRIES = 100;
40
 const MAX_HISTORY_ENTRIES = 100;
41
+const HISTORY_COALESCE_WINDOW = 500;
41
 
42
 
42
 function waitForStructuralOperations(isReady: () => boolean): Promise<void> {
43
 function waitForStructuralOperations(isReady: () => boolean): Promise<void> {
43
   return new Promise((resolve) => {
44
   return new Promise((resolve) => {
@@ -140,6 +141,8 @@ interface EditorStore {
140
   selectedBlockId: string | null;
141
   selectedBlockId: string | null;
141
   past: EditorHistoryEntry[];
142
   past: EditorHistoryEntry[];
142
   future: EditorHistoryEntry[];
143
   future: EditorHistoryEntry[];
144
+  lastHistoryEditBlockId: string | null;
145
+  lastHistoryEditAt: number | null;
143
   isHistoryApplying: boolean;
146
   isHistoryApplying: boolean;
144
   pendingStructuralOperations: number;
147
   pendingStructuralOperations: number;
145
 
148
 
@@ -359,6 +362,8 @@ const initialState = {
359
   selectedBlockId: null,
362
   selectedBlockId: null,
360
   past: [],
363
   past: [],
361
   future: [],
364
   future: [],
365
+  lastHistoryEditBlockId: null,
366
+  lastHistoryEditAt: null,
362
   isHistoryApplying: false,
367
   isHistoryApplying: false,
363
   pendingStructuralOperations: 0,
368
   pendingStructuralOperations: 0,
364
   isLoading: false,
369
   isLoading: false,
@@ -388,9 +393,36 @@ export const useEditorStore = create<EditorStore>((set, get) => {
388
         -MAX_HISTORY_ENTRIES
393
         -MAX_HISTORY_ENTRIES
389
       ),
394
       ),
390
       future: [],
395
       future: [],
396
+      lastHistoryEditBlockId: null,
397
+      lastHistoryEditAt: null,
391
     }));
398
     }));
392
   };
399
   };
393
 
400
 
401
+  const pushBlockEditHistory = (
402
+    blocks: DocumentBlock[],
403
+    selectedBlockId: string | null,
404
+    blockId: string
405
+  ) => {
406
+    const now = Date.now();
407
+    set((state) => {
408
+      const shouldCoalesce =
409
+        state.lastHistoryEditBlockId === blockId &&
410
+        state.lastHistoryEditAt !== null &&
411
+        now - state.lastHistoryEditAt < HISTORY_COALESCE_WINDOW;
412
+
413
+      return {
414
+        past: shouldCoalesce
415
+          ? state.past
416
+          : [...state.past, { blocks: snapshotBlocks(blocks), selectedBlockId }].slice(
417
+              -MAX_HISTORY_ENTRIES
418
+            ),
419
+        future: [],
420
+        lastHistoryEditBlockId: blockId,
421
+        lastHistoryEditAt: now,
422
+      };
423
+    });
424
+  };
425
+
394
   const restoreHistoryEntry = async (entry: EditorHistoryEntry) => {
426
   const restoreHistoryEntry = async (entry: EditorHistoryEntry) => {
395
     const { blockHashes, autoSaveEnabled, blocks: currentBlocks, documentId } = get();
427
     const { blockHashes, autoSaveEnabled, blocks: currentBlocks, documentId } = get();
396
     let restoredBlocks = entry.blocks;
428
     let restoredBlocks = entry.blocks;
@@ -540,6 +572,8 @@ export const useEditorStore = create<EditorStore>((set, get) => {
540
           loadAbortController: null,
572
           loadAbortController: null,
541
           past: [],
573
           past: [],
542
           future: [],
574
           future: [],
575
+          lastHistoryEditBlockId: null,
576
+          lastHistoryEditAt: null,
543
         });
577
         });
544
       } catch (error: unknown) {
578
       } catch (error: unknown) {
545
         if (controller.signal.aborted || isCanceledRequest(error)) return;
579
         if (controller.signal.aborted || isCanceledRequest(error)) return;
@@ -1149,6 +1183,15 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1149
         return;
1183
         return;
1150
       }
1184
       }
1151
 
1185
 
1186
+      const currentBlock = blocks[blockIndex];
1187
+      const hasChanged = Object.entries(updates).some(([key, value]) => {
1188
+        const currentValue = (currentBlock as unknown as Record<string, unknown>)[key];
1189
+        return !Object.is(currentValue, value);
1190
+      });
1191
+      if (!hasChanged) {
1192
+        return;
1193
+      }
1194
+
1152
       const previousBlocks = blocks;
1195
       const previousBlocks = blocks;
1153
 
1196
 
1154
       const newBlocks = blocks.slice();
1197
       const newBlocks = blocks.slice();
@@ -1179,7 +1222,7 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1179
         hasModified: newDirtyBlocks.size > 0,
1222
         hasModified: newDirtyBlocks.size > 0,
1180
       });
1223
       });
1181
       if (newHash !== previousHash) {
1224
       if (newHash !== previousHash) {
1182
-        pushHistory(previousBlocks, get().selectedBlockId);
1225
+        pushBlockEditHistory(previousBlocks, get().selectedBlockId, id);
1183
       }
1226
       }
1184
 
1227
 
1185
       // 触发自动保存
1228
       // 触发自动保存
@@ -1402,6 +1445,8 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1402
           future: [...state.future, { blocks: snapshotBlocks(blocks), selectedBlockId }].slice(
1445
           future: [...state.future, { blocks: snapshotBlocks(blocks), selectedBlockId }].slice(
1403
             -MAX_HISTORY_ENTRIES
1446
             -MAX_HISTORY_ENTRIES
1404
           ),
1447
           ),
1448
+          lastHistoryEditBlockId: null,
1449
+          lastHistoryEditAt: null,
1405
           isHistoryApplying: false,
1450
           isHistoryApplying: false,
1406
         }));
1451
         }));
1407
       } catch (error: unknown) {
1452
       } catch (error: unknown) {
@@ -1434,6 +1479,8 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1434
           past: [...state.past, { blocks: snapshotBlocks(blocks), selectedBlockId }].slice(
1479
           past: [...state.past, { blocks: snapshotBlocks(blocks), selectedBlockId }].slice(
1435
             -MAX_HISTORY_ENTRIES
1480
             -MAX_HISTORY_ENTRIES
1436
           ),
1481
           ),
1482
+          lastHistoryEditBlockId: null,
1483
+          lastHistoryEditAt: null,
1437
           isHistoryApplying: false,
1484
           isHistoryApplying: false,
1438
         }));
1485
         }));
1439
       } catch (error: unknown) {
1486
       } catch (error: unknown) {

+ 0 - 2
vite.config.ts

@@ -72,8 +72,6 @@ export default defineConfig({
72
       'zustand',
72
       'zustand',
73
       'antd',
73
       'antd',
74
       '@ant-design/icons',
74
       '@ant-design/icons',
75
-      'html2canvas',
76
-      'jspdf',
77
     ],
75
     ],
78
     // 排除不需要预构建的大型依赖
76
     // 排除不需要预构建的大型依赖
79
     exclude: [
77
     exclude: [