Procházet zdrojové kódy

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

Zhang Yice před 1 měsícem
rodič
revize
1301f9cbd4

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

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

+ 48 - 1
src/stores/editorStore.ts

@@ -38,6 +38,7 @@ const AUTO_SAVE_DELAY = 3000; // 3秒无操作后自动保存
38 38
 // 最大重试次数
39 39
 const MAX_RETRY_ATTEMPTS = 3;
40 40
 const MAX_HISTORY_ENTRIES = 100;
41
+const HISTORY_COALESCE_WINDOW = 500;
41 42
 
42 43
 function waitForStructuralOperations(isReady: () => boolean): Promise<void> {
43 44
   return new Promise((resolve) => {
@@ -140,6 +141,8 @@ interface EditorStore {
140 141
   selectedBlockId: string | null;
141 142
   past: EditorHistoryEntry[];
142 143
   future: EditorHistoryEntry[];
144
+  lastHistoryEditBlockId: string | null;
145
+  lastHistoryEditAt: number | null;
143 146
   isHistoryApplying: boolean;
144 147
   pendingStructuralOperations: number;
145 148
 
@@ -359,6 +362,8 @@ const initialState = {
359 362
   selectedBlockId: null,
360 363
   past: [],
361 364
   future: [],
365
+  lastHistoryEditBlockId: null,
366
+  lastHistoryEditAt: null,
362 367
   isHistoryApplying: false,
363 368
   pendingStructuralOperations: 0,
364 369
   isLoading: false,
@@ -388,9 +393,36 @@ export const useEditorStore = create<EditorStore>((set, get) => {
388 393
         -MAX_HISTORY_ENTRIES
389 394
       ),
390 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 426
   const restoreHistoryEntry = async (entry: EditorHistoryEntry) => {
395 427
     const { blockHashes, autoSaveEnabled, blocks: currentBlocks, documentId } = get();
396 428
     let restoredBlocks = entry.blocks;
@@ -540,6 +572,8 @@ export const useEditorStore = create<EditorStore>((set, get) => {
540 572
           loadAbortController: null,
541 573
           past: [],
542 574
           future: [],
575
+          lastHistoryEditBlockId: null,
576
+          lastHistoryEditAt: null,
543 577
         });
544 578
       } catch (error: unknown) {
545 579
         if (controller.signal.aborted || isCanceledRequest(error)) return;
@@ -1149,6 +1183,15 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1149 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 1195
       const previousBlocks = blocks;
1153 1196
 
1154 1197
       const newBlocks = blocks.slice();
@@ -1179,7 +1222,7 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1179 1222
         hasModified: newDirtyBlocks.size > 0,
1180 1223
       });
1181 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 1445
           future: [...state.future, { blocks: snapshotBlocks(blocks), selectedBlockId }].slice(
1403 1446
             -MAX_HISTORY_ENTRIES
1404 1447
           ),
1448
+          lastHistoryEditBlockId: null,
1449
+          lastHistoryEditAt: null,
1405 1450
           isHistoryApplying: false,
1406 1451
         }));
1407 1452
       } catch (error: unknown) {
@@ -1434,6 +1479,8 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1434 1479
           past: [...state.past, { blocks: snapshotBlocks(blocks), selectedBlockId }].slice(
1435 1480
             -MAX_HISTORY_ENTRIES
1436 1481
           ),
1482
+          lastHistoryEditBlockId: null,
1483
+          lastHistoryEditAt: null,
1437 1484
           isHistoryApplying: false,
1438 1485
         }));
1439 1486
       } catch (error: unknown) {

+ 0 - 2
vite.config.ts

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