Przeglądaj źródła

feat(编辑器): 优化API错误处理、图片缩放性能与编辑器更新逻辑

- 改进ChatPanel中的文档缓存错误处理,区分404和其他API错误
- 优化ImageBlock图片缩放交互,使用requestAnimationFrame批处理鼠标移动事件,提升性能
- 简化RichTextEditor的HTML转换逻辑,移除不必要的baseStyle参数传递
- 优化editorStore中updateBlock方法,改用数组slice和直接索引替代map遍历,提升更新效率
- 移除richTextConverter中未使用的unescapeHtml函数
- 清空环境变量中的AI服务API密钥
Zhang Yice 1 miesiąc temu
rodzic
commit
84f9a861d6

+ 2 - 2
.env

@@ -14,6 +14,6 @@ VITE_DEBUG=false
14 14
 
15 15
 # External AI services are called directly by the browser.
16 16
 VITE_AI_API_URL=http://114.242.25.27:3000/api/v1/chat/completions
17
-VITE_AI_API_KEY=XAgent-eVVoqEO7WJYwzCc5wQ8meEtIuyIQgHhxYvpd6fSwa7BwJW8CaBom4
17
+VITE_AI_API_KEY=
18 18
 VITE_WORKFLOW_API_URL=http://114.242.25.27:3000/api/v2/chat/completions
19
-VITE_WORKFLOW_API_KEY=XAgent-mWHBqQw06psUYRqx6PrHWiKdfY05ebt7I9drDBHzaG9QQesIkVEICj
19
+VITE_WORKFLOW_API_KEY=

+ 6 - 2
src/components/ChatPanel/MessageItem.tsx

