Bladeren bron

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

Zhang Yice 1 maand geleden
bovenliggende
commit
3516c3d9fe
3 gewijzigde bestanden met toevoegingen van 60 en 22 verwijderingen
  1. 7 3
      src/components/Editor/toolbar/MainToolbar.tsx
  2. 29 8
      src/services/blockService.ts
  3. 24 11
      src/stores/editorStore.ts

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

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

+ 29 - 8
src/services/blockService.ts

@@ -29,6 +29,23 @@ import type {
29
 
29
 
30
 const BLOCKS_BASE_URL = '/api/v1/documents';
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
 function createServiceError(message: string, error: unknown): Error {
49
 function createServiceError(message: string, error: unknown): Error {
33
   const detail = getErrorMessage(error);
50
   const detail = getErrorMessage(error);
34
   return new Error(detail === '发生未知错误' ? message : `${message}: ${detail}`, {
51
   return new Error(detail === '发生未知错误' ? message : `${message}: ${detail}`, {
@@ -61,7 +78,7 @@ export const blockService = {
61
     options?: { signal?: AbortSignal }
78
     options?: { signal?: AbortSignal }
62
   ): Promise<GetBlocksResponse> {
79
   ): Promise<GetBlocksResponse> {
63
     try {
80
     try {
64
-      const response = await apiClient.get(`${BLOCKS_BASE_URL}/${documentId}/blocks`, {
81
+      const response = await apiClient.get(documentBlocksPath(documentId), {
65
         signal: options?.signal,
82
         signal: options?.signal,
66
       });
83
       });
67
 
84
 
@@ -94,7 +111,9 @@ export const blockService = {
94
    */
111
    */
95
   async getBlock(documentId: string, blockId: string): Promise<GetBlockResponse> {
112
   async getBlock(documentId: string, blockId: string): Promise<GetBlockResponse> {
96
     try {
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
       const data = response.data?.data || response.data;
118
       const data = response.data?.data || response.data;
100
       return {
119
       return {
@@ -135,7 +154,7 @@ export const blockService = {
135
   ): Promise<UpdateBlockResponse> {
154
   ): Promise<UpdateBlockResponse> {
136
     try {
155
     try {
137
       const response = await apiClient.put(
156
       const response = await apiClient.put(
138
-        `${BLOCKS_BASE_URL}/${documentId}/blocks/${blockId}`,
157
+        `${documentBlocksPath(documentId)}/${encodeResourceId(blockId, '块 ID')}`,
139
         updates,
158
         updates,
140
         { signal: options?.signal }
159
         { signal: options?.signal }
141
       );
160
       );
@@ -170,7 +189,7 @@ export const blockService = {
170
    */
189
    */
171
   async createBlock(documentId: string, block: CreateBlockRequest): Promise<CreateBlockResponse> {
190
   async createBlock(documentId: string, block: CreateBlockRequest): Promise<CreateBlockResponse> {
172
     try {
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
       return response.data?.data || response.data;
194
       return response.data?.data || response.data;
176
     } catch (error: unknown) {
195
     } catch (error: unknown) {
@@ -191,7 +210,9 @@ export const blockService = {
191
    */
210
    */
192
   async deleteBlock(documentId: string, blockId: string): Promise<void> {
211
   async deleteBlock(documentId: string, blockId: string): Promise<void> {
193
     try {
212
     try {
194
-      await apiClient.delete(`${BLOCKS_BASE_URL}/${documentId}/blocks/${blockId}`);
213
+      await apiClient.delete(
214
+        `${documentBlocksPath(documentId)}/${encodeResourceId(blockId, '块 ID')}`
215
+      );
195
     } catch (error: unknown) {
216
     } catch (error: unknown) {
196
       throw createServiceError('删除块失败', error);
217
       throw createServiceError('删除块失败', error);
197
     }
218
     }
@@ -222,7 +243,7 @@ export const blockService = {
222
         params.type = type;
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
         params,
247
         params,
227
       });
248
       });
228
 
249
 
@@ -251,7 +272,7 @@ export const blockService = {
251
    */
272
    */
252
   async getTOC(documentId: string): Promise<GetTOCResponse> {
273
   async getTOC(documentId: string): Promise<GetTOCResponse> {
253
     try {
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
       const data = response.data?.data || response.data;
277
       const data = response.data?.data || response.data;
257
       return {
278
       return {
@@ -276,7 +297,7 @@ export const blockService = {
276
    */
297
    */
277
   async getStats(documentId: string): Promise<BlockStatsResponse> {
298
   async getStats(documentId: string): Promise<BlockStatsResponse> {
278
     try {
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
       const data = response.data?.data || response.data;
302
       const data = response.data?.data || response.data;
282
       return {
303
       return {

+ 24 - 11
src/stores/editorStore.ts

@@ -557,7 +557,7 @@ export const useEditorStore = create<EditorStore>((set, get) => {
557
       const {
557
       const {
558
         documentId,
558
         documentId,
559
         blocks,
559
         blocks,
560
-        dirtyBlocks,
560
+        dirtyBlocks: currentDirtyBlocks,
561
         currentSavePromise,
561
         currentSavePromise,
562
         saveAbortController,
562
         saveAbortController,
563
         retryAttempts,
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
       if (dirtyBlocks.size === 0) {
584
       if (dirtyBlocks.size === 0) {
585
+        set({
586
+          dirtyBlocks: new Set(),
587
+          failedBlocks: [],
588
+          hasModified: false,
589
+          error: null,
590
+        });
572
         return;
591
         return;
573
       }
592
       }
574
 
593
 
@@ -608,16 +627,6 @@ export const useEditorStore = create<EditorStore>((set, get) => {
608
               return false;
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
             return true;
630
             return true;
622
           });
631
           });
623
 
632
 
@@ -1136,6 +1145,10 @@ export const useEditorStore = create<EditorStore>((set, get) => {
1136
         return;
1145
         return;
1137
       }
1146
       }
1138
 
1147
 
1148
+      if (isReadonlyBlock(blocks[blockIndex])) {
1149
+        return;
1150
+      }
1151
+
1139
       const previousBlocks = blocks;
1152
       const previousBlocks = blocks;
1140
 
1153
 
1141
       const newBlocks = blocks.slice();
1154
       const newBlocks = blocks.slice();