Explorar el Código

refactor: Remove mock service and integrate real API layer

- Remove MockDocumentSelector component and related mock UI logic
- Delete mock service files (mockService.ts, mockData.ts)
- Remove mock mode initialization from main.tsx
- Update blockService.ts to use only real API endpoints with improved type safety
- Update documentService.ts to integrate real API with enhanced error handling
- Refactor store types and API response types for consistency
- Simplify imports across services layer by removing mockAPI references
- Improve type definitions in editor and document types for real API contracts
- These changes complete the migration from mock-only development to production API integration
Zhang Yice hace 1 mes
padre
commit
fd2c19a475

+ 0 - 4
src/App.tsx

@@ -32,7 +32,6 @@ import zhCN from 'antd/locale/zh_CN';
32 32
 import { ErrorBoundary } from './components/common';
33 33
 import ResizableLayout from './components/Layout/ResizableLayout';
34 34
 import { useUIStore } from './stores/uiStore';
35
-import MockDocumentSelector from './components/MockDocumentSelector';
36 35
 
37 36
 // ── Lazy-loaded panel components (Req 13.2, 13.8) ────────────────────────────
38 37
 // These non-first-screen panels are split into separate chunks by Vite,
@@ -315,9 +314,6 @@ const App: React.FC = () => {
315 314
               />
316 315
             </div>
317 316
           </div>
318
-
319
-          {/* Mock文档选择器(仅Mock模式显示) */}
320
-          <MockDocumentSelector />
321 317
         </div>
322 318
       </ErrorBoundary>
323 319
     </ConfigProvider>

+ 0 - 85
src/components/MockDocumentSelector.tsx

@@ -1,85 +0,0 @@
1
-/**
2
- * MockDocumentSelector.tsx - Mock文档选择器
3
- * 
4
- * 用于在Mock模式下快速选择测试文档
5
- * 
6
- * @module components
7
- */
8
-
9
-import React from 'react';
10
-import { Button, Space, Typography, Card } from 'antd';
11
-import { FileTextOutlined, ExperimentOutlined } from '@ant-design/icons';
12
-import { mockStorage } from '../services/mockData';
13
-import { isMockEnabled } from '../services/mockService';
14
-import { useUIStore } from '../stores/uiStore';
15
-
16
-const { Text } = Typography;
17
-
18
-/**
19
- * MockDocumentSelector - Mock文档选择器
20
- * 
21
- * 只在Mock模式下显示,提供快速打开测试文档的按钮
22
- */
23
-export const MockDocumentSelector: React.FC = () => {
24
-  const openDocumentPreview = useUIStore((state) => state.openDocumentPreview);
25
-
26
-  // 只在Mock模式下显示
27
-  if (!isMockEnabled()) {
28
-    return null;
29
-  }
30
-
31
-  const documents = mockStorage.getDocuments();
32
-
33
-  const handleOpenDocument = (documentId: string, title: string) => {
34
-    openDocumentPreview(documentId, title);
35
-  };
36
-
37
-  return (
38
-    <Card
39
-      style={{
40
-        position: 'fixed',
41
-        bottom: 20,
42
-        right: 20,
43
-        zIndex: 1000,
44
-        width: 350,
45
-        boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
46
-      }}
47
-      title={
48
-        <Space>
49
-          <ExperimentOutlined style={{ color: '#52c41a' }} />
50
-          <span>Mock测试模式</span>
51
-        </Space>
52
-      }
53
-      size="small"
54
-    >
55
-      <div style={{ marginBottom: 12 }}>
56
-        <Text type="secondary" style={{ fontSize: 12 }}>
57
-          当前为Mock模式,后端无需运行。点击下方按钮打开测试文档:
58
-        </Text>
59
-      </div>
60
-
61
-      <Space direction="vertical" style={{ width: '100%' }} size="small">
62
-        {documents.map((doc) => (
63
-          <Button
64
-            key={doc.id}
65
-            block
66
-            icon={<FileTextOutlined />}
67
-            onClick={() => handleOpenDocument(doc.id, doc.title)}
68
-            type="default"
69
-            size="small"
70
-          >
71
-            {doc.title}
72
-          </Button>
73
-        ))}
74
-      </Space>
75
-
76
-      <div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid #f0f0f0' }}>
77
-        <Text type="secondary" style={{ fontSize: 11 }}>
78
-          提示: 在 main.tsx 中注释掉 enableMockService() 即可切换到真实后端
79
-        </Text>
80
-      </div>
81
-    </Card>
82
-  );
83
-};
84
-
85
-export default MockDocumentSelector;

+ 0 - 5
src/main.tsx

@@ -2,11 +2,6 @@ import { StrictMode } from 'react';
2 2
 import { createRoot } from 'react-dom/client';
3 3
 import './index.css';
4 4
 import App from './App.tsx';
5
-import { enableMockService } from './services/mockService';
6
-
7
-// 启用Mock Service(后端不可用时使用)
8
-// 准备对接后端时,注释掉下面这行即可
9
-enableMockService();
10 5
 
11 6
 createRoot(document.getElementById('root')!).render(
12 7
   <StrictMode>

+ 108 - 77
src/services/blockService.ts

@@ -2,18 +2,25 @@
2 2
  * blockService.ts - Block API服务
3 3
  * 
4 4
  * 与后端blocks API交互的服务层
5
- * 支持Mock模式(用于前端独立开发)
5
+ * 
6
+ * 功能包括:
7
+ * - CRUD操作:获取、创建、更新、删除blocks
8
+ * - 高级功能:搜索、目录树、统计信息
6 9
  * 
7 10
  * @module services/blockService
8 11
  */
9 12
 
10 13
 import apiClient from './api';
11
-import { mockAPI } from './mockService';
12 14
 import type {
13 15
   DocumentBlock,
14 16
   GetBlocksResponse,
15
-  UpdateBlocksRequest,
17
+  GetBlockResponse,
18
+  UpdateBlockRequest,
19
+  UpdateBlockResponse,
16 20
   CreateBlockRequest,
21
+  SearchBlocksResponse,
22
+  GetTOCResponse,
23
+  BlockStatsResponse,
17 24
 } from '../types/editor';
18 25
 
19 26
 // ══════════════════════════════════════════════════════════════════════════════
@@ -43,21 +50,13 @@ export const blockService = {
43 50
    * ```
44 51
    */
45 52
   async getBlocks(documentId: string): Promise<GetBlocksResponse> {
46
-    // Mock模式
47
-    if (mockAPI.isEnabled()) {
48
-      return mockAPI.blocks.getBlocks(documentId);
49
-    }
50
-
51
-    // 真实API
52 53
     try {
53 54
       const response = await apiClient.get(`${BLOCKS_BASE_URL}/${documentId}/blocks`);
54 55
       
55
-      // 后端返回格式: { data: { documentId, title, blocks } }
56
+      // 后端返回格式: { code: 0, data: { blocks: [] } }
56 57
       const data = response.data?.data || response.data;
57 58
       
58 59
       return {
59
-        documentId: data.documentId || documentId,
60
-        title: data.title || '',
61 60
         blocks: data.blocks || [],
62 61
       };
63 62
     } catch (error: any) {
@@ -67,30 +66,31 @@ export const blockService = {
67 66
   },
68 67
 
69 68
   /**
70
-   * 批量更新文档的所有blocks
69
+   * 获取单个block
71 70
    * 
72 71
    * @param documentId 文档ID
73
-   * @param blocks 所有blocks数据
72
+   * @param blockId 块ID
73
+   * @returns Block数据
74 74
    * 
75 75
    * @example
76 76
    * ```ts
77
-   * await blockService.updateBlocks('doc-123', blocks);
77
+   * const data = await blockService.getBlock('doc-123', 'block-h1-0');
78
+   * console.log(data.block);
78 79
    * ```
79 80
    */
80
-  async updateBlocks(documentId: string, blocks: DocumentBlock[]): Promise<void> {
81
-    // Mock模式
82
-    if (mockAPI.isEnabled()) {
83
-      return mockAPI.blocks.updateBlocks(documentId, blocks);
84
-    }
85
-
86
-    // 真实API
81
+  async getBlock(documentId: string, blockId: string): Promise<GetBlockResponse> {
87 82
     try {
88
-      const requestData: UpdateBlocksRequest = { blocks };
83
+      const response = await apiClient.get(
84
+        `${BLOCKS_BASE_URL}/${documentId}/blocks/${blockId}`
85
+      );
89 86
       
90
-      await apiClient.put(`${BLOCKS_BASE_URL}/${documentId}/blocks`, requestData);
87
+      const data = response.data?.data || response.data;
88
+      return {
89
+        block: data.block,
90
+      };
91 91
     } catch (error: any) {
92
-      console.error('Failed to update blocks:', error);
93
-      throw new Error(error.response?.data?.message || '更新文档块失败');
92
+      console.error('Failed to get block:', error);
93
+      throw new Error(error.response?.data?.message || '获取块失败');
94 94
     }
95 95
   },
96 96
 
@@ -100,6 +100,7 @@ export const blockService = {
100 100
    * @param documentId 文档ID
101 101
    * @param blockId 块ID
102 102
    * @param updates 更新的字段
103
+   * @returns 更新响应
103 104
    * 
104 105
    * @example
105 106
    * ```ts
@@ -111,19 +112,19 @@ export const blockService = {
111 112
   async updateBlock(
112 113
     documentId: string,
113 114
     blockId: string,
114
-    updates: Partial<DocumentBlock>
115
-  ): Promise<void> {
116
-    // Mock模式
117
-    if (mockAPI.isEnabled()) {
118
-      return mockAPI.blocks.updateBlock(documentId, blockId, updates);
119
-    }
120
-
121
-    // 真实API
115
+    updates: UpdateBlockRequest
116
+  ): Promise<UpdateBlockResponse> {
122 117
     try {
123
-      await apiClient.patch(
118
+      const response = await apiClient.put(
124 119
         `${BLOCKS_BASE_URL}/${documentId}/blocks/${blockId}`,
125 120
         updates
126 121
       );
122
+      
123
+      const data = response.data?.data || response.data;
124
+      return {
125
+        blockId: data.blockId || blockId,
126
+        message: data.message || 'Block updated successfully',
127
+      };
127 128
     } catch (error: any) {
128 129
       console.error('Failed to update block:', error);
129 130
       throw new Error(error.response?.data?.message || '更新块失败');
@@ -141,7 +142,6 @@ export const blockService = {
141 142
    * ```ts
142 143
    * const newBlock = await blockService.createBlock('doc-123', {
143 144
    *   type: 'paragraph',
144
-   *   block_order: 100,
145 145
    *   level: 0,
146 146
    *   index: 0,
147 147
    *   content: '新段落',
@@ -153,12 +153,6 @@ export const blockService = {
153 153
     documentId: string,
154 154
     block: CreateBlockRequest
155 155
   ): Promise<DocumentBlock> {
156
-    // Mock模式
157
-    if (mockAPI.isEnabled()) {
158
-      return mockAPI.blocks.createBlock(documentId, block);
159
-    }
160
-
161
-    // 真实API
162 156
     try {
163 157
       const response = await apiClient.post(
164 158
         `${BLOCKS_BASE_URL}/${documentId}/blocks`,
@@ -184,12 +178,6 @@ export const blockService = {
184 178
    * ```
185 179
    */
186 180
   async deleteBlock(documentId: string, blockId: string): Promise<void> {
187
-    // Mock模式
188
-    if (mockAPI.isEnabled()) {
189
-      return mockAPI.blocks.deleteBlock(documentId, blockId);
190
-    }
191
-
192
-    // 真实API
193 181
     try {
194 182
       await apiClient.delete(`${BLOCKS_BASE_URL}/${documentId}/blocks/${blockId}`);
195 183
     } catch (error: any) {
@@ -199,58 +187,101 @@ export const blockService = {
199 187
   },
200 188
 
201 189
   /**
202
-   * 移动block(调整block_order)
190
+   * 搜索blocks
203 191
    * 
204 192
    * @param documentId 文档ID
205
-   * @param blockId 块ID
206
-   * @param newOrder 新的block_order
193
+   * @param query 搜索关键词
194
+   * @param type 可选的block类型筛选
195
+   * @returns 搜索结果
207 196
    * 
208 197
    * @example
209 198
    * ```ts
210
-   * await blockService.moveBlock('doc-123', 'block-h1-0', 150);
199
+   * const results = await blockService.searchBlocks('doc-123', '关键词');
200
+   * console.log(results.blocks, results.total);
211 201
    * ```
212 202
    */
213
-  async moveBlock(
203
+  async searchBlocks(
214 204
     documentId: string,
215
-    blockId: string,
216
-    newOrder: number
217
-  ): Promise<void> {
205
+    query: string,
206
+    type?: string
207
+  ): Promise<SearchBlocksResponse> {
208
+    try {
209
+      const params: Record<string, string> = { q: query };
210
+      if (type) {
211
+        params.type = type;
212
+      }
213
+
214
+      const response = await apiClient.get(
215
+        `${BLOCKS_BASE_URL}/${documentId}/blocks/search`,
216
+        { params }
217
+      );
218
+      
219
+      const data = response.data?.data || response.data;
220
+      return {
221
+        blocks: data.blocks || [],
222
+        total: data.total || 0,
223
+        query: data.query || query,
224
+      };
225
+    } catch (error: any) {
226
+      console.error('Failed to search blocks:', error);
227
+      throw new Error(error.response?.data?.message || '搜索块失败');
228
+    }
229
+  },
230
+
231
+  /**
232
+   * 获取文档目录树
233
+   * 
234
+   * @param documentId 文档ID
235
+   * @returns 目录树
236
+   * 
237
+   * @example
238
+   * ```ts
239
+   * const { toc } = await blockService.getTOC('doc-123');
240
+   * console.log(toc); // 树形结构的标题目录
241
+   * ```
242
+   */
243
+  async getTOC(documentId: string): Promise<GetTOCResponse> {
218 244
     try {
219
-      await apiClient.patch(`${BLOCKS_BASE_URL}/${documentId}/blocks/${blockId}`, {
220
-        block_order: newOrder,
221
-      });
245
+      const response = await apiClient.get(
246
+        `${BLOCKS_BASE_URL}/${documentId}/blocks/toc`
247
+      );
248
+      
249
+      const data = response.data?.data || response.data;
250
+      return {
251
+        toc: data.toc || [],
252
+      };
222 253
     } catch (error: any) {
223
-      console.error('Failed to move block:', error);
224
-      throw new Error(error.response?.data?.message || '移动块失败');
254
+      console.error('Failed to get TOC:', error);
255
+      throw new Error(error.response?.data?.message || '获取目录树失败');
225 256
     }
226 257
   },
227 258
 
228 259
   /**
229
-   * 重新排序所有blocks(批量更新block_order)
260
+   * 获取block统计信息
230 261
    * 
231 262
    * @param documentId 文档ID
232
-   * @param blockOrders block ID到新order的映射
263
+   * @returns 统计信息
233 264
    * 
234 265
    * @example
235 266
    * ```ts
236
-   * await blockService.reorderBlocks('doc-123', {
237
-   *   'block-h1-0': 0,
238
-   *   'block-p-1': 100,
239
-   *   'block-p-2': 200,
240
-   * });
267
+   * const stats = await blockService.getStats('doc-123');
268
+   * console.log(stats.total, stats.by_type);
241 269
    * ```
242 270
    */
243
-  async reorderBlocks(
244
-    documentId: string,
245
-    blockOrders: Record<string, number>
246
-  ): Promise<void> {
271
+  async getStats(documentId: string): Promise<BlockStatsResponse> {
247 272
     try {
248
-      await apiClient.post(`${BLOCKS_BASE_URL}/${documentId}/blocks/reorder`, {
249
-        blockOrders,
250
-      });
273
+      const response = await apiClient.get(
274
+        `${BLOCKS_BASE_URL}/${documentId}/blocks/stats`
275
+      );
276
+      
277
+      const data = response.data?.data || response.data;
278
+      return {
279
+        total: data.total || 0,
280
+        by_type: data.by_type || data.byType || {},
281
+      };
251 282
     } catch (error: any) {
252
-      console.error('Failed to reorder blocks:', error);
253
-      throw new Error(error.response?.data?.message || '重新排序块失败');
283
+      console.error('Failed to get stats:', error);
284
+      throw new Error(error.response?.data?.message || '获取统计信息失败');
254 285
     }
255 286
   },
256 287
 };

+ 22 - 70
src/services/documentService.ts

@@ -2,13 +2,15 @@
2 2
  * Document Service Module
3 3
  *
4 4
  * Provides methods for document management operations:
5
- * - createDocument: Parse Word document and create Markdown document
6
- * - getDocument: Get document details by ID
7
- * - updateDocument: Update document content (full or partial)
5
+ * - createDocument: Parse Word document and create document with blocks stored in SQLite
6
+ * - getDocument: Get document details by ID (optionally with blocks)
8 7
  * - deleteDocuments: Delete all documents for a session
9 8
  * - listDocuments: Get paginated list of documents
10 9
  *
11 10
  * All methods use the configured API client and provide error normalization.
11
+ * 
12
+ * Note: Document content is stored as blocks in SQLite (via contentDbPath).
13
+ * Use blockService to read/update document content.
12 14
  *
13 15
  * @module services/documentService
14 16
  */
@@ -19,20 +21,19 @@ import type {
19 21
   CreateDocumentRequest,
20 22
   CreateDocumentResponse,
21 23
   Document,
22
-  UpdateDocumentRequest,
23
-  UpdateDocumentResponse,
24 24
   ListDocumentsResponse,
25 25
   DocumentListFilters,
26
+  GetDocumentOptions,
26 27
 } from '../types/document';
27 28
 
28 29
 /**
29 30
  * Create a new document from a Word file URL
30 31
  *
31
- * Sends a Word document URL to the backend for parsing into Markdown format.
32
- * The backend downloads the file, parses it, and stores the Markdown content.
32
+ * Sends a Word document URL to the backend for parsing into blocks.
33
+ * The backend downloads the file, parses it, and stores blocks in SQLite.
33 34
  *
34 35
  * @param request - Create document request payload
35
- * @returns Created document metadata
36
+ * @returns Created document metadata including contentDbPath
36 37
  * @throws {Error} When the API request fails or file cannot be parsed
37 38
  */
38 39
 export const createDocument = async (
@@ -61,80 +62,33 @@ export const createDocument = async (
61 62
 /**
62 63
  * Get document details by ID
63 64
  *
64
- * Retrieves the full document including its Markdown content.
65
+ * Retrieves document metadata. Optionally includes blocks array.
65 66
  *
66 67
  * @param documentId - Document ID to retrieve
67
- * @returns Document entity with full content
68
+ * @param options - Options including includeBlocks flag
69
+ * @returns Document entity (with blocks if requested)
68 70
  * @throws {Error} When the document is not found or request fails
69 71
  */
70
-export const getDocument = async (documentId: string): Promise<Document> => {
72
+export const getDocument = async (
73
+  documentId: string,
74
+  options: GetDocumentOptions = {}
75
+): Promise<Document> => {
71 76
   try {
72
-    const response = await apiClient.get<ApiResponse<Document>>(
73
-      `/api/v1/documents/${documentId}`
74
-    );
75
-    return response.data.data;
76
-  } catch (error: any) {
77
-    let friendlyMessage = '获取文档失败';
78
-
79
-    if (error?.response?.status === 404) {
80
-      friendlyMessage = '文档不存在';
77
+    const params: Record<string, any> = {};
78
+    if (options.includeBlocks) {
79
+      params.includeBlocks = true;
81 80
     }
82 81
 
83
-    const message = getErrorMessage(error);
84
-    throw new Error(`${friendlyMessage}: ${message}`, { cause: error });
85
-  }
86
-};
87
-
88
-/**
89
- * Get document content by ID (convenience method)
90
- *
91
- * This is an alias for getDocument() that retrieves the full document
92
- * including its title and Markdown content. Useful for preview/editing scenarios.
93
- *
94
- * @param documentId - Document ID to retrieve
95
- * @returns Document with title and content
96
- * @throws {Error} When the document is not found or request fails
97
- */
98
-export const getDocumentContent = async (
99
-  documentId: string
100
-): Promise<{ title: string; content: string; id: string }> => {
101
-  const document = await getDocument(documentId);
102
-  return {
103
-    id: document.id,
104
-    title: document.title,
105
-    content: document.content,
106
-  };
107
-};
108
-
109
-/**
110
- * Update document content
111
- *
112
- * Supports two update modes:
113
- * - Full update: Pass `content` to replace entire document
114
- * - Partial update: Pass `blocks` to update specific sections by heading level
115
- *
116
- * @param documentId - Document ID to update
117
- * @param request - Update request (content OR blocks, not both)
118
- * @returns Update response with new timestamp
119
- * @throws {Error} When document not found or validation fails
120
- */
121
-export const updateDocument = async (
122
-  documentId: string,
123
-  request: UpdateDocumentRequest
124
-): Promise<UpdateDocumentResponse> => {
125
-  try {
126
-    const response = await apiClient.put<ApiResponse<UpdateDocumentResponse>>(
82
+    const response = await apiClient.get<ApiResponse<Document>>(
127 83
       `/api/v1/documents/${documentId}`,
128
-      request
84
+      { params }
129 85
     );
130 86
     return response.data.data;
131 87
   } catch (error: any) {
132
-    let friendlyMessage = '更新文档失败';
88
+    let friendlyMessage = '获取文档失败';
133 89
 
134 90
     if (error?.response?.status === 404) {
135 91
       friendlyMessage = '文档不存在';
136
-    } else if (error?.response?.status === 422) {
137
-      friendlyMessage = '更新数据验证失败(content和blocks不能同时传入)';
138 92
     }
139 93
 
140 94
     const message = getErrorMessage(error);
@@ -200,8 +154,6 @@ export const listDocuments = async (
200 154
 export const documentService = {
201 155
   create: createDocument,
202 156
   get: getDocument,
203
-  getContent: getDocumentContent,
204
-  update: updateDocument,
205 157
   delete: deleteDocuments,
206 158
   list: listDocuments,
207 159
 };

+ 9 - 1
src/services/index.ts

@@ -11,11 +11,19 @@ export {
11 11
   createDocument,
12 12
   getDocument,
13 13
   listDocuments,
14
-  updateDocument,
15 14
   deleteDocuments,
16 15
   documentService,
17 16
 } from './documentService';
18 17
 
19 18
 export { exportToWord, exportService } from './exportService';
20 19
 
20
+export {
21
+  listExportRecords,
22
+  downloadExportRecord,
23
+  deleteExportRecord,
24
+  getAdminStorage,
25
+  exportRecordService,
26
+} from './exportRecordService';
27
+
21 28
 export { blockService } from './blockService';
29
+

+ 0 - 384
src/services/mockData.ts

@@ -1,384 +0,0 @@
1
-/**
2
- * mockData.ts - Mock数据存储
3
- * 
4
- * 提供测试用的示例文档和块数据
5
- * 
6
- * @module services/mockData
7
- */
8
-
9
-import type { DocumentBlock, HeadingBlock, ParagraphBlock, TableBlock } from '../types/editor';
10
-
11
-// ══════════════════════════════════════════════════════════════════════════════
12
-// Mock文档数据
13
-// ══════════════════════════════════════════════════════════════════════════════
14
-
15
-interface MockDocument {
16
-  id: string;
17
-  title: string;
18
-  createdAt: string;
19
-  updatedAt: string;
20
-}
21
-
22
-const mockDocuments: MockDocument[] = [
23
-  {
24
-    id: 'doc-demo-1',
25
-    title: '示例文档 - 完整功能演示',
26
-    createdAt: '2026-01-01T00:00:00Z',
27
-    updatedAt: '2026-01-01T00:00:00Z',
28
-  },
29
-  {
30
-    id: 'doc-demo-2',
31
-    title: '空白文档',
32
-    createdAt: '2026-01-02T00:00:00Z',
33
-    updatedAt: '2026-01-02T00:00:00Z',
34
-  },
35
-];
36
-
37
-// ══════════════════════════════════════════════════════════════════════════════
38
-// Mock Block数据
39
-// ══════════════════════════════════════════════════════════════════════════════
40
-
41
-/**
42
- * 示例文档的blocks
43
- */
44
-const demoBlocks: DocumentBlock[] = [
45
-  // H1 标题
46
-  {
47
-    id: 'block-h1-1',
48
-    type: 'heading',
49
-    level: 1,
50
-    index: 0,
51
-    block_order: 0,
52
-    word_style: 'Heading1',
53
-    content: '第一章 文档编辑器功能演示',
54
-    style: {},
55
-    metadata: {
56
-      parent_id: null,
57
-    },
58
-  } as HeadingBlock,
59
-
60
-  // 段落
61
-  {
62
-    id: 'block-p-1',
63
-    type: 'paragraph',
64
-    level: 0,
65
-    index: 0,
66
-    block_order: 100,
67
-    word_style: 'Normal',
68
-    content: '这是一个功能完整的文档编辑器,支持标题、段落、表格、图片等多种块类型。',
69
-    style: {},
70
-    metadata: {
71
-      parent_heading_id: 'block-h1-1',
72
-    },
73
-  } as ParagraphBlock,
74
-
75
-  // H2 标题
76
-  {
77
-    id: 'block-h2-1',
78
-    type: 'heading',
79
-    level: 2,
80
-    index: 0,
81
-    block_order: 200,
82
-    word_style: 'Heading2',
83
-    content: '1.1 富文本编辑',
84
-    style: {},
85
-    metadata: {
86
-      parent_id: 'block-h1-1',
87
-    },
88
-  } as HeadingBlock,
89
-
90
-  // 段落 - 富文本
91
-  {
92
-    id: 'block-p-2',
93
-    type: 'paragraph',
94
-    level: 0,
95
-    index: 0,
96
-    block_order: 300,
97
-    word_style: 'Normal',
98
-    content: [
99
-      { text: '支持 ', style: {} },
100
-      { text: '加粗', style: { bold: true } },
101
-      { text: '、', style: {} },
102
-      { text: '斜体', style: { italic: true } },
103
-      { text: '、', style: {} },
104
-      { text: '下划线', style: { underline: true } },
105
-      { text: ' 等格式。', style: {} },
106
-    ],
107
-    style: {},
108
-    metadata: {
109
-      parent_heading_id: 'block-h2-1',
110
-    },
111
-  } as ParagraphBlock,
112
-
113
-  // H2 标题
114
-  {
115
-    id: 'block-h2-2',
116
-    type: 'heading',
117
-    level: 2,
118
-    index: 1,
119
-    block_order: 400,
120
-    word_style: 'Heading2',
121
-    content: '1.2 表格功能',
122
-    style: {},
123
-    metadata: {
124
-      parent_id: 'block-h1-1',
125
-    },
126
-  } as HeadingBlock,
127
-
128
-  // 段落
129
-  {
130
-    id: 'block-p-3',
131
-    type: 'paragraph',
132
-    level: 0,
133
-    index: 0,
134
-    block_order: 500,
135
-    word_style: 'Normal',
136
-    content: '表格支持插入、删除行列,以及单元格合并与拆分:',
137
-    style: {},
138
-    metadata: {
139
-      parent_heading_id: 'block-h2-2',
140
-    },
141
-  } as ParagraphBlock,
142
-
143
-  // 表格
144
-  {
145
-    id: 'block-tbl-1',
146
-    type: 'table',
147
-    level: 0,
148
-    index: 0,
149
-    block_order: 600,
150
-    word_style: 'TableNormal',
151
-    content: {
152
-      rows: [
153
-        {
154
-          cells: [
155
-            { text: '功能', rowspan: 1, colspan: 1, style: { bold: true } },
156
-            { text: '说明', rowspan: 1, colspan: 1, style: { bold: true } },
157
-            { text: '快捷键', rowspan: 1, colspan: 1, style: { bold: true } },
158
-          ],
159
-        },
160
-        {
161
-          cells: [
162
-            { text: '插入行', rowspan: 1, colspan: 1, style: {} },
163
-            { text: '在当前行下方插入新行', rowspan: 1, colspan: 1, style: {} },
164
-            { text: '-', rowspan: 1, colspan: 1, style: {} },
165
-          ],
166
-        },
167
-        {
168
-          cells: [
169
-            { text: '合并单元格', rowspan: 1, colspan: 1, style: {} },
170
-            { text: 'Shift+点击选择范围后合并', rowspan: 1, colspan: 1, style: {} },
171
-            { text: 'Shift+Click', rowspan: 1, colspan: 1, style: {} },
172
-          ],
173
-        },
174
-      ],
175
-    },
176
-    style: {},
177
-    metadata: {
178
-      cols: 3,
179
-      rows: 3,
180
-      table_width: 100,
181
-      table_width_unit: 'percent',
182
-      col_widths: [25, 50, 25],
183
-      parent_heading_id: 'block-h2-2',
184
-    },
185
-  } as TableBlock,
186
-
187
-  // H1 标题
188
-  {
189
-    id: 'block-h1-2',
190
-    type: 'heading',
191
-    level: 1,
192
-    index: 1,
193
-    block_order: 700,
194
-    word_style: 'Heading1',
195
-    content: '第二章 文档大纲',
196
-    style: {},
197
-    metadata: {
198
-      parent_id: null,
199
-    },
200
-  } as HeadingBlock,
201
-
202
-  // 段落
203
-  {
204
-    id: 'block-p-4',
205
-    type: 'paragraph',
206
-    level: 0,
207
-    index: 0,
208
-    block_order: 800,
209
-    word_style: 'Normal',
210
-    content: '右侧显示文档大纲,可以快速导航到各个章节。点击大纲节点会自动滚动到对应位置。',
211
-    style: {},
212
-    metadata: {
213
-      parent_heading_id: 'block-h1-2',
214
-    },
215
-  } as ParagraphBlock,
216
-
217
-  // H2 标题
218
-  {
219
-    id: 'block-h2-3',
220
-    type: 'heading',
221
-    level: 2,
222
-    index: 0,
223
-    block_order: 900,
224
-    word_style: 'Heading2',
225
-    content: '2.1 大纲功能',
226
-    style: {},
227
-    metadata: {
228
-      parent_id: 'block-h1-2',
229
-    },
230
-  } as HeadingBlock,
231
-
232
-  // 段落
233
-  {
234
-    id: 'block-p-5',
235
-    type: 'paragraph',
236
-    level: 0,
237
-    index: 0,
238
-    block_order: 1000,
239
-    word_style: 'Normal',
240
-    content: '• 展开/折叠子标题\n• 点击跳转定位\n• 高亮当前位置',
241
-    style: {},
242
-    metadata: {
243
-      parent_heading_id: 'block-h2-3',
244
-    },
245
-  } as ParagraphBlock,
246
-
247
-  // H3 标题
248
-  {
249
-    id: 'block-h3-1',
250
-    type: 'heading',
251
-    level: 3,
252
-    index: 0,
253
-    block_order: 1100,
254
-    word_style: 'Heading3',
255
-    content: '2.1.1 树形结构',
256
-    style: {},
257
-    metadata: {
258
-      parent_id: 'block-h2-3',
259
-    },
260
-  } as HeadingBlock,
261
-
262
-  // 段落
263
-  {
264
-    id: 'block-p-6',
265
-    type: 'paragraph',
266
-    level: 0,
267
-    index: 0,
268
-    block_order: 1200,
269
-    word_style: 'Normal',
270
-    content: '大纲自动根据标题层级构建树形结构,支持多级嵌套。',
271
-    style: {},
272
-    metadata: {
273
-      parent_heading_id: 'block-h3-1',
274
-    },
275
-  } as ParagraphBlock,
276
-];
277
-
278
-/**
279
- * 空白文档的blocks
280
- */
281
-const emptyBlocks: DocumentBlock[] = [];
282
-
283
-// ══════════════════════════════════════════════════════════════════════════════
284
-// Mock数据存储管理
285
-// ══════════════════════════════════════════════════════════════════════════════
286
-
287
-class MockStorage {
288
-  private documents: Map<string, MockDocument>;
289
-  private blocks: Map<string, DocumentBlock[]>;
290
-
291
-  constructor() {
292
-    this.documents = new Map();
293
-    this.blocks = new Map();
294
-    this.initialize();
295
-  }
296
-
297
-  /**
298
-   * 初始化Mock数据
299
-   */
300
-  private initialize() {
301
-    // 添加文档
302
-    mockDocuments.forEach((doc) => {
303
-      this.documents.set(doc.id, doc);
304
-    });
305
-
306
-    // 添加blocks
307
-    this.blocks.set('doc-demo-1', demoBlocks);
308
-    this.blocks.set('doc-demo-2', emptyBlocks);
309
-  }
310
-
311
-  /**
312
-   * 获取所有文档
313
-   */
314
-  getDocuments(): MockDocument[] {
315
-    return Array.from(this.documents.values());
316
-  }
317
-
318
-  /**
319
-   * 获取单个文档
320
-   */
321
-  getDocument(id: string): MockDocument | undefined {
322
-    return this.documents.get(id);
323
-  }
324
-
325
-  /**
326
-   * 创建文档
327
-   */
328
-  createDocument(title: string): MockDocument {
329
-    const id = `doc-${Date.now()}`;
330
-    const doc: MockDocument = {
331
-      id,
332
-      title,
333
-      createdAt: new Date().toISOString(),
334
-      updatedAt: new Date().toISOString(),
335
-    };
336
-
337
-    this.documents.set(id, doc);
338
-    this.blocks.set(id, []);
339
-
340
-    return doc;
341
-  }
342
-
343
-  /**
344
-   * 删除文档
345
-   */
346
-  deleteDocument(id: string): boolean {
347
-    const deleted = this.documents.delete(id);
348
-    if (deleted) {
349
-      this.blocks.delete(id);
350
-    }
351
-    return deleted;
352
-  }
353
-
354
-  /**
355
-   * 获取文档的blocks
356
-   */
357
-  getBlocks(documentId: string): DocumentBlock[] {
358
-    return this.blocks.get(documentId) || [];
359
-  }
360
-
361
-  /**
362
-   * 更新文档的blocks
363
-   */
364
-  updateBlocks(documentId: string, blocks: DocumentBlock[]): void {
365
-    this.blocks.set(documentId, blocks);
366
-  }
367
-
368
-  /**
369
-   * 重置所有数据
370
-   */
371
-  reset(): void {
372
-    this.documents.clear();
373
-    this.blocks.clear();
374
-    this.initialize();
375
-  }
376
-}
377
-
378
-// ══════════════════════════════════════════════════════════════════════════════
379
-// 导出
380
-// ══════════════════════════════════════════════════════════════════════════════
381
-
382
-export const mockStorage = new MockStorage();
383
-
384
-export default mockStorage;

+ 0 - 285
src/services/mockService.ts

@@ -1,285 +0,0 @@
1
-/**
2
- * mockService.ts - Mock API服务
3
- * 
4
- * 模拟后端API响应,用于前端独立开发测试
5
- * 
6
- * 使用方法:
7
- * 1. 在main.tsx中导入并启用: `enableMockService()`
8
- * 2. 正常使用API,会自动被拦截并返回Mock数据
9
- * 3. 准备对接后端时,移除enableMockService()调用
10
- * 
11
- * @module services/mockService
12
- */
13
-
14
-import { mockStorage } from './mockData';
15
-import type { DocumentBlock, GetBlocksResponse } from '../types/editor';
16
-
17
-// ══════════════════════════════════════════════════════════════════════════════
18
-// Mock延迟(模拟网络请求)
19
-// ══════════════════════════════════════════════════════════════════════════════
20
-
21
-const MOCK_DELAY = 300; // 毫秒
22
-
23
-function delay(ms: number = MOCK_DELAY): Promise<void> {
24
-  return new Promise((resolve) => setTimeout(resolve, ms));
25
-}
26
-
27
-// ══════════════════════════════════════════════════════════════════════════════
28
-// Mock API实现
29
-// ══════════════════════════════════════════════════════════════════════════════
30
-
31
-/**
32
- * Mock Blocks API
33
- */
34
-export const mockBlocksAPI = {
35
-  /**
36
-   * 获取文档blocks
37
-   */
38
-  async getBlocks(documentId: string): Promise<GetBlocksResponse> {
39
-    await delay();
40
-    
41
-    const doc = mockStorage.getDocument(documentId);
42
-    const blocks = mockStorage.getBlocks(documentId);
43
-    
44
-    if (!doc) {
45
-      throw new Error(`文档不存在: ${documentId}`);
46
-    }
47
-    
48
-    console.log(`[Mock API] GET /api/v1/documents/${documentId}/blocks`, {
49
-      documentId,
50
-      title: doc.title,
51
-      blocksCount: blocks.length,
52
-    });
53
-    
54
-    return {
55
-      documentId: doc.id,
56
-      title: doc.title,
57
-      blocks,
58
-    };
59
-  },
60
-
61
-  /**
62
-   * 更新文档blocks
63
-   */
64
-  async updateBlocks(documentId: string, blocks: DocumentBlock[]): Promise<void> {
65
-    await delay();
66
-    
67
-    const doc = mockStorage.getDocument(documentId);
68
-    if (!doc) {
69
-      throw new Error(`文档不存在: ${documentId}`);
70
-    }
71
-    
72
-    mockStorage.updateBlocks(documentId, blocks);
73
-    
74
-    console.log(`[Mock API] PUT /api/v1/documents/${documentId}/blocks`, {
75
-      documentId,
76
-      blocksCount: blocks.length,
77
-      success: true,
78
-    });
79
-  },
80
-
81
-  /**
82
-   * 更新单个block
83
-   */
84
-  async updateBlock(
85
-    documentId: string,
86
-    blockId: string,
87
-    updates: Partial<DocumentBlock>
88
-  ): Promise<void> {
89
-    await delay();
90
-    
91
-    const blocks = mockStorage.getBlocks(documentId);
92
-    const index = blocks.findIndex((b: DocumentBlock) => b.id === blockId);
93
-    
94
-    if (index === -1) {
95
-      throw new Error(`Block不存在: ${blockId}`);
96
-    }
97
-    
98
-    // 使用类型断言来合并更新,因为Partial<DocumentBlock>与联合类型兼容
99
-    blocks[index] = { ...blocks[index], ...updates } as DocumentBlock;
100
-    mockStorage.updateBlocks(documentId, blocks);
101
-    
102
-    console.log(`[Mock API] PATCH /api/v1/documents/${documentId}/blocks/${blockId}`, {
103
-      blockId,
104
-      updates,
105
-      success: true,
106
-    });
107
-  },
108
-
109
-  /**
110
-   * 创建block
111
-   */
112
-  async createBlock(documentId: string, block: any): Promise<DocumentBlock> {
113
-    await delay();
114
-    
115
-    const blocks = mockStorage.getBlocks(documentId);
116
-    const newBlock = {
117
-      id: `block-${Date.now()}`,
118
-      ...block,
119
-    } as DocumentBlock;
120
-    
121
-    blocks.push(newBlock);
122
-    mockStorage.updateBlocks(documentId, blocks);
123
-    
124
-    console.log(`[Mock API] POST /api/v1/documents/${documentId}/blocks`, {
125
-      documentId,
126
-      blockId: newBlock.id,
127
-      success: true,
128
-    });
129
-    
130
-    return newBlock;
131
-  },
132
-
133
-  /**
134
-   * 删除block
135
-   */
136
-  async deleteBlock(documentId: string, blockId: string): Promise<void> {
137
-    await delay();
138
-    
139
-    const blocks = mockStorage.getBlocks(documentId);
140
-    const filtered = blocks.filter((b: DocumentBlock) => b.id !== blockId);
141
-    
142
-    mockStorage.updateBlocks(documentId, filtered);
143
-    
144
-    console.log(`[Mock API] DELETE /api/v1/documents/${documentId}/blocks/${blockId}`, {
145
-      blockId,
146
-      success: true,
147
-    });
148
-  },
149
-};
150
-
151
-/**
152
- * Mock Documents API
153
- */
154
-export const mockDocumentsAPI = {
155
-  /**
156
-   * 获取文档列表
157
-   */
158
-  async listDocuments(): Promise<any[]> {
159
-    await delay();
160
-    
161
-    const docs = mockStorage.getDocuments();
162
-    
163
-    console.log(`[Mock API] GET /api/v1/documents`, {
164
-      count: docs.length,
165
-    });
166
-    
167
-    return docs;
168
-  },
169
-
170
-  /**
171
-   * 获取单个文档
172
-   */
173
-  async getDocument(id: string): Promise<any> {
174
-    await delay();
175
-    
176
-    const doc = mockStorage.getDocument(id);
177
-    
178
-    if (!doc) {
179
-      throw new Error(`文档不存在: ${id}`);
180
-    }
181
-    
182
-    console.log(`[Mock API] GET /api/v1/documents/${id}`, doc);
183
-    
184
-    return doc;
185
-  },
186
-
187
-  /**
188
-   * 创建文档
189
-   */
190
-  async createDocument(title: string): Promise<any> {
191
-    await delay();
192
-    
193
-    const doc = mockStorage.createDocument(title);
194
-    
195
-    console.log(`[Mock API] POST /api/v1/documents`, doc);
196
-    
197
-    return doc;
198
-  },
199
-
200
-  /**
201
-   * 删除文档
202
-   */
203
-  async deleteDocument(id: string): Promise<void> {
204
-    await delay();
205
-    
206
-    const success = mockStorage.deleteDocument(id);
207
-    
208
-    if (!success) {
209
-      throw new Error(`删除失败: ${id}`);
210
-    }
211
-    
212
-    console.log(`[Mock API] DELETE /api/v1/documents/${id}`, {
213
-      success: true,
214
-    });
215
-  },
216
-};
217
-
218
-// ══════════════════════════════════════════════════════════════════════════════
219
-// Mock Service启用/禁用
220
-// ══════════════════════════════════════════════════════════════════════════════
221
-
222
-let mockEnabled = false;
223
-
224
-/**
225
- * 启用Mock Service
226
- * 
227
- * 在main.tsx中调用此方法来启用Mock模式
228
- * 
229
- * @example
230
- * ```ts
231
- * import { enableMockService } from './services/mockService';
232
- * 
233
- * if (import.meta.env.DEV) {
234
- *   enableMockService();
235
- * }
236
- * ```
237
- */
238
-export function enableMockService() {
239
-  mockEnabled = true;
240
-  console.log(
241
-    '%c[Mock Service] 已启用',
242
-    'color: #52c41a; font-weight: bold; font-size: 14px;'
243
-  );
244
-  console.log(
245
-    '%c所有API请求将返回Mock数据,后端无需运行',
246
-    'color: #1890ff; font-size: 12px;'
247
-  );
248
-  console.log(
249
-    '%c可用的测试文档:',
250
-    'color: #722ed1; font-weight: bold; font-size: 12px;'
251
-  );
252
-  mockStorage.getDocuments().forEach((doc: any, index: number) => {
253
-    console.log(`  ${index + 1}. ${doc.title} (${doc.id})`);
254
-  });
255
-}
256
-
257
-/**
258
- * 禁用Mock Service
259
- */
260
-export function disableMockService() {
261
-  mockEnabled = false;
262
-  console.log(
263
-    '%c[Mock Service] 已禁用',
264
-    'color: #ff4d4f; font-weight: bold; font-size: 14px;'
265
-  );
266
-}
267
-
268
-/**
269
- * 检查Mock是否启用
270
- */
271
-export function isMockEnabled(): boolean {
272
-  return mockEnabled;
273
-}
274
-
275
-// ══════════════════════════════════════════════════════════════════════════════
276
-// 导出Mock API(供blockService使用)
277
-// ══════════════════════════════════════════════════════════════════════════════
278
-
279
-export const mockAPI = {
280
-  blocks: mockBlocksAPI,
281
-  documents: mockDocumentsAPI,
282
-  isEnabled: isMockEnabled,
283
-};
284
-
285
-export default mockAPI;

+ 27 - 104
src/stores/documentStore.ts

@@ -13,8 +13,8 @@
13 13
 
14 14
 import { create } from 'zustand';
15 15
 import type { DocumentStoreState } from '../types/store';
16
-import type { Document, DocumentListFilters } from '../types/document';
17
-import type { CreateDocumentRequest, UpdateDocumentRequest, ListFilters } from '../types/api';
16
+import type { Document, DocumentListFilters, CreateDocumentRequest } from '../types/document';
17
+import type { ListFilters } from '../types/api';
18 18
 import * as documentService from '../services/documentService';
19 19
 
20 20
 /**
@@ -69,23 +69,15 @@ export const useDocumentStore = create<DocumentStoreState>((set, get) => ({
69 69
    * Creates a document via the API and sets it as the current document.
70 70
    * Save status is set to 'saved' after successful creation.
71 71
    *
72
-   * @param req - Document creation request
72
+   * @param req - Document creation request (userId, fileUrl, sessionId)
73 73
    * @throws {Error} When document creation fails
74 74
    */
75 75
   createDocument: async (req: CreateDocumentRequest) => {
76 76
     try {
77 77
       set({ saveStatus: 'saving' });
78 78
 
79
-      // 转换API请求格式到documentService需要的格式
80
-      const serviceReq: import('../types/document').CreateDocumentRequest = {
81
-        userId: 'default-user', // TODO: 从认证上下文获取
82
-        fileUrl: '', // API类型中不需要此字段,使用空字符串
83
-        sessionId: req.sessionId || 'default-session',
84
-        title: req.title,
85
-      };
86
-
87 79
       // Call API to create document
88
-      const response = await documentService.createDocument(serviceReq);
80
+      const response = await documentService.createDocument(req);
89 81
 
90 82
       // Fetch the full document to set as current
91 83
       const fullDocument = await documentService.getDocument(response.documentId);
@@ -108,13 +100,15 @@ export const useDocumentStore = create<DocumentStoreState>((set, get) => ({
108 100
    * Fetch a document by ID
109 101
    *
110 102
    * Retrieves a document from the API and sets it as the current document.
103
+   * Optionally includes blocks array.
111 104
    *
112 105
    * @param id - Document ID to fetch
106
+   * @param includeBlocks - Whether to include blocks in response
113 107
    * @throws {Error} When document fetch fails
114 108
    */
115
-  fetchDocument: async (id: string) => {
109
+  fetchDocument: async (id: string, includeBlocks: boolean = false) => {
116 110
     try {
117
-      const document = await documentService.getDocument(id);
111
+      const document = await documentService.getDocument(id, { includeBlocks });
118 112
 
119 113
       set({
120 114
         currentDocument: document,
@@ -137,12 +131,14 @@ export const useDocumentStore = create<DocumentStoreState>((set, get) => ({
137 131
    */
138 132
   fetchDocumentList: async (filters?: ListFilters) => {
139 133
     try {
140
-      // 提供默认的filters以满足API要求
134
+      // 转换前端的camelCase字段名为后端的snake_case
141 135
       const apiFilters: DocumentListFilters = {
142 136
         userId: 'default-user', // TODO: 从认证上下文获取
143 137
         page: filters?.page,
144 138
         pageSize: filters?.pageSize,
145
-        sortBy: filters?.sortBy,
139
+        sortBy: filters?.sortBy === 'createdAt' ? 'created_at' : 
140
+                filters?.sortBy === 'updatedAt' ? 'updated_at' : 
141
+                undefined,
146 142
         sortOrder: filters?.sortOrder,
147 143
       };
148 144
       
@@ -159,54 +155,20 @@ export const useDocumentStore = create<DocumentStoreState>((set, get) => ({
159 155
   },
160 156
 
161 157
   /**
162
-   * Update an existing document
163
-   *
164
-   * Updates a document via the API. If the updated document is the current document,
165
-   * refreshes the current document state.
166
-   *
167
-   * @param id - Document ID to update
168
-   * @param req - Update request (partial fields supported)
169
-   * @throws {Error} When document update fails
170
-   */
171
-  updateDocument: async (id: string, req: UpdateDocumentRequest) => {
172
-    try {
173
-      set({ saveStatus: 'saving' });
174
-
175
-      await documentService.updateDocument(id, req);
176
-
177
-      // If we updated the current document, refresh it
178
-      const currentDoc = get().currentDocument;
179
-      if (currentDoc && currentDoc.id === id) {
180
-        await get().fetchDocument(id);
181
-      }
182
-
183
-      set({ saveStatus: 'saved' });
184
-
185
-      // Refresh document list to reflect changes
186
-      await get().fetchDocumentList();
187
-    } catch (error) {
188
-      set({ saveStatus: 'error' });
189
-      throw error;
190
-    }
191
-  },
192
-
193
-  /**
194
-   * Delete a document by ID
158
+   * Delete documents by session ID
195 159
    *
196
-   * Deletes a document via the API. If the deleted document is the current document,
197
-   * clears the current document state.
160
+   * Deletes all documents for a session via the API.
161
+   * If any deleted document is the current document, clears the current document state.
198 162
    *
199
-   * @param id - Document ID to delete
163
+   * @param sessionId - Session ID whose documents to delete
200 164
    * @throws {Error} When document deletion fails
201 165
    */
202
-  deleteDocument: async (id: string) => {
203
-    // 注意: documentService.deleteDocuments 接收 sessionId 参数
204
-    // 这里使用文档ID作为sessionId (需要根据实际API调整)
205
-    await documentService.deleteDocuments(id);
166
+  deleteDocumentsBySession: async (sessionId: string) => {
167
+    await documentService.deleteDocuments(sessionId);
206 168
 
207
-    // If we deleted the current document, clear it
169
+    // Clear current document if it belonged to this session
208 170
     const currentDoc = get().currentDocument;
209
-    if (currentDoc && currentDoc.id === id) {
171
+    if (currentDoc && currentDoc.sessionId === sessionId) {
210 172
       set({ currentDocument: null, saveStatus: 'saved' });
211 173
     }
212 174
 
@@ -215,56 +177,17 @@ export const useDocumentStore = create<DocumentStoreState>((set, get) => ({
215 177
   },
216 178
 
217 179
   /**
218
-   * Update current document content (local state only)
219
-   *
220
-   * Updates the content of the current document in local state only.
221
-   * Does NOT persist to the backend. Sets save status to 'unsaved'.
222
-   *
223
-   * This is typically used for real-time editing, with auto-save
224
-   * handling the actual persistence.
225
-   *
226
-   * @param content - New content string
227
-   */
228
-  updateContent: (content: string) => {
229
-    const currentDoc = get().currentDocument;
230
-    if (!currentDoc) return;
231
-
232
-    const updatedDoc: Document = {
233
-      ...currentDoc,
234
-      content,
235
-      updatedAt: Date.now(),
236
-    };
237
-
238
-    set({
239
-      currentDocument: updatedDoc,
240
-      saveStatus: 'unsaved',
241
-    });
242
-  },
243
-
244
-  /**
245
-   * Update current document title (local state only)
180
+   * Set current document
246 181
    *
247
-   * Updates the title of the current document in local state only.
248
-   * Does NOT persist to the backend. Sets save status to 'unsaved'.
182
+   * Directly sets the current document in state (local only).
183
+   * Used when document is already loaded.
249 184
    *
250
-   * This is typically used for real-time editing, with auto-save
251
-   * handling the actual persistence.
252
-   *
253
-   * @param title - New title string
185
+   * @param document - Document to set as current
254 186
    */
255
-  updateTitle: (title: string) => {
256
-    const currentDoc = get().currentDocument;
257
-    if (!currentDoc) return;
258
-
259
-    const updatedDoc: Document = {
260
-      ...currentDoc,
261
-      title,
262
-      updatedAt: Date.now(),
263
-    };
264
-
187
+  setCurrentDocument: (document: Document | null) => {
265 188
     set({
266
-      currentDocument: updatedDoc,
267
-      saveStatus: 'unsaved',
189
+      currentDocument: document,
190
+      saveStatus: 'saved',
268 191
     });
269 192
   },
270 193
 

+ 51 - 2
src/stores/editorStore.ts

@@ -91,6 +91,11 @@ interface EditorStore {
91 91
   getBlocksByType: (type: BlockType) => DocumentBlock[];
92 92
   
93 93
   /**
94
+   * 保存单个block的更改
95
+   */
96
+  saveBlock: (id: string) => Promise<void>;
97
+  
98
+  /**
94 99
    * 重新计算所有块的block_order(稀疏排序)
95 100
    */
96 101
   recomputeBlockOrders: () => void;
@@ -171,7 +176,7 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
171 176
       
172 177
       set({
173 178
         documentId,
174
-        documentTitle: data.title || '未命名文档',
179
+        documentTitle: '未命名文档', // 后端不返回title,使用默认值
175 180
         blocks: data.blocks,
176 181
         isLoading: false,
177 182
       });
@@ -195,7 +200,18 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
195 200
     set({ isSaving: true, error: null });
196 201
     
197 202
     try {
198
-      await blockService.updateBlocks(documentId, blocks);
203
+      // 后端不支持批量更新,需要逐个更新block
204
+      // 注意:这是一个临时方案,实际使用中应该优化为仅更新变化的blocks
205
+      // 或者使用防抖/节流策略减少API调用
206
+      for (const block of blocks) {
207
+        await blockService.updateBlock(documentId, block.id, {
208
+          content: block.content as any, // 类型断言:不同block类型的content类型不同
209
+          style: block.style,
210
+          word_style: block.word_style,
211
+          metadata: block.metadata,
212
+        });
213
+      }
214
+      
199 215
       set({ isSaving: false });
200 216
     } catch (error: any) {
201 217
       set({
@@ -308,6 +324,39 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
308 324
     return blocks.filter((block) => block.type === type);
309 325
   },
310 326
 
327
+  // ── saveBlock ───────────────────────────────────────────────────────────
328
+  saveBlock: async (id: string) => {
329
+    const { documentId, blocks } = get();
330
+    
331
+    if (!documentId) {
332
+      throw new Error('没有打开的文档');
333
+    }
334
+    
335
+    const block = blocks.find(b => b.id === id);
336
+    if (!block) {
337
+      throw new Error('块不存在');
338
+    }
339
+    
340
+    set({ isSaving: true, error: null });
341
+    
342
+    try {
343
+      await blockService.updateBlock(documentId, id, {
344
+        content: block.content as any, // 类型断言:不同block类型的content类型不同
345
+        style: block.style,
346
+        word_style: block.word_style,
347
+        metadata: block.metadata,
348
+      });
349
+      
350
+      set({ isSaving: false });
351
+    } catch (error: any) {
352
+      set({
353
+        error: error.message || '保存块失败',
354
+        isSaving: false,
355
+      });
356
+      throw error;
357
+    }
358
+  },
359
+  
311 360
   // ── recomputeBlockOrders ────────────────────────────────────────────────
312 361
   recomputeBlockOrders: () => {
313 362
     const { blocks } = get();

+ 22 - 43
src/types/api.ts

@@ -2,50 +2,28 @@
2 2
  * API request and response type definitions
3 3
  */
4 4
 
5
-// Re-export export-related types from export.ts to avoid duplication
6
-export type { ExportDocRequest, ExportDocResponse } from './export';
5
+// Re-export document and export types
6
+export type { 
7
+  CreateDocumentRequest,
8
+  CreateDocumentResponse,
9
+  Document,
10
+  DocumentListItem,
11
+  DocumentListFilters,
12
+  GetDocumentOptions,
13
+  ListDocumentsResponse,
14
+  DocumentPagination,
15
+} from './document';
7 16
 
8
-/**
9
- * Request payload for creating a new document
10
- */
11
-export interface CreateDocumentRequest {
12
-  /** Document title */
13
-  title: string;
14
-  /** Markdown content */
15
-  content: string;
16
-  /** Document format (fixed as markdown) */
17
-  format: 'markdown';
18
-  /** Optional chat session ID */
19
-  sessionId?: string;
20
-  /** Optional template ID */
21
-  templateId?: string;
22
-  /** Document source */
23
-  source: 'chat' | 'workflow';
24
-}
25
-
26
-/**
27
- * Block update for partial document updates
28
- */
29
-export interface BlockUpdate {
30
-  /** Heading level (1-6) */
31
-  level: number;
32
-  /** Index of this heading level */
33
-  index: number;
34
-  /** Block content */
35
-  content: string;
36
-}
37
-
38
-/**
39
- * Request payload for updating an existing document
40
- */
41
-export interface UpdateDocumentRequest {
42
-  /** Updated title */
43
-  title?: string;
44
-  /** Updated content (mutually exclusive with blocks) */
45
-  content?: string;
46
-  /** Partial block updates (mutually exclusive with content) */
47
-  blocks?: BlockUpdate[];
48
-}
17
+export type { 
18
+  ExportDocRequest, 
19
+  ExportDocResponse,
20
+  ExportRecord,
21
+  ExportRecordListItem,
22
+  ExportRecordListFilters,
23
+  ListExportRecordsResponse,
24
+  ExportRecordPagination,
25
+  StorageInfo,
26
+} from './export';
49 27
 
50 28
 /**
51 29
  * Generic API response wrapper
@@ -76,6 +54,7 @@ export interface PaginationInfo {
76 54
 
77 55
 /**
78 56
  * List filters for document queries
57
+ * @deprecated Use DocumentListFilters from ./document instead
79 58
  */
80 59
 export interface ListFilters {
81 60
   /** Filter by source */

+ 21 - 56
src/types/document.ts

@@ -6,19 +6,6 @@
6 6
  */
7 7
 
8 8
 /**
9
- * Block update for partial document updates
10
- * Used to update specific sections by heading level and index
11
- */
12
-export interface BlockUpdate {
13
-  /** Heading level (1-6, where 1 = H1, 2 = H2, etc.) */
14
-  level: number;
15
-  /** Index of this heading level in the document (0-based) */
16
-  index: number;
17
-  /** New content for this block (including the heading itself) */
18
-  content: string;
19
-}
20
-
21
-/**
22 9
  * Request payload for creating a document
23 10
  */
24 11
 export interface CreateDocumentRequest {
@@ -28,60 +15,38 @@ export interface CreateDocumentRequest {
28 15
   fileUrl: string;
29 16
   /** Session ID to associate this document with */
30 17
   sessionId: string;
31
-  /** Optional document title (if not provided, will be extracted from content) */
32
-  title?: string;
33
-}
34
-
35
-/**
36
- * Request payload for updating a document
37
- * Either content (full update) or blocks (partial update) must be provided
38
- */
39
-export interface UpdateDocumentRequest {
40
-  /** Document title */
41
-  title?: string;
42
-  /** Full document content (mutually exclusive with blocks) */
43
-  content?: string;
44
-  /** List of blocks to update (mutually exclusive with content) */
45
-  blocks?: BlockUpdate[];
46 18
 }
47 19
 
48 20
 /**
49 21
  * Document entity (full details)
22
+ * 对应后端Document模型,使用SQLite存储blocks内容
50 23
  */
51 24
 export interface Document {
52 25
   /** Document ID (format: doc-{uuid[:12]}) */
53 26
   id: string;
54
-  /** Document title */
55
-  title: string;
56
-  /** Document content in Markdown format */
57
-  content: string;
58
-  /** Content format (always "markdown") */
59
-  format: string;
27
+  /** Path to SQLite content database */
28
+  contentDbPath: string;
60 29
   /** Associated session ID */
61 30
   sessionId: string;
62
-  /** Document source (chat or workflow) */
63
-  source: 'chat' | 'workflow';
64 31
   /** User ID who created the document */
65
-  userId: string;
32
+  userId: string | null;
66 33
   /** Creation timestamp (milliseconds) */
67 34
   createdAt: number;
68 35
   /** Last update timestamp (milliseconds) */
69 36
   updatedAt: number;
37
+  /** Optional: blocks array (only when includeBlocks=true) */
38
+  blocks?: any[];
70 39
 }
71 40
 
72 41
 /**
73
- * Document list item (without content field)
42
+ * Document list item (without blocks)
74 43
  * Used in list responses to reduce payload size
75 44
  */
76 45
 export interface DocumentListItem {
77 46
   /** Document ID */
78 47
   id: string;
79
-  /** Document title */
80
-  title: string;
81 48
   /** Associated session ID */
82 49
   sessionId: string;
83
-  /** Document source (chat or workflow) */
84
-  source: 'chat' | 'workflow';
85 50
   /** Creation timestamp (milliseconds) */
86 51
   createdAt: number;
87 52
   /** Last update timestamp (milliseconds) */
@@ -94,21 +59,13 @@ export interface DocumentListItem {
94 59
 export interface CreateDocumentResponse {
95 60
   /** Created document ID */
96 61
   documentId: string;
97
-  /** Content format (always "markdown") */
98
-  format: string;
62
+  /** Path to SQLite content database */
63
+  contentDbPath: string;
99 64
   /** Creation timestamp (milliseconds) */
100 65
   createdAt: number;
101 66
 }
102 67
 
103
-/**
104
- * Response from update document API
105
- */
106
-export interface UpdateDocumentResponse {
107
-  /** Updated document ID */
108
-  documentId: string;
109
-  /** Update timestamp (milliseconds) */
110
-  updatedAt: number;
111
-}
68
+
112 69
 
113 70
 /**
114 71
  * Pagination info for document list
@@ -144,10 +101,18 @@ export interface DocumentListFilters {
144 101
   page?: number;
145 102
   /** Number of items per page */
146 103
   pageSize?: number;
147
-  /** Filter by session ID */
104
+  /** Filter by session ID (optional) */
148 105
   sessionId?: string;
149
-  /** Sort by field (createdAt or updatedAt) */
150
-  sortBy?: 'createdAt' | 'updatedAt';
106
+  /** Sort by field (created_at or updated_at) */
107
+  sortBy?: 'created_at' | 'updated_at';
151 108
   /** Sort order */
152 109
   sortOrder?: 'asc' | 'desc';
153 110
 }
111
+
112
+/**
113
+ * Options for getting document details
114
+ */
115
+export interface GetDocumentOptions {
116
+  /** Whether to include blocks array in response */
117
+  includeBlocks?: boolean;
118
+}

+ 59 - 9
src/types/editor.ts

@@ -207,36 +207,86 @@ export interface EditorState {
207 207
  * 获取blocks的API响应
208 208
  */
209 209
 export interface GetBlocksResponse {
210
-  documentId: string;
211
-  title: string;
212 210
   blocks: DocumentBlock[];
213 211
 }
214 212
 
215 213
 /**
216
- * 更新blocks的API请求
214
+ * 获取单个block的API响应
217 215
  */
218
-export interface UpdateBlocksRequest {
216
+export interface GetBlockResponse {
217
+  block: DocumentBlock;
218
+}
219
+
220
+/**
221
+ * 更新block的API请求(部分字段)
222
+ */
223
+export interface UpdateBlockRequest {
224
+  content?: string | RichText[] | TableContent;
225
+  style?: StyleOverrides;
226
+  word_style?: string;
227
+  metadata?: Record<string, any>;
228
+}
229
+
230
+/**
231
+ * 更新block的API响应
232
+ */
233
+export interface UpdateBlockResponse {
234
+  blockId: string;
235
+  message: string;
236
+}
237
+
238
+/**
239
+ * 搜索blocks的API响应
240
+ */
241
+export interface SearchBlocksResponse {
219 242
   blocks: DocumentBlock[];
243
+  total: number;
244
+  query: string;
220 245
 }
221 246
 
222 247
 /**
248
+ * 目录树节点
249
+ */
250
+export interface TOCNode {
251
+  id: string;
252
+  level: number;
253
+  content: string;
254
+  children: TOCNode[];
255
+}
256
+
257
+/**
258
+ * 获取目录树的API响应
259
+ */
260
+export interface GetTOCResponse {
261
+  toc: TOCNode[];
262
+}
263
+
264
+/**
265
+ * 统计信息
266
+ */
267
+export interface BlockStatsResponse {
268
+  total: number;
269
+  by_type: Record<string, number>;
270
+}
271
+
272
+// ══════════════════════════════════════════════════════════════════════════════
273
+// Utility Types
274
+// ══════════════════════════════════════════════════════════════════════════════
275
+
276
+/**
223 277
  * 创建block的请求
224 278
  */
225 279
 export interface CreateBlockRequest {
226 280
   type: BlockType;
227
-  block_order: number;
228 281
   level: number;
229 282
   index: number;
230 283
   content: any;
231 284
   word_style: string;
232 285
   style?: StyleOverrides;
233 286
   metadata?: Record<string, any>;
287
+  after_block_id?: string;  // 在哪个block后插入
234 288
 }
235 289
 
236
-// ══════════════════════════════════════════════════════════════════════════════
237
-// Utility Types
238
-// ══════════════════════════════════════════════════════════════════════════════
239
-
240 290
 /**
241 291
  * Block创建参数(部分字段可选)
242 292
  */

+ 51 - 6
src/types/index.ts

@@ -3,19 +3,63 @@
3 3
  */
4 4
 
5 5
 // Document types
6
-export type { Document, DocumentListItem } from './document';
6
+export type {
7
+  Document,
8
+  DocumentListItem,
9
+  CreateDocumentRequest,
10
+  CreateDocumentResponse,
11
+  DocumentListFilters,
12
+  GetDocumentOptions,
13
+  ListDocumentsResponse,
14
+  DocumentPagination,
15
+} from './document';
16
+
17
+// Export types
18
+export type {
19
+  ExportDocRequest,
20
+  ExportDocResponse,
21
+  ExportRecord,
22
+  ExportRecordListItem,
23
+  ExportRecordListFilters,
24
+  ListExportRecordsResponse,
25
+  ExportRecordPagination,
26
+  StorageInfo,
27
+} from './export';
28
+
29
+// Editor/Block types
30
+export type {
31
+  BlockType,
32
+  RichText,
33
+  RichTextStyle,
34
+  StyleOverrides,
35
+  DocumentBlock,
36
+  HeadingBlock,
37
+  ParagraphBlock,
38
+  TableBlock,
39
+  ImageBlock,
40
+  TableCell,
41
+  TableRow,
42
+  TableContent,
43
+  EditorState,
44
+  GetBlocksResponse,
45
+  GetBlockResponse,
46
+  UpdateBlockRequest,
47
+  UpdateBlockResponse,
48
+  CreateBlockRequest,
49
+  SearchBlocksResponse,
50
+  TOCNode,
51
+  GetTOCResponse,
52
+  BlockStatsResponse,
53
+  BlockUpdate,
54
+  PartialBlock,
55
+} from './editor';
7 56
 
8 57
 // Chat types
9 58
 export type { ChatMessage } from './chat';
10 59
 
11 60
 // API types
12 61
 export type {
13
-  CreateDocumentRequest,
14
-  UpdateDocumentRequest,
15
-  ExportDocRequest,
16
-  ExportDocResponse,
17 62
   ApiResponse,
18
-  BlockUpdate,
19 63
   PaginationInfo,
20 64
   ListFilters,
21 65
 } from './api';
@@ -25,3 +69,4 @@ export type { SaveStatus } from './ui';
25 69
 
26 70
 // Store types
27 71
 export type { DocumentStoreState, ChatStoreState, UIStoreState } from './store';
72
+

+ 6 - 12
src/types/store.ts

@@ -2,12 +2,10 @@
2 2
  * Zustand store state type definitions
3 3
  */
4 4
 
5
-import type { Document, DocumentListItem } from './document';
5
+import type { Document, DocumentListItem, CreateDocumentRequest } from './document';
6 6
 import type { ChatMessage, ChatSession } from './chat';
7 7
 import type { SaveStatus } from './ui';
8 8
 import type {
9
-  CreateDocumentRequest,
10
-  UpdateDocumentRequest,
11 9
   ListFilters,
12 10
   PaginationInfo,
13 11
 } from './api';
@@ -30,17 +28,13 @@ export interface DocumentStoreState {
30 28
   /** Create a new document */
31 29
   createDocument: (req: CreateDocumentRequest) => Promise<void>;
32 30
   /** Fetch a document by ID */
33
-  fetchDocument: (id: string) => Promise<void>;
31
+  fetchDocument: (id: string, includeBlocks?: boolean) => Promise<void>;
34 32
   /** Fetch document list with optional filters */
35 33
   fetchDocumentList: (filters?: ListFilters) => Promise<void>;
36
-  /** Update an existing document */
37
-  updateDocument: (id: string, req: UpdateDocumentRequest) => Promise<void>;
38
-  /** Delete a document by ID */
39
-  deleteDocument: (id: string) => Promise<void>;
40
-  /** Update current document content (local state only) */
41
-  updateContent: (content: string) => void;
42
-  /** Update current document title (local state only) */
43
-  updateTitle: (title: string) => void;
34
+  /** Delete documents by session ID */
35
+  deleteDocumentsBySession: (sessionId: string) => Promise<void>;
36
+  /** Set current document (local state only) */
37
+  setCurrentDocument: (document: Document | null) => void;
44 38
   /** Set save status */
45 39
   setSaveStatus: (status: SaveStatus) => void;
46 40
 }