Skip to content

Commit 117cdf7

Browse files
committed
feat: BaseForm重构与分子结构式组件
- 重构BaseForm组件,支持简单字段数组和Schema两种模式 - 新增MoleculeStructure组件,支持分子结构式渲染(RDKit) - Inventory页面使用table meta传递回调,减少不必要重渲染 - RDKit本地化,从CDN迁移到本地public目录 - Layout侧边栏活动指示器添加平滑动画过渡 - 删除.claude/settings.local.json从版本控制 - 添加代码分析文档到docs/plans/
1 parent 60f5fea commit 117cdf7

16 files changed

+2943
-1067
lines changed

.claude/settings.local.json

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# BaseForm 样式修复计划
2+
3+
## 问题概述
4+
5+
1. **BaseForm 样式与手写表单不一致**:需要确保 UI 完全一致,包括标签和输入框距离、错误文字距离、大小、输入框间距等
6+
2. **危险品 checkbox 未显示图标**:需要在 BaseForm 的危险品字段旁边显示 AlertTriangle 图标
7+
3. **布局不是 flex 布局,屏幕变窄时还是固定列数**
8+
9+
## 当前代码分析
10+
11+
### 手写表单样式(Inventory-old.tsx)
12+
13+
```jsx
14+
// 布局使用 grid
15+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
16+
// 每个字段
17+
<div>
18+
<Label ...>规格</Label>
19+
<Input ... />
20+
{error && <p>错误</p>}
21+
</div>
22+
</div>
23+
```
24+
25+
- 使用 `grid-cols-1 sm:grid-cols-3`,响应式列数
26+
- 每个字段是单独的 div,没有额外间距类
27+
28+
### BaseForm 当前样式
29+
30+
- 使用 `grid` 布局但列数固定:`style={{ gridTemplateColumns: repeat(${columns}, minmax(0, 1fr)) }}`
31+
- 没有响应式调整
32+
33+
## 修复方案
34+
35+
### 1. 修改 FormField 组件
36+
37+
**文件**: `frontend/src/components/ui/FormField.tsx`
38+
39+
调整样式使其与手写表单一致:
40+
- 移除 `space-y-2`,使用更精确的间距控制
41+
- Label: 保持 `text-base mb-1.5 block`
42+
- 错误文字: 保持 `text-sm text-destructive mt-1`
43+
44+
### 2. 修改 BaseForm 组件
45+
46+
**文件**: `frontend/src/components/BaseForm.tsx`
47+
48+
- 为 checkbox 类型字段添加特殊处理
49+
- 当字段名包含 "hazardous" 或 "危险品" 时,在复选框旁边显示 AlertTriangle 图标
50+
51+
### 3. 具体修改
52+
53+
#### FormField.tsx 修改
54+
55+
```tsx
56+
// 当前
57+
<div className={cn("space-y-2", className)}>
58+
59+
// 修改为
60+
<div className={cn("flex flex-col", className)}>
61+
```
62+
63+
#### BaseForm.tsx checkbox 字段修改
64+
65+
添加 AlertTriangle 图标到危险品复选框旁边。
66+
67+
## 验收标准
68+
69+
1. BaseForm 生成的表单样式与 Inventory-old.tsx 手写表单完全一致
70+
2. 危险品复选框旁边显示黄色 AlertTriangle 图标
71+
3. 标签与输入框的间距一致
72+
4. 错误文字与输入框的间距一致
73+
5. 输入框与输入框之间的上下间距一致
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
# Inventory.tsx 代码优化分析报告
2+
3+
## 一、问题确认
4+
5+
根据代码分析,确认以下问题确实存在:
6+
7+
### 1.1 TanStack Table 重渲染问题 ✅
8+
9+
**当前代码** ([`Inventory.tsx:452-560`](frontend/src/pages/Inventory.tsx:452)):
10+
```typescript
11+
const columns = useMemo(() => [
12+
// ...
13+
cell: info => (
14+
<HighlightText text={info.getValue() || ''} highlight={displayFilter} fuzzy={fuzzySearch} />
15+
),
16+
// ...
17+
], [displayFilter, handleEditClick, loadInventory, fuzzySearch])
18+
```
19+
20+
**问题**
21+
- `displayFilter``fuzzySearch``handleEditClick``loadInventory` 都在 columns 的依赖数组中
22+
- 每当用户输入一个字符,`displayFilter` 更新,整个 columns 定义被重新创建
23+
- 导致大量 DOM 节点销毁和重建,搜索时出现明显卡顿
24+
25+
---
26+
27+
### 1.2 分页类型确认 ✅
28+
29+
**当前实现** ([`Inventory.tsx:218-221`](frontend/src/pages/Inventory.tsx:218)):
30+
```typescript
31+
const params: Record<string, unknown> = {
32+
skip: pageParam,
33+
limit: 50,
34+
}
35+
```
36+
37+
**结论**:使用的是 **Offset 分页**,不是游标分页。
38+
39+
**后端实现** ([`app/api/inventory.py:756`](app/api/inventory.py:756)):
40+
```python
41+
items = db.exec(base.order_by(order_expr).offset(skip).limit(limit)).all()
42+
```
43+
44+
---
45+
46+
### 1.3 冗余状态 displayFilter ✅
47+
48+
**当前代码** ([`Inventory.tsx:194-203`](frontend/src/pages/Inventory.tsx:194)):
49+
```typescript
50+
const [globalFilter, setGlobalFilter] = useState('')
51+
const [displayFilter, setDisplayFilter] = useState('')
52+
53+
useEffect(() => {
54+
setDisplayFilter(globalFilter)
55+
}, [globalFilter])
56+
```
57+
58+
**问题**
59+
- `displayFilter` 只是简单复制 `globalFilter`,没有实际作用
60+
- 每次输入字符会触发两次状态更新和重渲染
61+
- 没有实现防抖 (Debounce) 功能
62+
63+
---
64+
65+
### 1.4 LoadingButton 颜色跳变 ✅
66+
67+
**当前代码** ([`Inventory.tsx:973-980`](frontend/src/pages/Inventory.tsx:973)):
68+
```typescript
69+
className={cn(
70+
"h-8 text-sm/4 px-3 border-0",
71+
isConfirming
72+
? isLoading
73+
? "text-destructive-foreground opacity-100 cursor-wait bg-destructive/70 transition-none"
74+
: "bg-destructive text-destructive-foreground hover:bg-destructive/70 transition-none"
75+
: "bg-primary hover:bg-primary/80"
76+
)}
77+
```
78+
79+
**问题**
80+
- `isLoading` 时虽然设置了 `bg-destructive/70`,但没有禁用 disabled 默认样式
81+
- 可能导致 loading 状态和其他状态颜色不一致
82+
83+
---
84+
85+
### 1.5 表单配置重复 ✅
86+
87+
**编辑表单** ([`Inventory.tsx:742-754`](frontend/src/pages/Inventory.tsx:742)):
88+
```typescript
89+
fields={[
90+
{ name: 'name', label: '试剂名称', type: 'input', required: true, colSpan: 2 },
91+
{ name: 'cas_number', label: 'CAS号', type: 'input', readOnly: true, colSpan: 1 },
92+
// ...
93+
]}
94+
```
95+
96+
**入库表单** ([`Inventory.tsx:783-795`](frontend/src/pages/Inventory.tsx:783)):
97+
```typescript
98+
fields={[
99+
{ name: 'name', label: '试剂名称', type: 'input', required: true, colSpan: 2 },
100+
{ name: 'cas_number', label: 'CAS号', type: 'input', required: true, colSpan: 1 },
101+
// ...
102+
]}
103+
```
104+
105+
**问题**:
106+
- 两个表单的字段配置大量重复
107+
- 维护困难,添加一个字段需要改4个地方
108+
109+
---
110+
111+
## 二、改进方案
112+
113+
### 2.1 表格列定义优化(优先级:P0)
114+
115+
**目标**:消除 columns 的外部依赖,解决搜索卡顿
116+
117+
**问题根因**:
118+
- 每次用户输入字符,`displayFilter` 更新
119+
- columns 的 useMemo 依赖了 `displayFilter`, `fuzzySearch`, `handleEditClick`, `loadInventory`
120+
- 导致整个列定义被重新创建,触发表格 DOM 树完全重绘
121+
122+
**影响程度**:100%(每次搜索都触发)
123+
124+
**方案**:利用 `table.meta` 注入状态和方法
125+
126+
```typescript
127+
// 类型定义
128+
interface TableMeta {
129+
fuzzySearch: boolean
130+
onEdit: (item: InventoryItem) => void
131+
onBorrowSuccess: () => void
132+
}
133+
134+
// 使用
135+
const table = useReactTable({
136+
data,
137+
columns,
138+
meta: {
139+
fuzzySearch,
140+
onEdit: handleEditClick,
141+
onBorrowSuccess: loadInventory,
142+
}
143+
})
144+
145+
// 列定义 - 依赖项清空
146+
const columns = useMemo(() => [
147+
columnHelper.accessor('name', {
148+
cell: info => {
149+
const filterValue = info.table.getState().globalFilter
150+
const isFuzzy = info.table.options.meta?.fuzzySearch
151+
return <HighlightText text={info.getValue()} highlight={filterValue} fuzzy={isFuzzy} />
152+
},
153+
}),
154+
], []) // 空依赖数组
155+
```
156+
157+
---
158+
159+
### 2.2 移除冗余状态(优先级:P1)
160+
161+
**目标**:删除无用的 `displayFilter` 状态
162+
163+
**问题根因**:
164+
- `displayFilter` 只是简单复制 `globalFilter`,没有任何实际作用
165+
- 每次输入触发两次状态更新和重渲染
166+
167+
**影响程度**:100%(每次搜索都触发额外渲染)
168+
169+
**方案**:删除 `displayFilter`,直接使用 `globalFilter`
170+
171+
```typescript
172+
// 删除这两行
173+
const [displayFilter, setDisplayFilter] = useState('')
174+
useEffect(() => {
175+
setDisplayFilter(globalFilter)
176+
}, [globalFilter])
177+
178+
// 在 columns 中直接使用 table.getState().globalFilter
179+
```
180+
181+
---
182+
183+
### 2.3 表单配置抽离(优先级:P1)
184+
185+
**目标**:消除 editForm 和 addForm 的字段配置重复
186+
187+
**问题根因**:
188+
- 两个表单的字段配置几乎相同
189+
- 添加一个字段需要改4个地方
190+
191+
**影响程度**:100%(每次维护都遇到)
192+
193+
**方案**:抽离表单字段工厂函数
194+
195+
```typescript
196+
// 定义在组件外部
197+
const INVENTORY_FORM_FIELDS = {
198+
common: [
199+
{ name: 'name', label: '试剂名称', type: 'input', required: true, colSpan: 2 },
200+
{ name: 'english_name', label: '英文名称', type: 'input', colSpan: 2 },
201+
{ name: 'alias', label: '别名', type: 'input' },
202+
{ name: 'storage_location', label: '存放位置', type: 'input' },
203+
{ name: 'brand', label: '品牌', type: 'input' },
204+
{ name: 'category', label: '分类', type: 'input' },
205+
{ name: 'is_hazardous', label: '危险品', type: 'checkbox' },
206+
{ name: 'notes', label: '备注', type: 'textarea', colSpan: 3 },
207+
],
208+
edit: [
209+
{ name: 'cas_number', label: 'CAS号', type: 'input', readOnly: true, colSpan: 1 },
210+
{ name: 'remaining_quantity', label: '剩余量', type: 'number', required: true },
211+
{ name: 'specification', label: '规格', type: 'input', required: true },
212+
],
213+
add: [
214+
{ name: 'cas_number', label: 'CAS号', type: 'input', required: true, colSpan: 1 },
215+
{ name: 'specification', label: '规格', type: 'input', required: true },
216+
{ name: 'quantity_bottles', label: '瓶数', type: 'number', required: true },
217+
],
218+
}
219+
220+
function getFormFields(mode: 'edit' | 'add') {
221+
return [
222+
...INVENTORY_FORM_FIELDS.common,
223+
...INVENTORY_FORM_FIELDS[mode],
224+
]
225+
}
226+
```
227+
228+
---
229+
230+
### 2.4 LoadingButton 修复(优先级:P2)
231+
232+
**目标**:修复状态切换时的颜色跳变问题
233+
234+
**问题根因**:
235+
- `isLoading` 时未强制锁定 disabled 样式
236+
237+
**影响程度**:100%(每次点击借用都发生)
238+
239+
**方案**:强制锁定 disabled 样式
240+
241+
```typescript
242+
className={cn(
243+
"h-8 text-sm/4 px-3 border-0 transition-none",
244+
isConfirming
245+
? isLoading
246+
? "bg-destructive/70 text-destructive-foreground cursor-wait disabled:bg-destructive/70 disabled:text-destructive-foreground disabled:opacity-100"
247+
: "bg-destructive text-destructive-foreground hover:bg-destructive/70"
248+
: "bg-primary hover:bg-primary/80"
249+
)}
250+
```
251+
252+
---
253+
254+
### 2.5 游标分页改造(优先级:未来优化项)
255+
256+
**状态**:⚠️ **文档与代码不一致**
257+
258+
- 文档 [`docs/done/[DONE]-inventory_performance_optimization.md`](docs/done/[DONE]-inventory_performance_optimization.md) 声称"后端游标分页 API ✅ 已完成"
259+
- 实际代码:后端 API 仍使用 `skip`/`limit`,未实现 `cursor` 参数
260+
261+
**影响分析**:
262+
- 发生概率:< 1%(实验室小规模并发极其罕见)
263+
- 影响程度:中(滚动时偶现重复或遗漏数据)
264+
- 修复成本:高(前后端联动重构)
265+
266+
**结论**:维持 Offset 分页,暂不需要改造
267+
268+
---
269+
270+
## 三、实施优先级(最终版)
271+
272+
| 优先级 | 改进项 | 发生概率 | 影响程度 | 修复成本 |
273+
|--------|--------|----------|----------|----------|
274+
| **P0** | TanStack Table 列定义重构 | 100% | 高(输入卡顿) | 低 |
275+
| **P1** | 提取通用表单字段配置 | 100% | 中(可维护性) | 低 |
276+
| **P2** | 修复 LoadingButton 状态跳变 | 100% | 低(视觉) | 极低 |
277+
| **未来优化项** | 游标分页改造 | < 1% | 中 | 高 |
278+
279+
---
280+
281+
## 四、总结
282+
283+
当前 Inventory.tsx 代码质量总体良好,主要性能瓶颈在表格列定义的依赖项过多。核心改进方向:
284+
285+
1. **P0 - 立即可做**:优化 columns 依赖,解决搜索卡顿
286+
2. **P1 - 建议做**:表单配置抽离 + 移除冗余状态
287+
3. **P2 - 顺手修**:LoadingButton 颜色修复
288+
4. **未来优化项**:游标分页,当前方案已满足需求

0 commit comments

Comments
 (0)