Parcourir la source

fix: 修复年龄输入框输入验证问题并实现实时输入拦截

- 在 NumberWithUnit.tsx 中添加实时输入拦截机制
  - 新增 onKeyDown 处理器阻止非数字字符键盘输入
  - 新增 onPaste 处理器过滤粘贴内容中的非数字字符
  - 新增 onCompositionStart/End 处理器支持中文输入法
  - 添加 isComposing 状态管理避免干扰输入法
  - 配置 min={0}、precision={0}、parser 等基础属性

- 在 registerSchema.ts 中增强年龄字段验证规则
  - 添加自定义验证器检查有效数字
  - 验证年龄范围(0-200)
  - 验证必须为整数
  - 提供中文错误提示信息

- 创建完整测试文档 docs/测试/年龄输入框修复测试方案.md
  - 包含40个测试用例,覆盖11个测试分类
  - 新增9个实时拦截测试用例(v1.1)
  - 提供详细的测试步骤和预期结果

- 更新 package.json 版本号至 1.1.1

改动文件:
- src/components/NumberWithUnit.tsx
- src/validation/patient/registerSchema.ts
- docs/测试/年龄输入框修复测试方案.md
- package.json
dengdx il y a 5 jours
Parent
commit
44f38e3a96

+ 738 - 0
docs/测试/年龄输入框修复测试方案.md

