|
@@ -0,0 +1,361 @@
|
|
|
+# 注册表单状态清理 - Bug分析与修复
|
|
|
+
|
|
|
+## Bug 描述
|
|
|
+
|
|
|
+**问题场景**:
|
|
|
+
|
|
|
+1. 第一次填写完整的患者信息并注册成功
|
|
|
+2. 表单UI清空,但内部状态未清空
|
|
|
+3. 第二次不填写任何信息,直接选择体位后点击注册
|
|
|
+4. ❌ Bug:仍然可以注册成功(应该失败)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 根本原因分析
|
|
|
+
|
|
|
+### 涉及的领域模型概念
|
|
|
+
|
|
|
+这个bug涉及到**两个独立的状态管理域**:
|
|
|
+
|
|
|
+#### 1. Ant Design Form State(组件级状态)
|
|
|
+
|
|
|
+```typescript
|
|
|
+const [form] = Form.useForm();
|
|
|
+// 内部维护表单字段值
|
|
|
+// 生命周期:从组件创建到手动重置
|
|
|
+// 清理方式:form.resetFields()
|
|
|
+```
|
|
|
+
|
|
|
+**特性**:
|
|
|
+
|
|
|
+- 状态持久化:即使UI不显示,内部store仍然保留数据
|
|
|
+- 独立管理:不受Redux状态影响
|
|
|
+- 需要显式清理:必须调用`form.resetFields()`
|
|
|
+
|
|
|
+#### 2. Redux formSlice State(全局状态)
|
|
|
+
|
|
|
+```typescript
|
|
|
+interface FormState {
|
|
|
+ formData: Record<string, any>;
|
|
|
+}
|
|
|
+// 存储表单数据的副本
|
|
|
+// 用于跨组件共享
|
|
|
+```
|
|
|
+
|
|
|
+**特性**:
|
|
|
+
|
|
|
+- 全局可访问
|
|
|
+- 需要通过action更新
|
|
|
+- 需要显式清理action
|
|
|
+
|
|
|
+### 因果链条(清晰明确)
|
|
|
+
|
|
|
+```
|
|
|
+第一次注册成功
|
|
|
+ ↓
|
|
|
+触发事件:registerWork API 返回 code === '0x000000'
|
|
|
+ ↓
|
|
|
+当前行为(Bug原因):
|
|
|
+ - ✅ Redux viewSelection.selectedViews 被清空
|
|
|
+ - ❌ Redux formSlice.formData 未清空
|
|
|
+ - ❌ Ant Design Form 内部状态未清空
|
|
|
+ ↓
|
|
|
+状态不一致:
|
|
|
+ - UI层:表单显示为空(用户视角)
|
|
|
+ - 数据层:form.getFieldsValue() 返回旧数据(系统视角)
|
|
|
+ ↓
|
|
|
+第二次注册
|
|
|
+ ↓
|
|
|
+用户操作:只选择体位,不填表单
|
|
|
+ ↓
|
|
|
+点击"注册"按钮
|
|
|
+ ↓
|
|
|
+handleRegister() 执行:
|
|
|
+ 1. let values = form.getFieldsValue();
|
|
|
+ // ❌ 返回第一次的完整数据(用户不知道)
|
|
|
+ 2. validateResult = schema.safeParse(values);
|
|
|
+ // ✅ 验证通过(因为values有完整数据)
|
|
|
+ 3. 调用 registerWork(registerInfo);
|
|
|
+ // ❌ 使用旧数据注册成功
|
|
|
+ ↓
|
|
|
+Bug结果:用户以为表单是空的,实际用旧数据注册成功
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 修复方案
|
|
|
+
|
|
|
+### 方案概述
|
|
|
+
|
|
|
+在两个关键时机清理**双重状态**:
|
|
|
+
|
|
|
+1. **清理时机1**:注册成功后(最重要)
|
|
|
+2. **清理时机3**:组件卸载时(防御性)
|
|
|
+
|
|
|
+### 实现细节
|
|
|
+
|
|
|
+#### 修改1:formSlice 添加清理action
|
|
|
+
|
|
|
+**文件**:`src/states/patient/register/formSlice.ts`
|
|
|
+
|
|
|
+```typescript
|
|
|
+const formSlice = createSlice({
|
|
|
+ name: 'form',
|
|
|
+ initialState,
|
|
|
+ reducers: {
|
|
|
+ setFormData: (state, action) => {
|
|
|
+ state.formData = action.payload;
|
|
|
+ },
|
|
|
+ // ✅ 新增:清空表单数据
|
|
|
+ clearFormData: (state) => {
|
|
|
+ state.formData = {};
|
|
|
+ console.log('Redux formSlice: 表单数据已清空');
|
|
|
+ },
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+export const { setFormData, clearFormData } = formSlice.actions;
|
|
|
+```
|
|
|
+
|
|
|
+**领域概念**:
|
|
|
+
|
|
|
+- **Action**:clearFormData - 表达"清空表单"的业务意图
|
|
|
+- **Reducer**:将状态重置为初始值 `{}`
|
|
|
+- **日志**:记录状态变化,便于调试
|
|
|
+
|
|
|
+#### 修改2:注册成功后清理(清理时机1)
|
|
|
+
|
|
|
+**文件**:`src/pages/patient/Register.tsx`
|
|
|
+
|
|
|
+```typescript
|
|
|
+const response = await registerWork(registerInfo);
|
|
|
+if (response.code !== '0x000000') {
|
|
|
+ message.error(`注册失败: ${response.description}`);
|
|
|
+ return { success: false, views: [] };
|
|
|
+}
|
|
|
+
|
|
|
+// ✅ 注册成功后的三重清理
|
|
|
+// 1. 清理 Redux formSlice
|
|
|
+dispatch(clearFormData());
|
|
|
+
|
|
|
+// 2. 清理 Ant Design Form
|
|
|
+form.resetFields();
|
|
|
+
|
|
|
+// 3. selectedViews 已在 viewSelectionSlice 中自动清空
|
|
|
+console.log('注册成功,表单已清空,可以开始新的注册');
|
|
|
+
|
|
|
+return { success: true, data: response.data, views: selectedViews };
|
|
|
+```
|
|
|
+
|
|
|
+**因果关系**:
|
|
|
+
|
|
|
+```
|
|
|
+API返回成功
|
|
|
+ ↓
|
|
|
+触发清理三重奏:
|
|
|
+ 1. dispatch(clearFormData()) → Redux formSlice.formData = {}
|
|
|
+ 2. form.resetFields() → Ant Design Form 内部状态清空
|
|
|
+ 3. selectedViews自动清空 → 已在viewSelectionSlice实现
|
|
|
+ ↓
|
|
|
+系统恢复初始状态
|
|
|
+ ↓
|
|
|
+用户可以开始新的注册流程 ✅
|
|
|
+```
|
|
|
+
|
|
|
+#### 修改3:组件卸载时清理(清理时机3)
|
|
|
+
|
|
|
+**文件**:`src/pages/patient/Register.tsx`
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 清理时机3:组件卸载时清理表单
|
|
|
+useEffect(() => {
|
|
|
+ return () => {
|
|
|
+ // 组件卸载时清理
|
|
|
+ dispatch(clearFormData());
|
|
|
+ form.resetFields();
|
|
|
+ console.log('注册页面已卸载,表单数据已清空');
|
|
|
+ };
|
|
|
+}, [dispatch, form]);
|
|
|
+```
|
|
|
+
|
|
|
+**领域概念**:
|
|
|
+
|
|
|
+- **React生命周期**:useEffect cleanup函数
|
|
|
+- **防御性编程**:即使用户没有完成注册就离开页面,也确保状态被清理
|
|
|
+- **避免数据泄漏**:用户再次进入注册页面时,不会看到旧数据
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 修复后的完整流程
|
|
|
+
|
|
|
+### 正常注册流程
|
|
|
+
|
|
|
+```
|
|
|
+用户进入注册页面
|
|
|
+ ↓
|
|
|
+填写患者信息
|
|
|
+ ↓
|
|
|
+ 每次字段变化 → dispatch(setFormData(allValues))
|
|
|
+ ↓
|
|
|
+选择体位
|
|
|
+ ↓
|
|
|
+ 点击体位 → dispatch(addSelectedView(view))
|
|
|
+ ↓
|
|
|
+点击"注册"按钮
|
|
|
+ ↓
|
|
|
+handleRegister():
|
|
|
+ 1. 获取表单值:form.getFieldsValue()
|
|
|
+ 2. 验证表单:schema.safeParse(values)
|
|
|
+ 3. 验证通过 → 调用API:registerWork(registerInfo)
|
|
|
+ ↓
|
|
|
+API返回成功
|
|
|
+ ↓
|
|
|
+【三重清理】
|
|
|
+ 1. dispatch(clearFormData())
|
|
|
+ → formSlice.formData = {}
|
|
|
+ 2. form.resetFields()
|
|
|
+ → Form内部状态清空
|
|
|
+ 3. selectedViews自动清空
|
|
|
+ → viewSelection.selectedViews = []
|
|
|
+ ↓
|
|
|
+显示成功消息
|
|
|
+ ↓
|
|
|
+用户可以开始新的注册 ✅
|
|
|
+```
|
|
|
+
|
|
|
+### 用户离开页面流程
|
|
|
+
|
|
|
+```
|
|
|
+用户填写部分信息
|
|
|
+ ↓
|
|
|
+未完成注册,切换到其他页面
|
|
|
+ ↓
|
|
|
+RegisterPage组件卸载
|
|
|
+ ↓
|
|
|
+useEffect cleanup函数执行
|
|
|
+ ↓
|
|
|
+【清理】
|
|
|
+ 1. dispatch(clearFormData())
|
|
|
+ 2. form.resetFields()
|
|
|
+ ↓
|
|
|
+用户再次进入注册页面
|
|
|
+ ↓
|
|
|
+看到的是干净的空表单 ✅
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 状态管理架构图
|
|
|
+
|
|
|
+### 修复前(有Bug)
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────────┐
|
|
|
+│ 注册成功后的状态 │
|
|
|
+├─────────────────────────────────────────────┤
|
|
|
+│ Redux viewSelection.selectedViews: [] ✅ │
|
|
|
+│ Redux formSlice.formData: {...旧数据} ❌ │
|
|
|
+│ Ant Design Form State: {...旧数据} ❌ │
|
|
|
+└─────────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+ 状态不一致!
|
|
|
+ ↓
|
|
|
+ 第二次注册时Bug
|
|
|
+```
|
|
|
+
|
|
|
+### 修复后(正常)
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────────┐
|
|
|
+│ 注册成功后的状态 │
|
|
|
+├─────────────────────────────────────────────┤
|
|
|
+│ Redux viewSelection.selectedViews: [] ✅ │
|
|
|
+│ Redux formSlice.formData: {} ✅ │
|
|
|
+│ Ant Design Form State: {} ✅ │
|
|
|
+└─────────────────────────────────────────────┘
|
|
|
+ ↓
|
|
|
+ 状态一致!
|
|
|
+ ↓
|
|
|
+ 系统恢复初始状态
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 关键概念总结
|
|
|
+
|
|
|
+### 1. 双重状态管理
|
|
|
+
|
|
|
+- **Form State**(组件级):负责UI交互
|
|
|
+- **Redux State**(全局级):负责跨组件共享
|
|
|
+
|
|
|
+### 2. 状态同步
|
|
|
+
|
|
|
+- 数据流入:`onValuesChange` → `dispatch(setFormData)`
|
|
|
+- 数据清理:必须同时清理两个状态
|
|
|
+
|
|
|
+### 3. 清理时机
|
|
|
+
|
|
|
+- **时机1**:业务成功后(注册成功)
|
|
|
+- **时机3**:组件卸载时(防御性)
|
|
|
+
|
|
|
+### 4. 因果关系
|
|
|
+
|
|
|
+```
|
|
|
+用户操作 → 状态变化 → 副作用处理 → UI更新
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 测试验证要点
|
|
|
+
|
|
|
+### 手动测试
|
|
|
+
|
|
|
+1. **测试场景1**:连续注册两次
|
|
|
+
|
|
|
+ - 第一次:填写完整信息 → 注册成功
|
|
|
+ - 验证:表单已清空(UI和数据层都清空)
|
|
|
+ - 第二次:不填表单 → 点击注册
|
|
|
+ - 预期:提示"必填项未填写"
|
|
|
+
|
|
|
+2. **测试场景2**:填写后离开页面
|
|
|
+ - 填写部分信息
|
|
|
+ - 切换到其他页面
|
|
|
+ - 再次返回注册页面
|
|
|
+ - 预期:看到空表单
|
|
|
+
|
|
|
+### 验证点
|
|
|
+
|
|
|
+- [ ] 注册成功后,`form.getFieldsValue()` 返回空对象
|
|
|
+- [ ] 注册成功后,Redux `formSlice.formData` 为空对象
|
|
|
+- [ ] 注册成功后,Redux `selectedViews` 为空数组
|
|
|
+- [ ] 组件卸载后再进入,表单显示为空
|
|
|
+- [ ] 控制台日志正确显示清理信息
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 相关文件清单
|
|
|
+
|
|
|
+### 已修改文件
|
|
|
+
|
|
|
+1. `src/states/patient/register/formSlice.ts`
|
|
|
+
|
|
|
+ - 添加 `clearFormData` action
|
|
|
+
|
|
|
+2. `src/pages/patient/Register.tsx`
|
|
|
+ - 导入 `clearFormData`, `useEffect`
|
|
|
+ - 实现清理时机1:注册成功后
|
|
|
+ - 实现清理时机3:组件卸载时
|
|
|
+
|
|
|
+### 相关参考文件
|
|
|
+
|
|
|
+- `src/states/patient/viewSelection/index.ts`(selectedViews的清理)
|
|
|
+- `src/hooks/useRegisterState.ts`(状态获取)
|
|
|
+- `src/validation/patient/registerSchema.ts`(验证逻辑)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 更新记录
|
|
|
+
|
|
|
+| 日期 | 修改人 | 修改内容 |
|
|
|
+| --------- | ------ | --------------------------- |
|
|
|
+| 2025/10/8 | - | Bug分析、修复实现、文档创建 |
|