Procházet zdrojové kódy

feat(编辑器): 优化应用元数据、性能指标与布局交互

- 更新HTML语言标签为zh-CN,添加主题色与应用描述元数据
- 调整预览服务配置,支持跨主机访问(--host 0.0.0.0)并新增生产构建预览命令
- 修复会话历表抽屉条件渲染逻辑,避免不必要的DOM挂载
- 为聊天气泡消息添加CSS containment属性(contentVisibility、containIntrinsicSize)提升长列表渲染性能
- 优化图片块加载性能:添加aspectRatio CSS支持、lazy加载与async解码属性
- 重构ResizableLayout拖拽逻辑,使用requestAnimationFrame合并高频更新,仅在拖拽结束时持久化宽度配置
- 统一配色方案为浅色模式,移除暗色模式支持
- 补全Vite构建配置的样式处理与资源优化策略
Zhang Yice před 1 měsícem
rodič
revize
e938108335

+ 4 - 2
index.html

@@ -1,10 +1,12 @@
1 1
 <!doctype html>
2
-<html lang="en">
2
+<html lang="zh-CN">
3 3
   <head>
4 4
     <meta charset="UTF-8" />
5 5
     <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6 6
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
-    <title>ax-frontend-app</title>
7
+    <meta name="theme-color" content="#ffffff" />
8
+    <meta name="description" content="AX 文档编辑与导出工作台" />
9
+    <title>AX 文档工作台</title>
8 10
   </head>
9 11
   <body>
10 12
     <div id="root"></div>

+ 3 - 1
package.json

@@ -11,7 +11,9 @@
11 11
     "lint": "eslint .",
12 12
     "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
13 13
     "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
14
-    "preview": "vite preview",
14
+    "preview": "vite preview --host 0.0.0.0 --port 4173",
15
+    "serve:prod": "npm run build && vite preview --host 0.0.0.0 --port 4173",
16
+    "lighthouse:ready": "npm run build && vite preview --host 0.0.0.0 --port 4173",
15 17
     "test": "vitest run",
16 18
     "test:watch": "vitest",
17 19
     "test:document": "vitest run src/utils/blockOperations.test.ts src/utils/richTextConverter.test.ts",

+ 3 - 3
src/App.tsx

@@ -190,12 +190,12 @@ const App: React.FC = () => {
190 190
   }, [previewDocumentId, previewDocumentName, handleCloseEditor, currentUserId]);
191 191
 
192 192
   // ── Session history drawer ────────────────────────────────────────────────