@@ -0,0 +1,738 @@
+# 年龄输入框修复测试方案
+
+## 修复概述
+
+针对 `src/pages/patient/components/register.form.tsx` 中年龄输入框的两个问题进行修复:
+1. 能调节到负值
+2. 能输入任意字符
+
+## 修复内容
+
+### 1. NumberWithUnit 组件修改
+**文件**: `src/components/NumberWithUnit.tsx`
+
+**修改点**:
+
+#### 基础属性配置
+- 添加 `min={0}` - 防止负值
+- 添加 `precision={0}` - 只允许整数
+- 添加 `parser={(value) => value?.replace(/\D/g, '') || ''}` - 作为兜底过滤机制
+- 添加 `keyboard={true}` - 确保键盘输入正常
+
+#### 实时输入拦截(v1.1 新增)
+- **onKeyDown 处理器** - 在键盘输入层面拦截非数字字符
+  - 阻止字母、特殊字符等非数字按键
+  - 允许功能键(退格、删除、方向键等)
+  - 允许 Ctrl/Cmd 组合键(复制、粘贴、全选等)
+  - 输入法激活时不拦截(避免干扰中文输入)
+
+- **onPaste 处理器** - 处理粘贴操作
+  - 拦截粘贴事件
+  - 自动过滤粘贴内容中的非数字字符
+  - 只保留数字部分并更新到输入框
+
+- **onCompositionStart/End 处理器** - 处理输入法输入
+  - 检测输入法激活状态
+  - 在输入法确认输入后立即过滤非数字字符
+  - 完美支持中文输入法等复杂输入场景
+
+**实现效果**:
+- ✅ 用户按下非数字键时,字符不会出现在输入框中
+- ✅ 粘贴包含非数字字符的内容时,自动过滤并只保留数字
+- ✅ 使用输入法输入中文后,确认输入时立即过滤
+- ✅ parser 作为最后防线,处理可能遗漏的特殊情况
+
+### 2. 表单验证规则增强
+**文件**: `src/validation/patient/registerSchema.ts`
+
+**修改点**:
+添加自定义验证器:
+- 检查是否为有效数字
+- 检查是否为负数
+- 检查是否超过200(合理范围)
+- 检查是否为整数
+
+## 测试用例
+
+### 测试环境准备
+
+1. 启动应用程序
+2. 进入患者注册页面
+3. 定位到"年龄"输入框
+
+---
+
+### 分类1:负值输入测试
+
+#### TC-AGE-001: 点击减号按钮测试
+**前置条件**: 年龄输入框值为 0
+
+**测试步骤**:
+1. 点击年龄输入框的减号按钮多次
+
+**预期结果**:
+- ✅ 数值保持为 0,不会变成负数
+- ✅ 减号按钮应该被禁用或点击无效
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-002: 直接输入负数测试
+**测试步骤**:
+1. 清空年龄输入框
+2. 直接输入 "-5"
+3. 点击输入框外部(触发 blur 事件)
+
+**预期结果**:
+- ✅ 输入框自动过滤 "-" 符号,只显示 "5"
+- ✅ 或者输入框为空(如果只输入了"-")
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-003: 通过键盘快捷键输入负数
+**测试步骤**:
+1. 聚焦到年龄输入框
+2. 按下 "负号" 或 "减号" 键
+
+**预期结果**:
+- ✅ 负号字符不会被输入到输入框中
+- ✅ 输入框保持当前值不变
+
+**优先级**: P1 (中)
+
+---
+
+### 分类2:非法字符输入测试
+
+#### TC-AGE-004: 输入字母测试
+**测试步骤**:
+1. 清空年龄输入框
+2. 依次输入: "a", "B", "xyz"
+3. 观察输入框内容
+
+**预期结果**:
+- ✅ 所有字母字符都被过滤,输入框保持为空或显示之前的有效数字
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-005: 输入特殊字符测试
+**测试步骤**:
+1. 清空年龄输入框
+2. 依次尝试输入: "!", "@", "#", "$", "%", "^", "&", "*", "(", ")"
+3. 观察输入框内容
+
+**预期结果**:
+- ✅ 所有特殊字符都被过滤,输入框保持为空或显示之前的有效数字
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-006: 输入中文字符测试
+**测试步骤**:
+1. 清空年龄输入框
+2. 尝试输入中文: "一", "二", "三", "年龄"
+3. 观察输入框内容
+
+**预期结果**:
+- ✅ 所有中文字符都被过滤,输入框保持为空或显示之前的有效数字
+
+**优先级**: P1 (中)
+
+---
+
+#### TC-AGE-007: 输入小数点测试
+**测试步骤**:
+1. 在年龄输入框中输入 "10"
+2. 尝试输入小数点 ".",完整输入 "10.5"
+3. 点击输入框外部(触发 blur 事件)
+
+**预期结果**:
+- ✅ 小数点及小数部分被自动移除
+- ✅ 输入框显示 "10" 或 "105"(取决于 parser 实现)
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-008: 混合输入测试
+**测试步骤**:
+1. 清空年龄输入框
+2. 输入混合内容: "12abc34#@!56"
+3. 观察输入框内容
+
+**预期结果**:
+- ✅ 输入框只显示数字部分: "123456"
+- ✅ 所有非数字字符都被过滤
+
+**优先级**: P1 (中)
+
+---
+
+### 分类3:边界值测试
+
+#### TC-AGE-009: 最小值边界测试
+**测试步骤**:
+1. 在年龄输入框中输入 "0"
+2. 选择单位为 "年"
+3. 点击输入框外部(触发验证)
+
+**预期结果**:
+- ✅ 输入框接受 0 作为有效值
+- ✅ 不显示任何验证错误
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-010: 最大值边界测试
+**测试步骤**:
+1. 在年龄输入框中输入 "200"
+2. 选择单位为 "年"
+3. 点击输入框外部(触发验证)
+
+**预期结果**:
+- ✅ 输入框接受 200 作为有效值
+- ✅ 不显示任何验证错误
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-011: 超过最大值测试
+**测试步骤**:
+1. 在年龄输入框中输入 "201"
+2. 选择单位为 "年"
+3. 点击输入框外部(触发验证)
+
+**预期结果**:
+- ✅ 显示验证错误: "年龄超出合理范围(最大200)"
+- ✅ 表单提交被阻止
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-012: 极大值测试
+**测试步骤**:
+1. 在年龄输入框中输入 "999999"
+2. 选择单位为 "年"
+3. 点击输入框外部(触发验证)
+
+**预期结果**:
+- ✅ 显示验证错误: "年龄超出合理范围(最大200)"
+- ✅ 表单提交被阻止
+
+**优先级**: P1 (中)
+
+---
+
+### 分类4:不同单位测试
+
+#### TC-AGE-013: 年单位有效值测试
+**测试步骤**:
+1. 在年龄输入框中输入 "25"
+2. 选择单位为 "年"
+3. 点击输入框外部
+
+**预期结果**:
+- ✅ 输入框显示 "25"
+- ✅ 单位显示 "年"
+- ✅ 不显示任何验证错误
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-014: 月单位有效值测试
+**测试步骤**:
+1. 在年龄输入框中输入 "18"
+2. 选择单位为 "月"
+3. 点击输入框外部
+
+**预期结果**:
+- ✅ 输入框显示 "18"
+- ✅ 单位显示 "月"
+- ✅ 不显示任何验证错误
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-015: 天单位有效值测试
+**测试步骤**:
+1. 在年龄输入框中输入 "30"
+2. 选择单位为 "天"
+3. 点击输入框外部
+
+**预期结果**:
+- ✅ 输入框显示 "30"
+- ✅ 单位显示 "天"
+- ✅ 不显示任何验证错误
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-016: 切换单位后保持值测试
+**测试步骤**:
+1. 在年龄输入框中输入 "25"
+2. 选择单位为 "年"
+3. 切换单位为 "月"
+4. 观察输入框的值
+
+**预期结果**:
+- ✅ 数字值保持为 "25"
+- ✅ 单位更新为 "月"
+- ✅ 出生日期相应更新
+
+**优先级**: P1 (中)
+
+---
+
+### 分类5:复制粘贴测试
+
+#### TC-AGE-017: 粘贴有效数字测试
+**测试步骤**:
+1. 复制数字 "30"
+2. 在年龄输入框中粘贴(Ctrl+V 或 Cmd+V)
+3. 观察输入框内容
+
+**预期结果**:
+- ✅ 输入框显示 "30"
+- ✅ 不显示任何验证错误
+
+**优先级**: P1 (中)
+
+---
+
+#### TC-AGE-018: 粘贴负数测试
+**测试步骤**:
+1. 复制文本 "-15"
+2. 在年龄输入框中粘贴
+3. 观察输入框内容
+
+**预期结果**:
+- ✅ 负号被过滤,输入框显示 "15"
+- ✅ 或者粘贴操作被阻止
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-019: 粘贴混合内容测试
+**测试步骤**:
+1. 复制文本 "abc123xyz456"
+2. 在年龄输入框中粘贴
+3. 观察输入框内容
+
+**预期结果**:
+- ✅ 只有数字被保留,输入框显示 "123456"
+- ✅ 字母字符被自动过滤
+
+**优先级**: P1 (中)
+
+---
+
+### 分类6:键盘输入测试
+
+#### TC-AGE-020: 使用方向键调整数值
+**测试步骤**:
+1. 在年龄输入框中输入 "10"
+2. 聚焦到输入框
+3. 按下向上箭头键(↑)多次
+4. 按下向下箭头键(↓)多次
+
+**预期结果**:
+- ✅ 向上箭头增加数值
+- ✅ 向下箭头减少数值
+- ✅ 数值不会低于 0
+
+**优先级**: P1 (中)
+
+---
+
+#### TC-AGE-021: Enter 键行为测试
+**测试步骤**:
+1. 在年龄输入框中输入 "25"
+2. 按下 Enter 键
+
+**预期结果**:
+- ✅ 值被保存
+- ✅ 触发表单验证(如果需要)
+- ✅ 不提交整个表单
+
+**优先级**: P2 (低)
+
+---
+
+### 分类7:年龄与出生日期联动测试
+
+#### TC-AGE-022: 修改年龄更新出生日期
+**测试步骤**:
+1. 在年龄输入框中输入 "25"
+2. 选择单位为 "年"
+3. 观察出生日期字段
+
+**预期结果**:
+- ✅ 出生日期自动计算并更新
+- ✅ 出生日期应该是当前日期减去 25 年
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-023: 输入负数不影响出生日期
+**测试步骤**:
+1. 记录当前出生日期值
+2. 尝试在年龄输入框输入 "-10"
+3. 观察出生日期是否变化
+
+**预期结果**:
+- ✅ 负数被过滤
+- ✅ 出生日期保持不变或根据有效部分更新
+
+**优先级**: P1 (中)
+
+---
+
+### 分类8:表单验证集成测试
+
+#### TC-AGE-024: 提交空年龄表单
+**测试步骤**:
+1. 清空年龄输入框
+2. 尝试提交表单
+
+**预期结果**:
+- ✅ 显示验证错误: "请输入有效的年龄"
+- ✅ 表单提交被阻止
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-025: 提交无效年龄表单
+**测试步骤**:
+1. 在年龄输入框输入 "300"
+2. 尝试提交表单
+
+**预期结果**:
+- ✅ 显示验证错误: "年龄超出合理范围(最大200)"
+- ✅ 表单提交被阻止
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-026: 提交有效年龄表单
+**测试步骤**:
+1. 填写所有必填字段
+2. 在年龄输入框输入 "30",单位选择 "年"
+3. 提交表单
+
+**预期结果**:
+- ✅ 表单验证通过
+- ✅ 表单成功提交
+- ✅ 年龄数据正确保存
+
+**优先级**: P0 (高)
+
+---
+
+### 分类9:浏览器兼容性测试
+
+#### TC-AGE-027: Chrome 浏览器测试
+**测试步骤**: 在 Chrome 浏览器中执行 TC-AGE-001 到 TC-AGE-026
+
+**预期结果**: 所有测试用例通过
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-028: Firefox 浏览器测试
+**测试步骤**: 在 Firefox 浏览器中执行核心测试用例(TC-AGE-001, 004, 007, 018)
+
+**预期结果**: 所有测试用例通过
+
+**优先级**: P1 (中)
+
+---
+
+#### TC-AGE-029: Edge 浏览器测试
+**测试步骤**: 在 Edge 浏览器中执行核心测试用例
+
+**预期结果**: 所有测试用例通过
+
+**优先级**: P1 (中)
+
+---
+
+### 分类10:实时输入拦截测试(v1.1 新增)
+
+#### TC-AGE-032: 实时拦截字母输入测试
+**测试步骤**:
+1. 聚焦到年龄输入框
+2. 尝试按键盘输入字母 "a"
+3. 观察输入框变化
+
+**预期结果**:
+- ✅ 字母 "a" **不会出现**在输入框中(实时拦截)
+- ✅ 输入框内容保持不变
+- ✅ 没有任何闪烁或字符出现后消失的情况
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-033: 实时拦截特殊字符输入测试
+**测试步骤**:
+1. 聚焦到年龄输入框
+2. 尝试按键盘输入特殊字符 "@"、"#"、"!"
+3. 观察输入框变化
+
+**预期结果**:
+- ✅ 特殊字符**不会出现**在输入框中(实时拦截)
+- ✅ 输入框内容保持不变
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-034: 实时拦截减号输入测试
+**测试步骤**:
+1. 聚焦到年龄输入框
+2. 按键盘输入减号 "-"
+3. 观察输入框变化
+
+**预期结果**:
+- ✅ 减号**不会出现**在输入框中(实时拦截)
+- ✅ 输入框内容保持不变
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-035: 中文输入法实时过滤测试
+**测试步骤**:
+1. 聚焦到年龄输入框
+2. 切换到中文输入法
+3. 输入拼音 "nian"(年)
+4. 按空格或回车确认输入
+5. 观察输入框内容
+
+**预期结果**:
+- ✅ 在输入拼音过程中,拼音可以正常显示(不干扰输入法)
+- ✅ 确认输入后,中文字符立即被过滤
+- ✅ 输入框最终为空或保持之前的有效数字
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-036: 粘贴实时过滤测试
+**测试步骤**:
+1. 复制文本 "age25years"
+2. 在年龄输入框中粘贴
+3. 观察输入框变化过程
+
+**预期结果**:
+- ✅ 粘贴后,输入框**立即显示** "25"(实时过滤)
+- ✅ 不会先显示 "age25years" 然后再过滤
+- ✅ 用户感知到的是直接粘贴了 "25"
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-037: 混合输入实时拦截测试
+**测试步骤**:
+1. 清空年龄输入框
+2. 依次按键输入: "1", "a", "2", "b", "3"
+3. 观察每次按键后的输入框内容
+
+**预期结果**:
+- ✅ 输入 "1" 后显示 "1"
+- ✅ 输入 "a" 后仍显示 "1"(字母被拦截)
+- ✅ 输入 "2" 后显示 "12"
+- ✅ 输入 "b" 后仍显示 "12"(字母被拦截)
+- ✅ 输入 "3" 后显示 "123"
+- ✅ **全程没有字母出现在输入框中**
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-038: Ctrl+V 粘贴功能测试
+**测试步骤**:
+1. 复制数字 "50"
+2. 聚焦到年龄输入框
+3. 按 Ctrl+V(或 Cmd+V)粘贴
+
+**预期结果**:
+- ✅ Ctrl/Cmd 组合键不被拦截
+- ✅ 粘贴功能正常工作
+- ✅ 输入框显示 "50"
+
+**优先级**: P0 (高)
+
+---
+
+#### TC-AGE-039: 功能键正常工作测试
+**测试步骤**:
+1. 在年龄输入框输入 "123"
+2. 按 Home 键(光标移到开头)
+3. 按 End 键(光标移到结尾)
+4. 按 Backspace 键
+5. 按 Delete 键
+
+**预期结果**:
+- ✅ Home、End、Backspace、Delete 等功能键正常工作
+- ✅ 不被拦截处理器影响
+
+**优先级**: P1 (中)
+
+---
+
+#### TC-AGE-040: 输入法切换时的稳定性测试
+**测试步骤**:
+1. 聚焦到年龄输入框
+2. 在中英文输入法之间来回切换
+3. 尝试在不同输入法状态下输入
+4. 观察输入框行为
+
+**预期结果**:
+- ✅ 输入法切换不影响输入拦截功能
+- ✅ 无论哪种输入法,非数字字符都被正确处理
+- ✅ 无异常报错或卡顿
+
+**优先级**: P1 (中)
+
+---
+
+### 分类11:性能与用户体验测试
+
+#### TC-AGE-030: 快速连续输入测试
+**测试步骤**:
+1. 快速连续输入数字: "1234567890"
+2. 观察输入框响应
+
+**预期结果**:
+- ✅ 输入响应流畅,无延迟
+- ✅ 所有数字都被正确显示
+- ✅ 超过最大值时显示验证错误
+
+**优先级**: P1 (中)
+
+---
+
+#### TC-AGE-031: 错误提示清晰度测试
+**测试步骤**:
+1. 在年龄输入框输入 "300"
+2. 点击输入框外部
+3. 观察错误提示
+
+**预期结果**:
+- ✅ 错误提示清晰可见
+- ✅ 错误信息准确描述问题
+- ✅ 错误提示位置合理
+
+**优先级**: P2 (低)
+
+---
+
+## 测试执行记录表
+
+| 用例编号 | 测试日期 | 测试人员 | 测试结果 | 备注 |
+|---------|---------|---------|---------|------|
+| TC-AGE-001 | | | ⬜ Pass / ❌ Fail | 负值输入 |
+| TC-AGE-002 | | | ⬜ Pass / ❌ Fail | 负值输入 |
+| TC-AGE-003 | | | ⬜ Pass / ❌ Fail | 负值输入 |
+| TC-AGE-004 | | | ⬜ Pass / ❌ Fail | 非法字符 |
+| TC-AGE-005 | | | ⬜ Pass / ❌ Fail | 非法字符 |
+| TC-AGE-006 | | | ⬜ Pass / ❌ Fail | 非法字符 |
+| TC-AGE-007 | | | ⬜ Pass / ❌ Fail | 非法字符 |
+| TC-AGE-008 | | | ⬜ Pass / ❌ Fail | 非法字符 |
+| TC-AGE-009 | | | ⬜ Pass / ❌ Fail | 边界值 |
+| TC-AGE-010 | | | ⬜ Pass / ❌ Fail | 边界值 |
+| TC-AGE-011 | | | ⬜ Pass / ❌ Fail | 边界值 |
+| TC-AGE-012 | | | ⬜ Pass / ❌ Fail | 边界值 |
+| TC-AGE-013 | | | ⬜ Pass / ❌ Fail | 单位测试 |
+| TC-AGE-014 | | | ⬜ Pass / ❌ Fail | 单位测试 |
+| TC-AGE-015 | | | ⬜ Pass / ❌ Fail | 单位测试 |
+| TC-AGE-016 | | | ⬜ Pass / ❌ Fail | 单位测试 |
+| TC-AGE-017 | | | ⬜ Pass / ❌ Fail | 粘贴测试 |
+| TC-AGE-018 | | | ⬜ Pass / ❌ Fail | 粘贴测试 |
+| TC-AGE-019 | | | ⬜ Pass / ❌ Fail | 粘贴测试 |
+| TC-AGE-020 | | | ⬜ Pass / ❌ Fail | 键盘输入 |
+| TC-AGE-021 | | | ⬜ Pass / ❌ Fail | 键盘输入 |
+| TC-AGE-022 | | | ⬜ Pass / ❌ Fail | 联动测试 |
+| TC-AGE-023 | | | ⬜ Pass / ❌ Fail | 联动测试 |
+| TC-AGE-024 | | | ⬜ Pass / ❌ Fail | 表单验证 |
+| TC-AGE-025 | | | ⬜ Pass / ❌ Fail | 表单验证 |
+| TC-AGE-026 | | | ⬜ Pass / ❌ Fail | 表单验证 |
+| TC-AGE-027 | | | ⬜ Pass / ❌ Fail | 浏览器兼容 |
+| TC-AGE-028 | | | ⬜ Pass / ❌ Fail | 浏览器兼容 |
+| TC-AGE-029 | | | ⬜ Pass / ❌ Fail | 浏览器兼容 |
+| TC-AGE-030 | | | ⬜ Pass / ❌ Fail | 性能测试 |
+| TC-AGE-031 | | | ⬜ Pass / ❌ Fail | 用户体验 |
+| **TC-AGE-032** | | | ⬜ Pass / ❌ Fail | **实时拦截 (v1.1)** |
+| **TC-AGE-033** | | | ⬜ Pass / ❌ Fail | **实时拦截 (v1.1)** |
+| **TC-AGE-034** | | | ⬜ Pass / ❌ Fail | **实时拦截 (v1.1)** |
+| **TC-AGE-035** | | | ⬜ Pass / ❌ Fail | **实时拦截 (v1.1)** |
+| **TC-AGE-036** | | | ⬜ Pass / ❌ Fail | **实时拦截 (v1.1)** |
+| **TC-AGE-037** | | | ⬜ Pass / ❌ Fail | **实时拦截 (v1.1)** |
+| **TC-AGE-038** | | | ⬜ Pass / ❌ Fail | **实时拦截 (v1.1)** |
+| **TC-AGE-039** | | | ⬜ Pass / ❌ Fail | **实时拦截 (v1.1)** |
+| **TC-AGE-040** | | | ⬜ Pass / ❌ Fail | **实时拦截 (v1.1)** |
+
+## 测试优先级说明
+
+- **P0 (高)**: 核心功能,必须通过
+- **P1 (中)**: 重要功能,建议通过
+- **P2 (低)**: 辅助功能,可选通过
+
+## 缺陷记录模板
+
+如果测试中发现问题,请记录:
+
+```
+缺陷ID: BUG-AGE-XXX
+相关用例: TC-AGE-XXX
+严重程度: Critical / Major / Minor
+复现步骤:
+1. ...
+2. ...
+3. ...
+
+实际结果: ...
+预期结果: ...
+附件: (截图/视频)
+```
+
+## 测试完成标准
+
+- ✅ 所有 P0 优先级测试用例通过
+- ✅ 至少 80% 的 P1 优先级测试用例通过
+- ✅ 所有发现的 Critical/Major 缺陷已修复
+- ✅ 回归测试通过
+
+---
+
+## 版本历史
+
+| 版本 | 日期 | 修改内容 | 修改人 |
+|------|------|---------|--------|
+| v1.0 | 2025-11-28 | 初始版本,包含基础输入限制和表单验证 | - |
+| v1.1 | 2025-11-28 | 新增实时输入拦截功能(9个新测试用例:TC-AGE-032~040) | - |
+
+---
+
+**测试计划创建日期**: 2025-11-28  
+**最后更新日期**: 2025-11-28  
+**文档版本**: v1.1

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "zsis",
-  "version": "1.1.0",
+  "version": "1.1.1",
   "private": true,
   "description": "医学成像系统",
   "main": "main.js",