@@ -158,7 +158,7 @@ const MessageItem: React.FC<MessageItemProps> = memo(
158 158
         setIsCreatingDocument(true);
159 159
         
160 160
         // Step 1: Check localStorage cache
161
-        const { default: apiClient } = await import('../../services/api');
161
+        const { default: apiClient, isApiError } = await import('../../services/api');
162 162
         const cacheKey = `doc_cache_${currentSessionId}_${exportRecord.recordId}`;
163 163
         const cachedDocId = localStorage.getItem(cacheKey);
164 164
         
@@ -170,7 +170,11 @@ const MessageItem: React.FC<MessageItemProps> = memo(
170 170
             await apiClient.get(`/api/v1/documents/${cachedDocId}`);
171 171
             // Document exists, reuse it
172 172
             documentId = cachedDocId;
173
-          } catch {
173
+          } catch (error) {
174
+            if (!isApiError(error) || error.status !== 404) {
175
+              throw error;
176
+            }
177
+
174 178
             // Document no longer exists, clear cache and create new
175 179
             localStorage.removeItem(cacheKey);
176 180
             

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

@@ -233,14 +233,7 @@ export const RichTextEditor: React.FC<RichTextEditorProps> = ({
233 233
       return;
234 234
     }
235 235
     
236
-    // 提取baseStyle中的字号和字体
237
-    const baseFontSize = baseStyle.fontSize ? parseFloat(String(baseStyle.fontSize)) : undefined;
238
-    const baseFontFamily = baseStyle.fontFamily ? String(baseStyle.fontFamily) : undefined;
239
-    
240
-    const html = richTextToHtml(value, {
241
-      fontSize: baseFontSize,
242
-      fontFamily: baseFontFamily,
243
-    });
236
+    const html = richTextToHtml(value);
244 237
     
245 238
     // 只在内容真正改变时更新,避免光标跳动
246 239
     if (editorRef.current.innerHTML !== html) {

+ 39 - 10
src/components/Editor/blocks/ImageBlock.tsx

@@ -47,6 +47,8 @@ export const ImageBlock: React.FC<ImageBlockProps> = ({
47 47
     mouseX: 0,
48 48
     mouseY: 0,
49 49
   });
50
+  const pendingResizeEventRef = useRef<MouseEvent | null>(null);
51
+  const resizeFrameRef = useRef<number | null>(null);
50 52
 
51 53
   // 查找当前块的位置
52 54
   const isFirst = index === 0;
@@ -159,22 +161,19 @@ export const ImageBlock: React.FC<ImageBlockProps> = ({
159 161
     setIsResizing(true);
160 162
   }, []);
161 163
 
162
-  // 拖拽调整中
163
-  const handleResizeMove = useCallback((e: MouseEvent) => {
164
-    if (!isResizing) return;
165
-    
166
-    const deltaX = e.clientX - startSizeRef.current.mouseX;
167
-    
164
+  const applyResizeEvent = useCallback((event: MouseEvent) => {
165
+    const deltaX = event.clientX - startSizeRef.current.mouseX;
166
+
168 167
     // 计算新宽度(保持宽高比)
169 168
     const newWidth = Math.max(50, startSizeRef.current.width + deltaX);
170 169
     const aspectRatio = startSizeRef.current.height / startSizeRef.current.width;
171 170
     const newHeight = newWidth * aspectRatio;
172
-    
171
+
173 172
     // 转换为当前单位
174 173
     const { unit } = block.style;
175 174
     const unitMap = { cm: 37.795, inch: 96, px: 1 };
176 175
     const multiplier = unitMap[unit] || 1;
177
-    
176
+
178 177
     updateBlock(block.id, {
179 178
       style: {
180 179
         ...block.style,
@@ -182,12 +181,37 @@ export const ImageBlock: React.FC<ImageBlockProps> = ({
182 181
         height: parseFloat((newHeight / multiplier).toFixed(2)),
183 182
       },
184 183
     });
185
-  }, [isResizing, block.id, block.style, updateBlock]);
184
+  }, [block.id, block.style, updateBlock]);
185
+
186
+  // 拖拽调整中
187
+  const handleResizeMove = useCallback((e: MouseEvent) => {
188
+    if (!isResizing) return;
189
+    pendingResizeEventRef.current = e;
190
+
191
+    if (resizeFrameRef.current !== null) return;
192
+
193
+    resizeFrameRef.current = requestAnimationFrame(() => {
194
+      resizeFrameRef.current = null;
195
+      const pendingEvent = pendingResizeEventRef.current;
196
+      pendingResizeEventRef.current = null;
197
+      if (!pendingEvent) return;
198
+
199
+      applyResizeEvent(pendingEvent);
200
+    });
201
+  }, [applyResizeEvent, isResizing]);
186 202
 
187 203
   // 结束拖拽调整
188 204
   const handleResizeEnd = useCallback(() => {
205
+    if (pendingResizeEventRef.current) {
206
+      applyResizeEvent(pendingResizeEventRef.current);
207
+    }
208
+    if (resizeFrameRef.current !== null) {
209
+      cancelAnimationFrame(resizeFrameRef.current);
210
+      resizeFrameRef.current = null;
211
+    }
212
+    pendingResizeEventRef.current = null;
189 213
     setIsResizing(false);
190
-  }, []);
214
+  }, [applyResizeEvent]);
191 215
   // 监听鼠标移动和释放事件
192 216
   React.useEffect(() => {
193 217
     if (isResizing) {
@@ -199,6 +223,11 @@ export const ImageBlock: React.FC<ImageBlockProps> = ({
199 223
       return () => {
200 224
         document.removeEventListener('mousemove', handleResizeMove);
201 225
         document.removeEventListener('mouseup', handleResizeEnd);
226
+        if (resizeFrameRef.current !== null) {
227
+          cancelAnimationFrame(resizeFrameRef.current);
228
+          resizeFrameRef.current = null;
229
+        }
230
+        pendingResizeEventRef.current = null;
202 231
         document.body.style.cursor = '';
203 232
         document.body.style.userSelect = '';
204 233
       };

+ 10 - 9
src/stores/editorStore.ts

@@ -982,16 +982,17 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
982 982
   // ── updateBlock ─────────────────────────────────────────────────────────
983 983
   updateBlock: (id: string, updates: BlockUpdate) => {
984 984
     const { blocks, dirtyBlocks, blockHashes, autoSaveEnabled } = get();
985
-    
986
-    const newBlocks = blocks.map((block) =>
987
-      block.id === id ? { ...block, ...updates } as DocumentBlock : block
988
-    );
989
-    
990
-    // 获取更新后的块
991
-    const updatedBlock = newBlocks.find(b => b.id === id);
992
-    if (!updatedBlock) {
993
-      return; // 块不存在,跳过
985
+
986
+    const blockIndex = blocks.findIndex((block) => block.id === id);
987
+    if (blockIndex < 0) {
988
+      return;
994 989
     }
990
+
991
+    const newBlocks = blocks.slice();
992
+    newBlocks[blockIndex] = { ...blocks[blockIndex], ...updates } as DocumentBlock;
993
+
994
+    // 获取更新后的块
995
+    const updatedBlock = newBlocks[blockIndex];
995 996
     
996 997
     // 计算新的哈希值
997 998
     const newHash = computeBlockHash(updatedBlock);

+ 2 - 17
src/utils/richTextConverter.ts

@@ -26,20 +26,6 @@ function escapeHtml(text: string): string {
26 26
   return text.replace(/[&<>"']/g, (char) => map[char]);
27 27
 }
28 28
 
29
-/**
30
- * 反转义HTML特殊字符
31
- */
32
-function unescapeHtml(html: string): string {
33
-  const map: Record<string, string> = {
34
-    '&amp;': '&',
35
-    '&lt;': '<',
36
-    '&gt;': '>',
37
-    '&quot;': '"',
38
-    '&#39;': "'",
39
-  };
40
-  return html.replace(/&(?:amp|lt|gt|quot|#39);/g, (entity) => map[entity]);
41
-}
42
-
43 29
 function toCssString(value: string): string {
44 30
   const escaped = Array.from(value, (char) => {
45 31
     const codePoint = char.codePointAt(0) ?? 0;
@@ -108,7 +94,6 @@ export function hexToRgb(hex: string): string {
108 94
  * RichText数组转HTML字符串(用于contenteditable渲染)
109 95
  * 
110 96
  * @param content 富文本内容
111
- * @param baseStyle 基础样式(可选,用于提供默认字号和字体)
112 97
  * @returns HTML字符串
113 98
  * 
114 99
  * @example
@@ -120,7 +105,7 @@ export function hexToRgb(hex: string): string {
120 105
  * // "<span>普通</span><strong>加粗</strong>"
121 106
  * ```
122 107
  */
123
-export function richTextToHtml(content: string | RichText[], baseStyle?: { fontSize?: number; fontFamily?: string }): string {
108
+export function richTextToHtml(content: string | RichText[]): string {
124 109
   if (typeof content === 'string') {
125 110
     return escapeHtml(content);
126 111
   }
@@ -324,7 +309,7 @@ export function htmlToRichText(html: string): RichText[] {
324 309
       const text = node.textContent || '';
325 310
       if (text) {
326 311
         richText.push({
327
-          text: unescapeHtml(text),
312
+          text,
328 313
           style: extractStyleFromElement(node.parentElement),
329 314
         });
330 315
       }