Ver código fonte

feat(编辑器状态管理): 增强脏块管理逻辑,优化保存条件与状态更新

Zhang Yice 1 mês atrás
pai
commit
3516c3d9fe

+ 7 - 3
src/components/Editor/toolbar/MainToolbar.tsx

@@ -67,6 +67,7 @@ export const MainToolbar: React.FC<MainToolbarProps> = ({
67 67
   // 获取文档修改状态和保存状态
68 68
   const {
69 69
     hasModified,
70
+    failedBlocks,
70 71
     isSaving,
71 72
     autoSaveEnabled,
72 73
     setAutoSaveEnabled,
@@ -81,6 +82,7 @@ export const MainToolbar: React.FC<MainToolbarProps> = ({
81 82
   } = useEditorStore(
82 83
     useShallow((state) => ({
83 84
       hasModified: state.hasModified,
85
+      failedBlocks: state.failedBlocks,
84 86
       isSaving: state.isSaving,
85 87
       autoSaveEnabled: state.autoSaveEnabled,
86 88
       setAutoSaveEnabled: state.setAutoSaveEnabled,
@@ -110,11 +112,13 @@ export const MainToolbar: React.FC<MainToolbarProps> = ({
110 112
    * 如果有未保存的修改,弹出确认对话框
111 113
    */
112 114
   const handleClose = () => {
113
-    if (hasModified) {
115
+    if (hasModified || isSaving || failedBlocks.length > 0) {
114 116
       Modal.confirm({
115
-        title: '未保存的修改',
117
+        title: isSaving ? '文档正在保存' : '未保存的修改',
116 118
         icon: <ExclamationCircleOutlined />,
117
-        content: '您有未保存的修改,确定要关闭吗?',
119
+        content: isSaving
120
+          ? '保存请求尚未完成,关闭可能导致最新修改未写入服务器。确定要关闭吗?'
121
+          : '您有未保存或保存失败的修改,确定要关闭吗?',
118 122
         okText: '关闭',
119 123
         okType: 'danger',
120 124
         cancelText: '取消',

+ 29 - 8
src/services/blockService.ts

@@ -29,6 +29,23 @@ import type {
29 29
 
30 30
 const BLOCKS_BASE_URL = '/api/v1/documents';
31 31
 
32
+function encodeResourceId(value: string, name: string): string {
33
+  const hasUnsafeCharacter = Array.from(value).some((character) => {
34
+    const codePoint = character.codePointAt(0) ?? 0;
35
+    return codePoint < 32 || codePoint === 127 || character === '/' || character === '\\';
36
+  });
37
+
38
+  if (!value || value.length > 128 || hasUnsafeCharacter) {
39
+    throw new Error(`无效的${name}`);
40
+  }
41
+
42
+  return encodeURIComponent(value);
43
+}
44
+
45
+function documentBlocksPath(documentId: string): string {
46
+  return `${BLOCKS_BASE_URL}/${encodeResourceId(documentId, '文档 ID')}/blocks`;
47
+}
48
+
32 49
 function createServiceError(message: string, error: unknown): Error {
33 50
   const detail = getErrorMessage(error);
34 51
   return new Error(detail === '发生未知错误' ? message : `${message}: ${detail}`, {
@@ -61,7 +78,7 @@ export const blockService = {
61 78
     options?: { signal?: AbortSignal }
62 79
   ): Promise<GetBlocksResponse> {
63 80
     try {
64
-      const response = await apiClient.get(`${BLOCKS_BASE_URL}/${documentId}/blocks`, {
81
+      const response = await apiClient.get(documentBlocksPath(documentId), {
65 82
         signal: options?.signal,
66 83
       });
67 84
 
@@ -94,7 +111,9 @@ export const blockService = {
94 111
    */
95 112
   async getBlock(documentId: string, blockId: string): Promise<GetBlockResponse> {
96 113
     try {
97
-      const response = await apiClient.get(`${BLOCKS_BASE_URL}/${documentId}/blocks/${blockId}`);
114
+      const response = await apiClient.get(
115
+        `${documentBlocksPath(documentId)}/${encodeResourceId(blockId, '块 ID')}`
116
+      );
98 117
 
99 118
       const data = response.data?.data || response.data;
100 119
       return {
@@ -135,7 +154,7 @@ export const blockService = {
135 154
   ): Promise<UpdateBlockResponse> {
136 155
     try {
137 156
       const response = await apiClient.put(
138
-        `${BLOCKS_BASE_URL}/${documentId}/blocks/${blockId}`,
157
+        `${documentBlocksPath(documentId)}/${encodeResourceId(blockId, '块 ID')}`,
139 158
         updates,
140 159
         { signal: options?.signal }
141 160
       );
@@ -170,7 +189,7 @@ export const blockService = {
170 189
    */
171 190
   async createBlock(documentId: string, block: CreateBlockRequest): Promise<CreateBlockResponse> {
172 191
     try {
173
-      const response = await apiClient.post(`${BLOCKS_BASE_URL}/${documentId}/blocks`, block);
192
+      const response = await apiClient.post(documentBlocksPath(documentId), block);
174 193
 
175 194
       return response.data?.data || response.data;
176 195
     } catch (error: unknown) {
@@ -191,7 +210,9 @@ export const blockService = {
191 210
    */
192 211
   async deleteBlock(documentId: string, blockId: string): Promise<void> {
193 212
     try {
194
-      await apiClient.delete(`${BLOCKS_BASE_URL}/${documentId}/blocks/${blockId}`);
213
+      await apiClient.delete(
214
+        `${documentBlocksPath(documentId)}/${encodeResourceId(blockId, '块 ID')}`
215
+      );
195 216
     } catch (error: unknown) {
196 217
       throw createServiceError('删除块失败', error);
197 218
     }
@@ -222,7 +243,7 @@ export const blockService = {
222 243
         params.type = type;
223 244
       }
224 245
 
225
-      const response = await apiClient.get(`${BLOCKS_BASE_URL}/${documentId}/blocks/search`, {
246
+      const response = await apiClient.get(`${documentBlocksPath(documentId)}/search`, {
226 247
         params,
227 248
       });
228 249
 
@@ -251,7 +272,7 @@ export const blockService = {
251 272
    */
252 273
   async getTOC(documentId: string): Promise<GetTOCResponse> {
253 274
     try {
254
-      const response = await apiClient.get(`${BLOCKS_BASE_URL}/${documentId}/blocks/toc`);
275
+      const response = await apiClient.get(`${documentBlocksPath(documentId)}/toc`);
255 276
 
256 277
       const data = response.data?.data || response.data;
257 278
       return {
@@ -276,7 +297,7 @@ export const blockService = {
276 297
    */
277 298
   async getStats(documentId: string): Promise<BlockStatsResponse> {
278 299
     try {
279
-      const response = await apiClient.get(`${BLOCKS_BASE_URL}/${documentId}/blocks/stats`);
300
+      const response = await apiClient.get(`${documentBlocksPath(documentId)}/stats`);
280 301
 
281 302
       const data = response.data?.data || response.data;
282 303
       return {

+ 24 - 11
src/stores/editorStore.ts

@@ -557,7 +557,7 @@ export const useEditorStore = create<EditorStore>((set, get) => {
557 557
       const {
558 558
         documentId,
559 559
         blocks,
560
-        dirtyBlocks,
560
+        dirtyBlocks: currentDirtyBlocks,
561 561
         currentSavePromise,
562 562
         saveAbortController,
563 563
         retryAttempts,
@@ -568,7 +568,26 @@ export const useEditorStore = create<EditorStore>((set, get) => {
568 568
       }
569 569
 
570 570
       // 如果没有脏块,无需保存
571
+      if (currentDirtyBlocks.size === 0) {
572
+        return;
573
+      }
574
+
575
+      const dirtyBlocks = new Set(
576
+        blocks
577
+          .filter(
578
+            (block) =>
579
+              currentDirtyBlocks.has(block.id) && block.type !== 'toc' && !isReadonlyBlock(block)
580
+          )
581
+          .map((block) => block.id)
582
+      );
583
+
571 584
       if (dirtyBlocks.size === 0) {
585
+        set({
586
+          dirtyBlocks: new Set(),
587
+          failedBlocks: [],
588
+          hasModified: false,
589
+          error: null,
590
+        });
572 591
         return;
573 592
       }
574 593
 
@@ -608,16 +627,6 @@ export const useEditorStore = create<EditorStore>((set, get) => {
608 627
               return false;
609 628
             }
610 629
 
611
-            // 跳过TOC块
612
-            if (block.type === 'toc') {
613
-              return false;
614
-            }
615
-
616
-            // 跳过metadata中标记为readonly的块
617
-            if (isReadonlyBlock(block)) {
618
-              return false;
619
-            }
620
-
621 630
             return true;
622 631
           });
623 632
 
@@ -1136,6 +1145,10 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1136 1145
         return;
1137 1146
       }
1138 1147
 
1148
+      if (isReadonlyBlock(blocks[blockIndex])) {
1149
+        return;
1150
+      }
1151
+
1139 1152
       const previousBlocks = blocks;
1140 1153
 
1141 1154
       const newBlocks = blocks.slice();