+ 82 - 1
src/components/NumberWithUnit.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useState } from 'react';
 import { InputNumber, Select, Space, SpaceProps } from 'antd';
 import type { InputNumberProps } from 'antd/es/input-number';
 import type { SelectProps } from 'antd/es/select';
@@ -42,11 +42,84 @@ const NumberWithUnit: React.FC<NumberWithUnitProps> = ({
   ...rest // ← 这里会收到、style 等
 }) => {
   const unit = value?.unit ?? defaultUnit ?? undefined;
+  const [isComposing, setIsComposing] = useState(false);
 
   const triggerChange = (changed: Partial<NumberUnitValue>) => {
     onChange?.({ ...value, ...changed });
   };
 
+  // 处理键盘输入 - 阻止非数字字符的输入
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    // 如果正在使用输入法,不拦截
+    if (isComposing) return;
+
+    // 允许的功能键
+    const allowedKeys = [
+      'Backspace',
+      'Delete',
+      'ArrowLeft',
+      'ArrowRight',
+      'ArrowUp',
+      'ArrowDown',
+      'Tab',
+      'Enter',
+      'Escape',
+      'Home',
+      'End',
+    ];
+
+    // 如果是功能键,允许
+    if (allowedKeys.includes(e.key)) return;
+
+    // 如果是 Ctrl/Cmd 组合键(如 Ctrl+A, Ctrl+C, Ctrl+V),允许
+    if (e.ctrlKey || e.metaKey) return;
+
+    // 如果不是数字,阻止输入
+    if (!/^\d$/.test(e.key)) {
+      e.preventDefault();
+    }
+  };
+
+  // 处理粘贴 - 过滤粘贴内容中的非数字字符
+  const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
+    e.preventDefault();
+
+    // 获取剪贴板内容
+    const pastedText = e.clipboardData.getData('text');
+
+    // 只保留数字
+    const cleanedText = pastedText.replace(/\D/g, '');
+
+    if (cleanedText) {
+      const numValue = parseInt(cleanedText, 10);
+      triggerChange({ number: numValue });
+    }
+  };
+
+  // 处理输入法开始
+  const handleCompositionStart = () => {
+    setIsComposing(true);
+  };
+
+  // 处理输入法结束 - 过滤输入法输入的非数字字符
+  const handleCompositionEnd = (
+    e: React.CompositionEvent<HTMLInputElement>
+  ) => {
+    setIsComposing(false);
+
+    // 获取输入的内容并过滤
+    const inputValue = e.currentTarget.value;
+    const cleanedValue = inputValue.replace(/\D/g, '');
+
+    if (cleanedValue) {
+      const numValue = parseInt(cleanedValue, 10);
+      triggerChange({ number: numValue });
+    } else {
+      // 如果过滤后没有有效数字,清空
+      triggerChange({ number: undefined });
+    }
+  };
+
   return (
     <>
       <style>{`
@@ -64,6 +137,14 @@ const NumberWithUnit: React.FC<NumberWithUnitProps> = ({
           onChange={(n) =>
             triggerChange({ number: typeof n === 'number' ? n : undefined })
           }
+          min={0}
+          precision={0}
+          parser={(value) => value?.replace(/\D/g, '') || ''}
+          keyboard={true}
+          onKeyDown={handleKeyDown}
+          onPaste={handlePaste}
+          onCompositionStart={handleCompositionStart}
+          onCompositionEnd={handleCompositionEnd}
           {...numberProps}
           defaultValue={defaultNumber}
           onBlur={onBlur}

+ 20 - 1
src/validation/patient/registerSchema.ts

@@ -86,7 +86,26 @@ export const registerFormFields = {
     label: 'Patient Age',
     required: true,
     requiredLabel: 'Patient Age is required',
-    validation: zodToAntdRules(registerformSchema.shape.patient_age),
+    validation: [
+      ...zodToAntdRules(registerformSchema.shape.patient_age),
+      {
+        validator: (_, value) => {
+          if (!value || typeof value.number !== 'number') {
+            return Promise.reject(new Error('请输入有效的年龄'));
+          }
+          if (value.number < 0) {
+            return Promise.reject(new Error('年龄不能为负数'));
+          }
+          if (value.number > 200) {
+            return Promise.reject(new Error('年龄超出合理范围(最大200)'));
+          }
+          if (!Number.isInteger(value.number)) {
+            return Promise.reject(new Error('年龄必须为整数'));
+          }
+          return Promise.resolve();
+        },
+      },
+    ],
     message: 'Patient Age is required',
     trigger: ['onChange', 'onBlur'],
   },