在患者注册流程中,用户需要填写患者的年龄和出生日期。这两个字段存在逻辑关联,当用户修改其中一个字段时,另一个字段应该自动计算并更新。
双向联动关系:
年龄 → 出生日期
出生日期 → 年龄
formSlice
- 表单数据切片位置: src/states/patient/register/formSlice.ts
State 结构:
interface FormState {
formData: Record<string, any>;
}
关键字段:
patient_age
: { number: number, unit: 'D' | 'M' | 'Y' }
- 患者年龄patient_dob
: Dayjs
- 患者出生日期关键 Action:
setFormData(payload: Record<string, any>)
- 更新整个表单数据对象用户修改年龄/出生日期
↓
Form onChange 触发
↓
组件内部联动逻辑
├─→ 计算关联字段值
└─→ form.setFieldsValue() 更新表单
↓
onValuesChange 回调触发
↓
dispatch(setFormData(allValues))
↓
Redux store.form.formData 更新
假设我们这样实现联动:
// ❌ 错误示例 - 会导致无限循环
const patient_age = Form.useWatch('patient_age', form);
const patient_dob = Form.useWatch('patient_dob', form);
useEffect(() => {
// 当年龄变化时,更新出生日期
if (patient_age) {
const newDob = calculateDobFromAge(patient_age);
form.setFieldsValue({ patient_dob: newDob });
}
}, [patient_age]);
useEffect(() => {
// 当出生日期变化时,更新年龄
if (patient_dob) {
const newAge = calculateAgeFromDob(patient_dob);
form.setFieldsValue({ patient_age: newAge });
}
}, [patient_dob]);
循环过程:
patient_age
变化patient_dob
patient_dob
变化 → 触发第二个 useEffectpatient_age
patient_age
变化 → 又触发第一个 useEffectuseRef
有两个关键特性适合解决这个问题:
这使得 ref 非常适合作为"标志位"来存储状态信息。
const BasicInfoForm: React.FC<BasicInfoFormProps> = ({ form, ... }) => {
// 使用 ref 记录是谁触发的更新
const updateSourceRef = useRef<'age' | 'dob' | null>(null);
const patient_age = Form.useWatch('patient_age', form);
const patient_dob = Form.useWatch('patient_dob', form);
// 监听年龄变化
useEffect(() => {
// 如果是出生日期联动更新的年龄,跳过
if (updateSourceRef.current === 'dob') {
updateSourceRef.current = null; // 重置标志
return;
}
if (patient_age) {
// 标记本次更新来自年龄变化
updateSourceRef.current = 'age';
const newDob = calculateDobFromAge(patient_age);
form.setFieldsValue({ patient_dob: newDob });
}
}, [patient_age]);
// 监听出生日期变化
useEffect(() => {
// 如果是年龄联动更新的出生日期,跳过
if (updateSourceRef.current === 'age') {
updateSourceRef.current = null; // 重置标志
return;
}
if (patient_dob) {
// 标记本次更新来自出生日期变化
updateSourceRef.current = 'dob';
const newAge = calculateAgeFromDob(patient_dob);
form.setFieldsValue({ patient_age: newAge });
}
}, [patient_dob]);
return (
<Form form={form}>
{/* 表单字段 */}
</Form>
);
};
场景 1:用户修改年龄
1. 用户输入年龄 25Y
2. patient_age 变化 → 触发年龄 useEffect
3. updateSourceRef.current === null → 检查通过,继续执行
4. updateSourceRef.current = 'age' → 标记更新源为年龄
5. form.setFieldsValue({ patient_dob: ... }) → 设置出生日期
6. patient_dob 变化 → 触发出生日期 useEffect
7. updateSourceRef.current === 'age' → 检查不通过,直接 return
8. updateSourceRef.current = null → 重置标志
9. ✅ 完成,没有循环
场景 2:用户修改出生日期
1. 用户选择出生日期 2000-01-01
2. patient_dob 变化 → 触发出生日期 useEffect
3. updateSourceRef.current === null → 检查通过,继续执行
4. updateSourceRef.current = 'dob' → 标记更新源为出生日期
5. form.setFieldsValue({ patient_age: ... }) → 设置年龄
6. patient_age 变化 → 触发年龄 useEffect
7. updateSourceRef.current === 'dob' → 检查不通过,直接 return
8. updateSourceRef.current = null → 重置标志
9. ✅ 完成,没有循环
为什么 useRef 能解决问题?
关键点:
updateSourceRef.current = 'A'
updateSourceRef.current = null
,为下次用户操作做准备/**
* 根据年龄计算出生日期
* @param age - 年龄对象 { number: number, unit: 'D' | 'M' | 'Y' }
* @returns Dayjs对象表示的出生日期
*/
function calculateDobFromAge(age: {
number: number;
unit: 'D' | 'M' | 'Y';
}): Dayjs {
const now = dayjs();
switch (age.unit) {
case 'D':
return now.subtract(age.number, 'day');
case 'M':
return now.subtract(age.number, 'month');
case 'Y':
return now.subtract(age.number, 'year');
default:
return now;
}
}
示例:
{ number: 25, unit: 'Y' }
(25岁)/**
* 根据出生日期计算年龄
* @param dob - Dayjs对象表示的出生日期
* @returns 年龄对象 { number: number, unit: 'D' | 'M' | 'Y' }
*/
function calculateAgeFromDob(dob: Dayjs): {
number: number;
unit: 'D' | 'M' | 'Y';
} {
const now = dayjs();
// 计算年份差
const years = now.diff(dob, 'year');
if (years >= 1) {
return { number: years, unit: 'Y' };
}
// 计算月份差
const months = now.diff(dob, 'month');
if (months >= 1) {
return { number: months, unit: 'M' };
}
// 计算天数差
const days = now.diff(dob, 'day');
return { number: Math.max(0, days), unit: 'D' };
}
示例:
{ number: 24, unit: 'Y' }
(24岁)负数年龄:出生日期在未来
空值处理:
初始化时的联动:
文件: src/pages/patient/components/register.form.tsx
import React, { useEffect, useRef } from 'react';
import dayjs, { Dayjs } from 'dayjs';
const BasicInfoForm: React.FC<BasicInfoFormProps> = ({ form, ... }) => {
// 使用 ref 记录更新源,防止循环触发
const updateSourceRef = useRef<'age' | 'dob' | null>(null);
// ... 其他代码
};
// 根据年龄计算出生日期
const calculateDobFromAge = (age: {
number: number;
unit: 'D' | 'M' | 'Y';
}): Dayjs => {
const now = dayjs();
switch (age.unit) {
case 'D':
return now.subtract(age.number, 'day');
case 'M':
return now.subtract(age.number, 'month');
case 'Y':
return now.subtract(age.number, 'year');
default:
return now;
}
};
// 根据出生日期计算年龄
const calculateAgeFromDob = (
dob: Dayjs
): { number: number; unit: 'D' | 'M' | 'Y' } => {
const now = dayjs();
const years = now.diff(dob, 'year');
if (years >= 1) {
return { number: years, unit: 'Y' };
}
const months = now.diff(dob, 'month');
if (months >= 1) {
return { number: months, unit: 'M' };
}
const days = now.diff(dob, 'day');
return { number: Math.max(0, days), unit: 'D' };
};
const patient_age = Form.useWatch('patient_age', form);
useEffect(() => {
// 如果是出生日期联动更新的年龄,跳过
if (updateSourceRef.current === 'dob') {
updateSourceRef.current = null;
return;
}
// 如果年龄有效,计算出生日期
if (patient_age && patient_age.number >= 0) {
updateSourceRef.current = 'age';
const newDob = calculateDobFromAge(patient_age);
form.setFieldsValue({ patient_dob: newDob });
console.log('年龄变化,更新出生日期:', newDob.format('YYYY-MM-DD'));
}
}, [patient_age, form]);
const patient_dob = Form.useWatch('patient_dob', form);
useEffect(() => {
// 如果是年龄联动更新的出生日期,跳过
if (updateSourceRef.current === 'age') {
updateSourceRef.current = null;
return;
}
// 如果出生日期有效,计算年龄
if (patient_dob && dayjs.isDayjs(patient_dob)) {
updateSourceRef.current = 'dob';
const newAge = calculateAgeFromDob(patient_dob);
form.setFieldsValue({ patient_age: newAge });
console.log('出生日期变化,更新年龄:', newAge);
}
}, [patient_dob, form]);
cypress/e2e/patient/age-dob-sync.cy.ts
Given(前置条件):
When(操作步骤):
Then(预期结果):
patient_dob
已更新Given(前置条件):
When(操作步骤):
Then(预期结果):
patient_age
已更新Given(前置条件):
When(操作步骤):
Then(预期结果):
Given(前置条件):
When(操作步骤):
Then(预期结果):
Given(前置条件):
When(操作步骤):
Then(预期结果):
Given(前置条件):
When(操作步骤):
Then(预期结果):
Given(前置条件):
When(操作步骤):
Then(预期结果):
src/pages/patient/components/register.form.tsx
- 添加联动逻辑cypress/e2e/patient/age-dob-sync.cy.ts
- E2E 测试src/states/patient/register/formSlice.ts
- 表单状态管理src/domain/patient/registerLogic.ts
- 注册逻辑(使用表单数据)src/validation/patient/registerSchema.ts
- 表单验证规则确保使用的 dayjs
版本支持 diff
和 subtract
方法。
出生日期的计算应该考虑时区问题,建议统一使用 UTC 时间。
智能单位选择:
年龄范围限制:
精确到时分秒:
const useAgeDobSync = (form: FormInstance) => {
// 将联动逻辑封装为独立的 Hook
};
日期 | 修改人 | 修改内容 |
---|---|---|
2025/10/7 | - | 创建文档,详细说明年龄和出生日期联动的实现 |