193
-  const sessionListDrawer = (
193
+  const sessionListDrawer = sessionListOpen ? (
194 194
     <Drawer
195 195
       title="会话历史"
196 196
       placement="left"
197 197
       width={360}
198
-      open={sessionListOpen}
198
+      open
199 199
       onClose={handleCloseSessionList}
200 200
       styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column' } }}
201 201
       data-testid="session-list-drawer"
@@ -204,7 +204,7 @@ const App: React.FC = () => {
204 204
         <SessionList onSessionLoad={handleCloseSessionList} />
205 205
       </Suspense>
206 206
     </Drawer>
207
-  );
207
+  ) : null;
208 208
 
209 209
   // ── Session history toggle button ─────────────────────────────────────────
210 210
   const sessionListButton = (

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

@@ -27,6 +27,8 @@ const messageRowStyle = (role: ChatMessage['role']): React.CSSProperties => ({
27 27
   flexDirection: 'column',
28 28
   alignItems: role === 'user' ? 'flex-end' : 'flex-start',
29 29
   gap: '4px',
30
+  contentVisibility: 'auto',
31
+  containIntrinsicSize: '0 96px',
30 32
 });
31 33
 
32 34
 const bubbleStyle = (role: ChatMessage['role']): React.CSSProperties => ({

+ 16 - 1
src/components/Editor/blocks/ImageBlock.tsx

@@ -137,6 +137,10 @@ export const ImageBlock: React.FC<ImageBlockProps> = ({
137 137
     };
138 138
   }, [block.style]);
139 139
 
140
+  const imageAspectRatio = block.style.width > 0 && block.style.height > 0
141
+    ? `${block.style.width} / ${block.style.height}`
142
+    : undefined;
143
+
140 144
   // 开始拖拽调整尺寸
141 145
   const handleResizeStart = useCallback((e: React.MouseEvent) => {
142 146
     e.preventDefault();
@@ -470,7 +474,16 @@ export const ImageBlock: React.FC<ImageBlockProps> = ({
470 474
       {/* 图片容器 */}
471 475
       <div className={`image-block align-${block.style.align}`}>
472 476
         {block.content ? (
473
-          <div className="image-wrapper" style={{ position: 'relative', display: 'inline-block' }}>
477
+          <div
478
+            className="image-wrapper"
479
+            style={{
480
+              position: 'relative',
481
+              display: 'inline-block',
482
+              width: imageSize.width,
483
+              maxWidth: '100%',
484
+              aspectRatio: imageAspectRatio,
485
+            }}
486
+          >
474 487
             <img
475 488
               ref={imageRef}
476 489
               src={block.content}
@@ -479,6 +492,8 @@ export const ImageBlock: React.FC<ImageBlockProps> = ({
479 492
                 maxWidth: '100%',
480 493
                 ...imageSize,
481 494
               }}
495
+              loading="lazy"
496
+              decoding="async"
482 497
               draggable={false}
483 498
             />
484 499
             

+ 27 - 3
src/components/Layout/ResizableLayout.tsx

@@ -7,7 +7,7 @@
7 7
  * @module components/Layout/ResizableLayout
8 8
  */
9 9
 
10
-import React, { useState, useRef, useCallback } from 'react';
10
+import React, { useState, useRef, useCallback, useEffect } from 'react';
11 11
 import { getItem, setItem } from '../../utils/storage';
12 12
 
13 13
 /**
@@ -73,6 +73,8 @@ const ResizableLayout: React.FC<ResizableLayoutProps> = ({
73 73
 }) => {
74 74
   const containerRef = useRef<HTMLDivElement>(null);
75 75
   const isDraggingRef = useRef(false);
76
+  const pendingWidthRef = useRef<number | null>(null);
77
+  const animationFrameRef = useRef<number | null>(null);
76 78
 
77 79
   // State-tracked dragging flag for render (avoids ref read during render)
78 80
   const [isDragging, setIsDragging] = useState(false);
@@ -88,12 +90,17 @@ const ResizableLayout: React.FC<ResizableLayoutProps> = ({
88 90
   const applyWidth = useCallback(
89 91
     (newWidth: number) => {
90 92
       setLeftWidth(newWidth);
91
-      setItem(STORAGE_KEY, newWidth);
92 93
       onWidthChange?.(newWidth);
93 94
     },
94 95
     [onWidthChange]
95 96
   );
96 97
 
98
+  useEffect(() => () => {
99
+    if (animationFrameRef.current !== null) {
100
+      cancelAnimationFrame(animationFrameRef.current);
101
+    }
102
+  }, []);
103
+
97 104
   // ── Drag handlers ───────────────────────────────────────────────────────
98 105
 
99 106
   const handleMouseDown = useCallback(
@@ -123,12 +130,29 @@ const ResizableLayout: React.FC<ResizableLayoutProps> = ({
123 130
           maxWidthPct
124 131
         );
125 132
 
126
-        applyWidth(newLeftPct);
133
+        pendingWidthRef.current = newLeftPct;
134
+        if (animationFrameRef.current === null) {
135
+          animationFrameRef.current = requestAnimationFrame(() => {
136
+            animationFrameRef.current = null;
137
+            if (pendingWidthRef.current !== null) {
138
+              applyWidth(pendingWidthRef.current);
139
+            }
140
+          });
141
+        }
127 142
       };
128 143
 
129 144
       const onMouseUp = () => {
130 145
         isDraggingRef.current = false;
131 146
         setIsDragging(false);
147
+        if (animationFrameRef.current !== null) {
148
+          cancelAnimationFrame(animationFrameRef.current);
149
+          animationFrameRef.current = null;
150
+        }
151
+        if (pendingWidthRef.current !== null) {
152
+          applyWidth(pendingWidthRef.current);
153
+          setItem(STORAGE_KEY, pendingWidthRef.current);
154
+          pendingWidthRef.current = null;
155
+        }
132 156
         document.removeEventListener('mousemove', onMouseMove);
133 157
         document.removeEventListener('mouseup', onMouseUp);
134 158
       };

+ 1 - 1
src/index.css

@@ -16,7 +16,7 @@
16 16
 
17 17
   font: 18px/145% var(--sans);
18 18
   letter-spacing: 0.18px;
19
-  color-scheme: light dark;
19
+  color-scheme: light;
20 20
   color: var(--text);
21 21
   background: var(--bg);
22 22
   font-synthesis: none;

+ 14 - 5
vite.config.ts

@@ -18,17 +18,20 @@ export default defineConfig({
18 18
   ].filter(Boolean),
19 19
 
20 20
   build: {
21
-    // Enable minification for production builds (Req 13.9)
22
-    // Vite 8+ uses oxc by default; 'esbuild' requires a separate install.
23
-    // Leaving minify at its default ('oxc' in Vite 8) gives fast, aggressive
24
-    // minification out of the box.
25
-    // minify: 'esbuild',  // uncomment if esbuild is installed separately
21
+    // Keep production output minified for Lighthouse and real deployments.
22
+    minify: 'oxc',
23
+    cssMinify: 'lightningcss',
24
+    sourcemap: false,
25
+    reportCompressedSize: true,
26 26
 
27 27
     // Large optional libraries are split by dynamic imports.
28 28
     chunkSizeWarningLimit: 600,
29 29
 
30 30
     rollupOptions: {
31 31
       output: {
32
+        // Do not eagerly preload every dependency of the application entry.
33
+        // Dynamic panels and export libraries should be fetched only when used.
34
+        hoistTransitiveImports: false,
32 35
         manualChunks(id: string) {
33 36
           if (/node_modules[\\/](?:react|react-dom)[\\/]/.test(id)) {
34 37
             return 'react-vendor';
@@ -54,6 +57,12 @@ export default defineConfig({
54 57
     open: false,
55 58
   },
56 59
 
60
+  preview: {
61
+    port: 4173,
62
+    host: true,
63
+    strictPort: true,
64
+  },
65
+
57 66
   // 依赖预构建优化
58 67
   optimizeDeps: {
59 68
     include: [