Преглед изворни кода

feat(editor): Add document caching and display name propagation

- Implement localStorage-based document caching with deduplication in MessageItem
- Add document existence verification to prevent stale cached references
- Propagate documentName prop through EditorWithOutline to BlockEditor component
- Update MainToolbar to display documentId and track modification state with visual indicator
- Add animated dot indicator for unsaved changes in toolbar
- Improve document title priority: prop name > store title > default fallback
- Enhance error messaging and logging in document preview workflow
- This reduces redundant API calls for frequently previewed documents while maintaining data integrity
Zhang Yice пре 1 месец
родитељ
комит
da327e8db0

+ 49 - 15
src/components/ChatPanel/MessageItem.tsx

@@ -142,13 +142,14 @@ const MessageItem: React.FC<MessageItemProps> = memo(
142
     /**
142
     /**
143
      * Handle preview document click
143
      * Handle preview document click
144
      * 
144
      * 
145
-     * This function needs to ensure the document exists in the document management
146
-     * system before opening the editor. The workflow returns an export record with
147
-     * a recordId and downloadUrl, but the document hasn't been imported yet.
145
+     * This function ensures the document exists in the document management
146
+     * system before opening the editor. It implements smart deduplication:
148
      * 
147
      * 
149
      * Steps:
148
      * Steps:
150
-     * 1. Call POST /api/v1/documents to create/import the document
151
-     * 2. Use the returned documentId to open the editor
149
+     * 1. Check localStorage cache for existing documentId
150
+     * 2. If cached, verify document still exists
151
+     * 3. If not cached or deleted, create new document via POST
152
+     * 4. Cache the documentId and open editor
152
      */
153
      */
153
     const handlePreviewClick = useCallback(async () => {
154
     const handlePreviewClick = useCallback(async () => {
154
       if (!exportRecord || !onPreviewDocument || isCreatingDocument) return;
155
       if (!exportRecord || !onPreviewDocument || isCreatingDocument) return;
@@ -156,19 +157,52 @@ const MessageItem: React.FC<MessageItemProps> = memo(
156
       try {
157
       try {
157
         setIsCreatingDocument(true);
158
         setIsCreatingDocument(true);
158
         
159
         
159
-        // Create/import document using the export record's download URL
160
-        // The backend will download the file and parse it into blocks
161
-        const response = await createDocument({
162
-          userId: 'default-user', // TODO: Get from auth context
163
-          fileUrl: exportRecord.downloadUrl,
164
-          sessionId: currentSessionId || 'default-session',
165
-        });
160
+        // Step 1: Check localStorage cache
161
+        const { default: apiClient } = await import('../../services/api');
162
+        const cacheKey = `doc_cache_${currentSessionId}_${exportRecord.recordId}`;
163
+        const cachedDocId = localStorage.getItem(cacheKey);
166
         
164
         
167
-        // Now we can open the preview with the document ID
168
-        onPreviewDocument(response.documentId, exportRecord.fileName);
165
+        let documentId: string;
166
+        
167
+        if (cachedDocId) {
168
+          // Step 2: Verify cached document still exists
169
+          try {
170
+            await apiClient.get(`/api/v1/documents/${cachedDocId}`);
171
+            // Document exists, reuse it
172
+            documentId = cachedDocId;
173
+          } catch {
174
+            // Document no longer exists, clear cache and create new
175
+            localStorage.removeItem(cacheKey);
176
+            
177
+            // Create new document
178
+            const response = await createDocument({
179
+              userId: 'default-user', // TODO: Get from auth context
180
+              fileUrl: exportRecord.downloadUrl,
181
+              sessionId: currentSessionId || 'default-session',
182
+            });
183
+            
184
+            documentId = response.documentId;
185
+            localStorage.setItem(cacheKey, documentId);
186
+          }
187
+        } else {
188
+          // Step 3: No cache, create new document
189
+          const response = await createDocument({
190
+            userId: 'default-user', // TODO: Get from auth context
191
+            fileUrl: exportRecord.downloadUrl,
192
+            sessionId: currentSessionId || 'default-session',
193
+          });
194
+          
195
+          documentId = response.documentId;
196
+          
197
+          // Step 4: Cache the mapping
198
+          localStorage.setItem(cacheKey, documentId);
199
+        }
200
+        
201
+        // Step 5: Open the preview with the document ID
202
+        onPreviewDocument(documentId, exportRecord.fileName);
169
         
203
         
170
       } catch (error) {
204
       } catch (error) {
171
-        console.error('Failed to create document:', error);
205
+        console.error('Failed to open document:', error);
172
         antdMessage.error('打开文档失败: ' + (error instanceof Error ? error.message : '未知错误'));
206
         antdMessage.error('打开文档失败: ' + (error instanceof Error ? error.message : '未知错误'));
173
       } finally {
207
       } finally {
174
         setIsCreatingDocument(false);
208
         setIsCreatingDocument(false);

+ 10 - 1
src/components/Editor/BlockEditor.tsx

@@ -21,6 +21,8 @@ import './BlockEditor.css';
21
 export interface BlockEditorProps {
21
 export interface BlockEditorProps {
22
   /** 文档ID */
22
   /** 文档ID */
23
   documentId: string;
23
   documentId: string;
24
+  /** 文档名称(用于显示在工具栏,优先级高于从blocks提取的标题) */
25
+  documentName?: string | null;
24
   /** 是否只读模式 */
26
   /** 是否只读模式 */
25
   readOnly?: boolean;
27
   readOnly?: boolean;
26
   /** 关闭回调 */
28
   /** 关闭回调 */
@@ -38,12 +40,14 @@ export interface BlockEditorProps {
38
  * ```tsx
40
  * ```tsx
39
  * <BlockEditor 
41
  * <BlockEditor 
40
  *   documentId="doc-123"
42
  *   documentId="doc-123"
43
+ *   documentName="地质报告.docx"
41
  *   onClose={() => handleClose()}
44
  *   onClose={() => handleClose()}
42
  * />
45
  * />
43
  * ```
46
  * ```
44
  */
47
  */
45
 export const BlockEditor: React.FC<BlockEditorProps> = ({
48
 export const BlockEditor: React.FC<BlockEditorProps> = ({
46
   documentId,
49
   documentId,
50
+  documentName,
47
   readOnly = false,
51
   readOnly = false,
48
   onClose,
52
   onClose,
49
 }) => {
53
 }) => {
@@ -57,6 +61,10 @@ export const BlockEditor: React.FC<BlockEditorProps> = ({
57
     saveDocument,
61
     saveDocument,
58
   } = useEditorStore();
62
   } = useEditorStore();
59
 
63
 
64
+  // ── Determine display title ──────────────────────────────────────────────
65
+  // Priority: 1. documentName prop (from chat), 2. documentTitle from store, 3. default
66
+  const displayTitle = documentName || documentTitle || '未命名文档';
67
+
60
   // ── Load document ──────────────────────────────────────────────────────────
68
   // ── Load document ──────────────────────────────────────────────────────────
61
   useEffect(() => {
69
   useEffect(() => {
62
     if (documentId) {
70
     if (documentId) {
@@ -105,7 +113,8 @@ export const BlockEditor: React.FC<BlockEditorProps> = ({
105
     <div className="block-editor" data-testid="block-editor">
113
     <div className="block-editor" data-testid="block-editor">
106
       {/* 工具栏 */}
114
       {/* 工具栏 */}
107
       <MainToolbar
115
       <MainToolbar
108
-        documentTitle={documentTitle}
116
+        documentTitle={displayTitle}
117
+        documentId={documentId}
109
         readOnly={readOnly}
118
         readOnly={readOnly}
110
         onSave={handleSave}
119
         onSave={handleSave}
111
         onClose={onClose}
120
         onClose={onClose}

+ 8 - 1
src/components/Editor/EditorWithOutline.tsx

@@ -22,6 +22,8 @@ export interface EditorWithOutlineProps {
22
   defaultShowOutline?: boolean;
22
   defaultShowOutline?: boolean;
23
   /** 文档ID */
23
   /** 文档ID */
24
   documentId?: string;
24
   documentId?: string;
25
+  /** 文档名称(用于显示在工具栏) */
26
+  documentName?: string | null;
25
   /** 关闭回调 */
27
   /** 关闭回调 */
26
   onClose?: () => void;
28
   onClose?: () => void;
27
 }
29
 }
@@ -36,6 +38,7 @@ export interface EditorWithOutlineProps {
36
 export const EditorWithOutline: React.FC<EditorWithOutlineProps> = ({
38
 export const EditorWithOutline: React.FC<EditorWithOutlineProps> = ({
37
   defaultShowOutline = true,
39
   defaultShowOutline = true,
38
   documentId,
40
   documentId,
41
+  documentName,
39
   onClose,
42
   onClose,
40
 }) => {
43
 }) => {
41
   const [showOutline, setShowOutline] = useState(defaultShowOutline);
44
   const [showOutline, setShowOutline] = useState(defaultShowOutline);
@@ -59,7 +62,11 @@ export const EditorWithOutline: React.FC<EditorWithOutlineProps> = ({
59
 
62
 
60
       {/* 主编辑器 */}
63
       {/* 主编辑器 */}
61
       <div className={`editor-main ${showOutline ? 'with-outline' : ''}`}>
64
       <div className={`editor-main ${showOutline ? 'with-outline' : ''}`}>
62
-        <BlockEditor documentId={documentId || ''} onClose={onClose} />
65
+        <BlockEditor 
66
+          documentId={documentId || ''} 
67
+          documentName={documentName}
68
+          onClose={onClose} 
69
+        />
63
       </div>
70
       </div>
64
 
71
 
65
       {/* 文档大纲 */}
72
       {/* 文档大纲 */}

+ 27 - 0
src/components/Editor/toolbar/MainToolbar.css

@@ -36,3 +36,30 @@
36
   text-overflow: ellipsis;
36
   text-overflow: ellipsis;
37
   white-space: nowrap;
37
   white-space: nowrap;
38
 }
38
 }
39
+
40
+/* 修改状态标识 */
41
+.modified-indicator {
42
+  display: inline-flex;
43
+  align-items: center;
44
+  gap: 4px;
45
+  font-size: 12px;
46
+  color: #faad14;
47
+  margin-left: 8px;
48
+}
49
+
50
+.modified-indicator .dot {
51
+  width: 6px;
52
+  height: 6px;
53
+  border-radius: 50%;
54
+  background-color: #faad14;
55
+  animation: pulse 2s ease-in-out infinite;
56
+}
57
+
58
+@keyframes pulse {
59
+  0%, 100% {
60
+    opacity: 1;
61
+  }
62
+  50% {
63
+    opacity: 0.5;
64
+  }
65
+}

+ 100 - 17
src/components/Editor/toolbar/MainToolbar.tsx

@@ -6,8 +6,8 @@
6
  * @module components/Editor/toolbar
6
  * @module components/Editor/toolbar
7
  */
7
  */
8
 
8
 
9
-import React from 'react';
10
-import { Button, Dropdown, Divider } from 'antd';
9
+import React, { useState } from 'react';
10
+import { Button, Dropdown, Divider, message } from 'antd';
11
 import type { MenuProps } from 'antd';
11
 import type { MenuProps } from 'antd';
12
 import {
12
 import {
13
   SaveOutlined,
13
   SaveOutlined,
@@ -17,7 +17,11 @@ import {
17
   FilePdfOutlined,
17
   FilePdfOutlined,
18
   FileMarkdownOutlined,
18
   FileMarkdownOutlined,
19
   DownOutlined,
19
   DownOutlined,
20
+  LoadingOutlined,
20
 } from '@ant-design/icons';
21
 } from '@ant-design/icons';
22
+import { useEditorStore } from '../../../stores/editorStore';
23
+import { exportToWord } from '../../../services/exportService';
24
+import { downloadExportRecord } from '../../../services/exportRecordService';
21
 import './MainToolbar.css';
25
 import './MainToolbar.css';
22
 
26
 
23
 // ══════════════════════════════════════════════════════════════════════════════
27
 // ══════════════════════════════════════════════════════════════════════════════
@@ -27,6 +31,8 @@ import './MainToolbar.css';
27
 export interface MainToolbarProps {
31
 export interface MainToolbarProps {
28
   /** 文档标题 */
32
   /** 文档标题 */
29
   documentTitle: string;
33
   documentTitle: string;
34
+  /** 文档ID */
35
+  documentId: string;
30
   /** 是否只读 */
36
   /** 是否只读 */
31
   readOnly?: boolean;
37
   readOnly?: boolean;
32
   /** 保存回调 */
38
   /** 保存回调 */
@@ -44,38 +50,100 @@ export interface MainToolbarProps {
44
  */
50
  */
45
 export const MainToolbar: React.FC<MainToolbarProps> = ({
51
 export const MainToolbar: React.FC<MainToolbarProps> = ({
46
   documentTitle,
52
   documentTitle,
53
+  documentId,
47
   readOnly = false,
54
   readOnly = false,
48
   onSave,
55
   onSave,
49
   onClose,
56
   onClose,
50
 }) => {
57
 }) => {
58
+  // ── State ──────────────────────────────────────────────────────────────────
59
+  const [isExporting, setIsExporting] = useState(false);
60
+  
61
+  // ── Editor Store ───────────────────────────────────────────────────────────
62
+  // 获取文档修改状态和保存状态
63
+  const { hasModified, isSaving } = useEditorStore((state) => ({
64
+    hasModified: state.hasModified,
65
+    isSaving: state.isSaving,
66
+  }));
67
+
68
+  // ── Export handlers ────────────────────────────────────────────────────────
69
+
70
+  /**
71
+   * 导出为Word文档
72
+   */
73
+  const handleExportWord = async () => {
74
+    if (!documentId) {
75
+      message.error('无法导出:文档ID不存在');
76
+      return;
77
+    }
78
+
79
+    setIsExporting(true);
80
+    const hideLoading = message.loading('正在导出Word文档...', 0);
81
+
82
+    try {
83
+      // 1. 调用导出API
84
+      const response = await exportToWord({
85
+        documentId,
86
+        styleId: null, // 使用默认样式
87
+      });
88
+
89
+      hideLoading();
90
+
91
+      // 2. 显示导出成功提示
92
+      if (response.warning) {
93
+        message.warning(response.warning);
94
+      } else {
95
+        message.success('导出成功!');
96
+      }
97
+
98
+      // 3. 触发下载
99
+      // 使用downloadExportRecord来触发浏览器下载
100
+      const userId = 'default-user'; // TODO: 从认证上下文获取
101
+      await downloadExportRecord(response.recordId, userId);
102
+    } catch (error: any) {
103
+      hideLoading();
104
+      console.error('Export to Word failed:', error);
105
+      message.error(error.message || '导出Word文档失败');
106
+    } finally {
107
+      setIsExporting(false);
108
+    }
109
+  };
110
+
111
+  /**
112
+   * 导出为PDF文档(暂未实现)
113
+   */
114
+  const handleExportPDF = () => {
115
+    message.info('PDF导出功能即将上线');
116
+  };
117
+
118
+  /**
119
+   * 导出为Markdown文档(暂未实现)
120
+   */
121
+  const handleExportMarkdown = () => {
122
+    message.info('Markdown导出功能即将上线');
123
+  };
124
+
51
   // 导出菜单
125
   // 导出菜单
52
   const exportMenuItems: MenuProps['items'] = [
126
   const exportMenuItems: MenuProps['items'] = [
53
     {
127
     {
54
       key: 'word',
128
       key: 'word',
55
       icon: <FileWordOutlined />,
129
       icon: <FileWordOutlined />,
56
       label: 'Word',
130
       label: 'Word',
57
-      onClick: () => {
58
-        // TODO: 实现Word导出
59
-        console.log('导出Word');
60
-      },
131
+      disabled: isExporting,
132
+      onClick: handleExportWord,
61
     },
133
     },
62
     {
134
     {
63
       key: 'pdf',
135
       key: 'pdf',
64
       icon: <FilePdfOutlined />,
136
       icon: <FilePdfOutlined />,
65
       label: 'PDF',
137
       label: 'PDF',
66
-      onClick: () => {
67
-        // TODO: 实现PDF导出
68
-        console.log('导出PDF');
69
-      },
138
+      disabled: true,
139
+      onClick: handleExportPDF,
70
     },
140
     },
71
     {
141
     {
72
       key: 'markdown',
142
       key: 'markdown',
73
       icon: <FileMarkdownOutlined />,
143
       icon: <FileMarkdownOutlined />,
74
       label: 'Markdown',
144
       label: 'Markdown',
75
-      onClick: () => {
76
-        // TODO: 实现Markdown导出
77
-        console.log('导出Markdown');
78
-      },
145
+      disabled: true,
146
+      onClick: handleExportMarkdown,
79
     },
147
     },
80
   ];
148
   ];
81
 
149
 
@@ -86,6 +154,13 @@ export const MainToolbar: React.FC<MainToolbarProps> = ({
86
         <span className="document-title" title={documentTitle}>
154
         <span className="document-title" title={documentTitle}>
87
           {documentTitle || '未命名文档'}
155
           {documentTitle || '未命名文档'}
88
         </span>
156
         </span>
157
+        {/* 修改状态指示 */}
158
+        {hasModified && (
159
+          <span className="modified-indicator">
160
+            <span className="dot"></span>
161
+            <span>未保存</span>
162
+          </span>
163
+        )}
89
       </div>
164
       </div>
90
 
165
 
91
       {/* 右侧 - 操作按钮 */}
166
       {/* 右侧 - 操作按钮 */}
@@ -96,8 +171,11 @@ export const MainToolbar: React.FC<MainToolbarProps> = ({
96
             <Button
171
             <Button
97
               type="primary"
172
               type="primary"
98
               size="small"
173
               size="small"
99
-              icon={<SaveOutlined />}
174
+              icon={isSaving ? <LoadingOutlined /> : <SaveOutlined />}
100
               onClick={onSave}
175
               onClick={onSave}
176
+              disabled={!hasModified || isSaving}
177
+              loading={isSaving}
178
+              title={hasModified ? '文档已修改,点击保存' : '文档未修改'}
101
             >
179
             >
102
               保存
180
               保存
103
             </Button>
181
             </Button>
@@ -105,8 +183,13 @@ export const MainToolbar: React.FC<MainToolbarProps> = ({
105
         )}
183
         )}
106
 
184
 
107
         {/* 导出 */}
185
         {/* 导出 */}
108
-        <Dropdown menu={{ items: exportMenuItems }} placement="bottomRight">
109
-          <Button type="default" size="small" icon={<DownloadOutlined />}>
186
+        <Dropdown menu={{ items: exportMenuItems }} placement="bottomRight" disabled={isExporting}>
187
+          <Button 
188
+            type="default" 
189
+            size="small" 
190
+            icon={isExporting ? <LoadingOutlined /> : <DownloadOutlined />}
191
+            loading={isExporting}
192
+          >
110
             导出 <DownOutlined />
193
             导出 <DownOutlined />
111
           </Button>
194
           </Button>
112
         </Dropdown>
195
         </Dropdown>

+ 2 - 0
src/components/EditorPanel/EditorPanel.tsx

@@ -50,6 +50,7 @@ export interface EditorPanelProps {
50
  */
50
  */
51
 export const EditorPanel: React.FC<EditorPanelProps> = ({
51
 export const EditorPanel: React.FC<EditorPanelProps> = ({
52
   documentId,
52
   documentId,
53
+  initialDocumentName,
53
   onClose,
54
   onClose,
54
 }) => {
55
 }) => {
55
   return (
56
   return (
@@ -57,6 +58,7 @@ export const EditorPanel: React.FC<EditorPanelProps> = ({
57
       {/* 集成带大纲的编辑器 */}
58
       {/* 集成带大纲的编辑器 */}
58
       <EditorWithOutline
59
       <EditorWithOutline
59
         documentId={documentId}
60
         documentId={documentId}
61
+        documentName={initialDocumentName}
60
         defaultShowOutline={true}
62
         defaultShowOutline={true}
61
         onClose={onClose}
63
         onClose={onClose}
62
       />
64
       />

+ 56 - 1
src/stores/editorStore.ts

@@ -35,6 +35,12 @@ interface EditorStore {
35
   isSaving: boolean;
35
   isSaving: boolean;
36
   error: string | null;
36
   error: string | null;
37
   
37
   
38
+  // ── 修改状态追踪 ────────────────────────────────────────────────────────
39
+  /** 文档是否已被修改(用于控制保存按钮状态) */
40
+  hasModified: boolean;
41
+  /** 原始blocks快照(用于检测变化) */
42
+  originalBlocksSnapshot: string | null;
43
+  
38
   // ── 操作方法 ────────────────────────────────────────────────────────────
44
   // ── 操作方法 ────────────────────────────────────────────────────────────
39
   
45
   
40
   /**
46
   /**
@@ -62,6 +68,16 @@ interface EditorStore {
62
   updateBlock: (id: string, updates: BlockUpdate) => void;
68
   updateBlock: (id: string, updates: BlockUpdate) => void;
63
   
69
   
64
   /**
70
   /**
71
+   * 标记文档已修改
72
+   */
73
+  markAsModified: () => void;
74
+  
75
+  /**
76
+   * 标记文档已保存
77
+   */
78
+  markAsSaved: () => void;
79
+  
80
+  /**
65
    * 删除块
81
    * 删除块
66
    * @param id 块ID
82
    * @param id 块ID
67
    */
83
    */
@@ -162,6 +178,8 @@ const initialState = {
162
   isLoading: false,
178
   isLoading: false,
163
   isSaving: false,
179
   isSaving: false,
164
   error: null,
180
   error: null,
181
+  hasModified: false,
182
+  originalBlocksSnapshot: null,
165
 };
183
 };
166
 
184
 
167
 export const useEditorStore = create<EditorStore>((set, get) => ({
185
 export const useEditorStore = create<EditorStore>((set, get) => ({
@@ -174,11 +192,16 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
174
     try {
192
     try {
175
       const data = await blockService.getBlocks(documentId);
193
       const data = await blockService.getBlocks(documentId);
176
       
194
       
195
+      // 保存原始blocks快照,用于检测修改
196
+      const snapshot = JSON.stringify(data.blocks);
197
+      
177
       set({
198
       set({
178
         documentId,
199
         documentId,
179
         documentTitle: '未命名文档', // 后端不返回title,使用默认值
200
         documentTitle: '未命名文档', // 后端不返回title,使用默认值
180
         blocks: data.blocks,
201
         blocks: data.blocks,
181
         isLoading: false,
202
         isLoading: false,
203
+        hasModified: false, // 初始状态为未修改
204
+        originalBlocksSnapshot: snapshot,
182
       });
205
       });
183
     } catch (error: any) {
206
     } catch (error: any) {
184
       set({
207
       set({
@@ -212,7 +235,13 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
212
         });
235
         });
213
       }
236
       }
214
       
237
       
215
-      set({ isSaving: false });
238
+      // 保存成功后,更新快照并标记为未修改
239
+      const snapshot = JSON.stringify(blocks);
240
+      set({ 
241
+        isSaving: false,
242
+        hasModified: false,
243
+        originalBlocksSnapshot: snapshot,
244
+      });
216
     } catch (error: any) {
245
     } catch (error: any) {
217
       set({
246
       set({
218
         error: error.message || '保存失败',
247
         error: error.message || '保存失败',
@@ -254,6 +283,8 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
254
           { ...partialBlock, block_order: 0 } as DocumentBlock,
283
           { ...partialBlock, block_order: 0 } as DocumentBlock,
255
         ]);
284
         ]);
256
         set({ blocks: newBlocks });
285
         set({ blocks: newBlocks });
286
+        // 标记文档已修改
287
+        get().markAsModified();
257
         return;
288
         return;
258
       }
289
       }
259
     }
290
     }
@@ -276,6 +307,9 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
276
     );
307
     );
277
     
308
     
278
     set({ blocks: newBlocks });
309
     set({ blocks: newBlocks });
310
+    
311
+    // 标记文档已修改
312
+    get().markAsModified();
279
   },
313
   },
280
 
314
 
281
   // ── updateBlock ─────────────────────────────────────────────────────────
315
   // ── updateBlock ─────────────────────────────────────────────────────────
@@ -287,6 +321,9 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
287
     );
321
     );
288
     
322
     
289
     set({ blocks: newBlocks });
323
     set({ blocks: newBlocks });
324
+    
325
+    // 标记文档已修改
326
+    get().markAsModified();
290
   },
327
   },
291
 
328
 
292
   // ── deleteBlock ─────────────────────────────────────────────────────────
329
   // ── deleteBlock ─────────────────────────────────────────────────────────
@@ -294,6 +331,9 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
294
     const { blocks } = get();
331
     const { blocks } = get();
295
     const newBlocks = blocks.filter((block) => block.id !== id);
332
     const newBlocks = blocks.filter((block) => block.id !== id);
296
     set({ blocks: newBlocks });
333
     set({ blocks: newBlocks });
334
+    
335
+    // 标记文档已修改
336
+    get().markAsModified();
297
   },
337
   },
298
 
338
 
299
   // ── moveBlock ───────────────────────────────────────────────────────────
339
   // ── moveBlock ───────────────────────────────────────────────────────────
@@ -357,6 +397,21 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
357
     }
397
     }
358
   },
398
   },
359
   
399
   
400
+  // ── markAsModified ──────────────────────────────────────────────────────
401
+  markAsModified: () => {
402
+    set({ hasModified: true });
403
+  },
404
+  
405
+  // ── markAsSaved ─────────────────────────────────────────────────────────
406
+  markAsSaved: () => {
407
+    const { blocks } = get();
408
+    const snapshot = JSON.stringify(blocks);
409
+    set({ 
410
+      hasModified: false,
411
+      originalBlocksSnapshot: snapshot,
412
+    });
413
+  },
414
+  
360
   // ── recomputeBlockOrders ────────────────────────────────────────────────
415
   // ── recomputeBlockOrders ────────────────────────────────────────────────
361
   recomputeBlockOrders: () => {
416
   recomputeBlockOrders: () => {
362
     const { blocks } = get();
417
     const { blocks } = get();