本功能允许用户通过扫描二维码快速录入病人信息。如果二维码中包含体位信息,系统将自动创建检查并进入检查页面。
interface QRCodeData {
// 基础患者信息(必填)
patient_id: string; // 患者ID
patient_name: string; // 患者姓名
patient_sex: string; // 性别
patient_age?: { // 年龄(可选)
number: number;
unit: 'D' | 'M' | 'Y';
};
patient_dob?: string; // 出生日期(可选)
// 扩展信息(可选)
accession_number?: string; // 检查号
patient_size?: string; // 患者尺寸
weight?: number; // 体重
thickness?: number; // 厚度
length?: number; // 身高
ref_physician?: string; // 医师
operator_id?: string; // 操作员
comment?: string; // 备注
// 宠物专用字段(可选)
owner_name?: string; // 宠物主人
variety?: string; // 品种
chip_number?: string; // 芯片号
sex_neutered?: string; // 绝育状态
// 人医专用字段(可选)
pregnancy_status?: string; // 妊娠状态
// 体位信息(可选,如果存在则自动进入检查)
views?: Array<{
view_id: string; // 体位ID
procedure_id: string; // 协议ID
}>;
}
QRCodeScanButton(新建)
QRCodeScanModal(新建)
BasicInfoForm(修改)
RegisterPage(修改)
QRCodeScanner(新建)
QRCodeDataProcessor(新建)
AutoRegisterHandler(新建)
registerWork (已存在)
src/API/patient/workActions.tsfetchViews (已存在)
formSlice (已存在)
src/states/patient/register/formSlice.tsviewSelectionSlice (需要确认)
qrCodeScanSlice(新建)
BusinessFlowSlice (已存在)
src/states/BusinessFlowSlice.tssrc/components/QRCodeScanner/QRCodeScanButton.tsx - 扫码触发按钮src/components/QRCodeScanner/QRCodeScanModal.tsx - 扫码模态框src/components/QRCodeScanner/index.ts - 导出文件src/domain/qrcode/qrCodeDataProcessor.ts - 二维码数据处理器src/domain/qrcode/autoRegisterHandler.ts - 自动注册处理器src/domain/qrcode/qrCodeValidator.ts - 二维码数据验证器src/states/patient/register/qrCodeScanSlice.ts - 扫码状态管理src/types/qrcode.ts - 二维码相关类型定义src/hooks/useQRCodeScanner.ts - 二维码扫描 Hooksrc/pages/patient/register.tsx - 添加扫码按钮和逻辑src/pages/patient/components/register.form.tsx - 支持外部数据填充src/states/store.ts - 注册新的 qrCodeScanSlicepackage.json - 添加二维码扫描库依赖src/states/patient/register/viewSelectionSlice.ts - 添加批量选择体位的方法src/validation/patient/registerSchema.ts - 可能需要调整验证规则sequenceDiagram
participant U as 用户
participant B as 扫码按钮
participant M as 扫码模态框
participant S as QRCodeScanner
participant P as QRCodeDataProcessor
participant F as 注册表单
participant A as AutoRegisterHandler
participant API as registerWork API
participant BF as BusinessFlow
U->>B: 点击扫码按钮
B->>M: 打开模态框
M->>S: 启动摄像头
S->>S: 扫描二维码
S->>M: 返回二维码原始数据
M->>P: 传递数据进行处理
P->>P: 验证数据格式
P->>P: 转换为表单格式
alt 数据验证失败
P->>M: 返回错误信息
M->>U: 显示错误提示
else 数据验证成功
P->>F: 填充表单数据
M->>U: 关闭模态框
alt 包含体位信息
P->>A: 触发自动注册
A->>API: 调用注册接口
API->>A: 返回注册结果
alt 注册成功
A->>BF: 切换到检查页面
BF->>U: 显示检查页面
else 注册失败
A->>U: 显示错误提示
end
else 不包含体位信息
F->>U: 显示填充后的表单
U->>U: 手动选择体位和注册
end
end
flowchart TD
A[二维码] --> B[QRCodeScanner]
B --> C[原始字符串数据]
C --> D[JSON.parse]
D --> E[QRCodeData 对象]
E --> F[QRCodeDataProcessor]
F --> G{验证数据}
G -->|失败| H[错误提示]
G -->|成功| I[转换数据格式]
I --> J[FormData]
J --> K[填充表单]
K --> L{是否包含体位?}
L -->|否| M[等待用户操作]
L -->|是| N[AutoRegisterHandler]
N --> O[构建 RegisterInfo]
O --> P[调用 registerWork API]
P --> Q{注册成功?}
Q -->|是| R[切换到 exam 页面]
Q -->|否| S[显示错误]
classDiagram
class RegisterPage {
+Form form
+handleQRCodeScan()
+handleRegister()
}
class QRCodeScanButton {
+onClick()
}
class QRCodeScanModal {
+visible: boolean
+onScan(data)
+onCancel()
}
class QRCodeScanner {
+startScan()
+stopScan()
+onDetected(callback)
}
class QRCodeDataProcessor {
+validate(data)
+transform(data)
+hasViews(data)
}
class AutoRegisterHandler {
+execute(data)
+selectViews(views)
+callRegisterAPI()
+navigateToExam()
}
class BasicInfoForm {
+form: Form
+setFieldsValue(data)
}
RegisterPage --> QRCodeScanButton
RegisterPage --> QRCodeScanModal
RegisterPage --> BasicInfoForm
QRCodeScanModal --> QRCodeScanner
QRCodeScanModal --> QRCodeDataProcessor
QRCodeDataProcessor --> AutoRegisterHandler
AutoRegisterHandler --> RegisterPage
/**
* 二维码数据格式
* 用于定义二维码中包含的患者和检查信息
*/
interface QRCodeData {
// 基础患者信息(必填)
patient_id: string; // 患者ID
patient_name: string; // 患者姓名
patient_sex: string; // 性别: 'M', 'F', 'O'
// 年龄信息(可选,如果提供则会覆盖 patient_dob)
patient_age?: {
number: number; // 数值
unit: 'D' | 'M' | 'Y'; // 单位:天/月/年
};
// 出生日期(可选,如果提供 patient_age 则优先使用 age)
patient_dob?: string; // ISO 8601 格式
// 扩展信息(可选)
accession_number?: string; // 检查号
patient_size?: string; // 患者尺寸: 'Small', 'Medium', 'Large'
weight?: number; // 体重(kg)
thickness?: number; // 厚度(cm)
length?: number; // 身高(cm)
ref_physician?: string; // 医师
operator_id?: string; // 操作员
comment?: string; // 备注
// 宠物专用字段(可选)
owner_name?: string; // 宠物主人
variety?: string; // 品种
chip_number?: string; // 芯片号
sex_neutered?: 'ALTERED' | 'UNALTERED'; // 绝育状态
// 人医专用字段(可选)
pregnancy_status?: 'NOT_PREGNANT' | 'POSSIBLY_PREGNANT' | 'DEFINITELY_PREGNANT' | 'UNKNOWN';
// 体位信息(可选,如果存在则自动进入检查)
views?: Array<{
view_id: string; // 体位ID
procedure_id: string; // 协议ID
}>;
}
/**
* 二维码扫描状态
* 用于 Redux 状态管理
*/
interface QRCodeScanState {
isScanning: boolean; // 是否正在扫描
isProcessing: boolean; // 是否正在处理数据
error: string | null; // 错误信息
lastScanData: QRCodeData | null; // 最后扫描的数据
autoRegisterPending: boolean; // 是否等待自动注册
}
/**
* 数据验证结果
*/
interface ValidationResult {
success: boolean; // 验证是否成功
errors: string[]; // 错误列表
warnings: string[]; // 警告列表
}
用户操作:用户在注册页面点击"扫码录入"按钮
flowchart TD
Start([用户点击扫码按钮]) --> A[打开 QRCodeScanModal]
A --> B{请求摄像头权限}
B -->|拒绝| C[显示权限提示]
B -->|允许| D[启动摄像头]
D --> E[实时扫描二维码]
E --> F{检测到二维码?}
F -->|否| E
F -->|是| G[解析二维码内容]
G --> H{JSON 格式有效?}
H -->|否| I[显示格式错误]
I --> J{用户选择}
J -->|重试| E
J -->|取消| End1([结束])
H -->|是| K[验证必填字段]
K --> L{验证通过?}
L -->|否| M[显示验证错误]
M --> J
L -->|是| N[转换数据格式]
N --> O[填充表单]
O --> P[关闭模态框]
P --> Q{是否包含体位?}
Q -->|否| R[等待用户手动操作]
R --> End2([结束 - 表单已填充])
Q -->|是| S[选择体位]
S --> T[调用注册 API]
T --> U{注册成功?}
U -->|否| V[显示注册错误]
V --> End3([结束 - 保持在注册页])
U -->|是| W[切换到 exam 页面]
W --> End4([结束 - 进入检查])
使用以下 JSON 数据生成测试二维码(可以使用在线二维码生成器,如 qr-code-generator.com)
示例 1:仅患者信息(不自动进入检查)
{
"patient_id": "P001",
"patient_name": "张三",
"patient_sex": "M",
"patient_age": {
"number": 30,
"unit": "Y"
},
"patient_size": "Medium",
"weight": 70,
"thickness": 25,
"length": 175,
"ref_physician": "李医生",
"operator_id": "OP001",
"comment": "常规检查"
}
示例 2:包含体位信息(自动进入检查)
{
"patient_id": "P002",
"patient_name": "李四",
"patient_sex": "F",
"patient_age": {
"number": 25,
"unit": "Y"
},
"patient_size": "Small",
"weight": 55,
"views": [
{
"view_id": "view_001",
"procedure_id": "proc_001"
}
]
}
示例 3:宠物患者信息
{
"patient_id": "PET001",
"patient_name": "旺财",
"patient_sex": "M",
"patient_age": {
"number": 3,
"unit": "Y"
},
"owner_name": "王主人",
"variety": "金毛",
"chip_number": "CHI123456",
"sex_neutered": "UNALTERED",
"weight": 30,
"thickness": 20
}
示例 4:无效数据(测试错误处理)
{
"patient_name": "赵六",
"patient_sex": "M"
}
(缺少必填字段 patient_id)
测试目标:验证扫码功能能够正确填充表单
前置条件:
操作步骤:
预期结果:
实际结果:(测试时填写)
测试状态:⬜ 通过 / ⬜ 失败
测试目标:验证包含体位信息的二维码能够自动创建检查并进入检查页面
前置条件:
操作步骤:
预期结果:
实际结果:(测试时填写)
测试状态:⬜ 通过 / ⬜ 失败
测试目标:验证系统能够正确处理非 JSON 格式的二维码
前置条件:
操作步骤:
预期结果:
实际结果:(测试时填写)
测试状态:⬜ 通过 / ⬜ 失败
测试目标:验证系统能够检测并提示缺少必填字段
前置条件:
操作步骤:
预期结果:
实际结果:(测试时填写)
测试状态:⬜ 通过 / ⬜ 失败
测试目标:验证系统能够正确处理摄像头权限问题
前置条件:
操作步骤:
预期结果:
实际结果:(测试时填写)
测试状态:⬜ 通过 / ⬜ 失败
测试目标:验证宠物患者信息能够正确填充
前置条件:
操作步骤:
预期结果:
实际结果:(测试时填写)
测试状态:⬜ 通过 / ⬜ 失败
测试目标:验证扫码填充的年龄能够正确联动出生日期
前置条件:
操作步骤:
预期结果:
实际结果:(测试时填写)
测试状态:⬜ 通过 / ⬜ 失败
测试目标:验证用户可以随时取消扫码操作
前置条件:
操作步骤:
预期结果:
实际结果:(测试时填写)
测试状态:⬜ 通过 / ⬜ 失败
测试目标:验证自动注册失败后的错误处理
前置条件:
操作步骤:
预期结果:
实际结果:(测试时填写)
测试状态:⬜ 通过 / ⬜ 失败
测试目标:确保手动填写表单注册功能不受影响
操作步骤:
预期结果:
测试状态:⬜ 通过 / ⬜ 失败
测试目标:确保从历史记录预填充功能不受影响
操作步骤:
预期结果:
测试状态:⬜ 通过 / ⬜ 失败
测试目标:确保急诊快速注册功能不受影响
操作步骤:
预期结果:
测试状态:⬜ 通过 / ⬜ 失败
测试目标:验证扫码识别的响应速度
测试方法:
预期结果:
实际结果:(测试时填写)
测试目标:验证表单填充的流畅度
测试方法:
预期结果:
实际结果:(测试时填写)
| 浏览器 | 版本 | 摄像头调用 | 二维码识别 | 表单填充 | 测试状态 |
|---|---|---|---|---|---|
| Chrome | 最新 | ⬜ | ⬜ | ⬜ | ⬜ 通过 / ⬜ 失败 |
| Edge | 最新 | ⬜ | ⬜ | ⬜ | ⬜ 通过 / ⬜ 失败 |
| Firefox | 最新 | ⬜ | ⬜ | ⬜ | ⬜ 通过 / ⬜ 失败 |
| Safari | 最新 | ⬜ | ⬜ | ⬜ | ⬜ 通过 / ⬜ 失败 |
问题描述:
风险等级:🔴 高
影响范围:扫码功能完全不可用
解决方案:
代码示例:
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
// 启动扫描
} catch (error) {
if (error.name === 'NotAllowedError') {
message.error('需要摄像头权限才能使用扫码功能,请允许访问摄像头');
} else if (error.name === 'NotFoundError') {
message.error('未检测到摄像头设备');
} else {
message.error('摄像头启动失败:' + error.message);
}
}
问题描述:
风险等级:🟡 中
影响范围:用户体验
解决方案:
优化建议:
// 配置二维码扫描器以提高识别率
const config = {
fps: 10, // 每秒扫描帧数
qrbox: { width: 250, height: 250 }, // 扫描框大小
aspectRatio: 1.0,
};
问题描述:
风险等级:🟡 中
影响范围:数据填充失败或错误
解决方案:
验证示例:
const validateQRCodeData = (data: any): ValidationResult => {
const errors: string[] = [];
// 必填字段检查
if (!data.patient_id) errors.push('缺少患者ID');
if (!data.patient_name) errors.push('缺少患者姓名');
if (!data.patient_sex) errors.push('缺少性别信息');
// 格式检查
if (data.patient_sex && !['M', 'F', 'O'].includes(data.patient_sex)) {
errors.push('性别格式错误');
}
return {
success: errors.length === 0,
errors,
warnings: []
};
};
问题描述:
风险等级:🟡 中
影响范围:自动注册失败
解决方案:
验证流程:
const validateViews = async (views: Array<{view_id: string, procedure_id: string}>): Promise<boolean> => {
try {
for (const view of views) {
const viewDetail = await fetchViewDetail(view.view_id);
if (!viewDetail || !viewDetail.is_enabled) {
message.warning(`体位 ${view.view_id} 不可用,已跳过自动注册`);
return false;
}
}
return true;
} catch (error) {
message.error('验证体位信息失败');
return false;
}
};
问题描述:
风险等级:🔴 高
影响范围:自动注册流程中断
解决方案:
错误处理示例:
const handleAutoRegister = async (data: QRCodeData) => {
try {
dispatch(setQRCodeProcessing(true));
const result = await registerWork(registerInfo);
if (result.code === '0x000000') {
message.success('注册成功,正在进入检查页面');
dispatch(setBusinessFlow('exam'));
} else {
message.error(`注册失败:${result.description}`);
}
} catch (error) {
console.error('自动注册失败:', error);
message.error('网络错误,注册失败,请检查网络连接后重试');
} finally {
dispatch(setQRCodeProcessing(false));
}
};
问题描述:
风险等级:🟢 低
影响范围:数据覆盖或状态混乱
解决方案:
防抖示例:
const debouncedScan = debounce((decodedText: string) => {
handleQRCodeScanned(decodedText);
}, 500);
问题描述:
风险等级:🟡 中
影响范围:部分用户无法使用功能
解决方案:
功能检测:
const isCameraSupported = (): boolean => {
return !!(
navigator.mediaDevices &&
navigator.mediaDevices.getUserMedia
);
};
// 在组件中使用
{isCameraSupported() && <QRCodeScanButton />}
问题描述:
风险等级:🟢 低
影响范围:页面性能下降
解决方案:
资源清理:
useEffect(() => {
return () => {
// 组件卸载时停止摄像头
if (scannerRef.current) {
scannerRef.current.stop();
}
// 释放媒体流
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
}
};
}, []);
问题描述:
风险等级:🔴 高
影响范围:系统安全
解决方案:
数据消毒:
const sanitizeData = (data: any): QRCodeData => {
// 只保留允许的字段
const allowedFields = [
'patient_id', 'patient_name', 'patient_sex',
// ... 其他允许的字段
];
const sanitized: any = {};
allowedFields.forEach(field => {
if (data[field] !== undefined) {
// 转义特殊字符
sanitized[field] = typeof data[field] === 'string'
? escapeHtml(data[field])
: data[field];
}
});
return sanitized as QRCodeData;
};
问题描述:
风险等级:🟡 中
影响范围:用户满意度
解决方案:
UX 优化建议:
| 库名称 | Stars | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|---|
| html5-qrcode | 4.5k+ | 简单易用、文档完善、持续维护 | 相对较重 | ⭐⭐⭐⭐⭐ |
| react-qr-scanner | 1k+ | React 友好 | 文档较少、维护不活跃 | ⭐⭐⭐ |
| jsQR | 3k+ | 轻量级、纯 JS | 需要自己处理视频流 | ⭐⭐⭐⭐ |
| qrcode-decoder | 500+ | 轻量 | 功能有限 | ⭐⭐⭐ |
最终选择:html5-qrcode
理由:
服务端验证:
摄像头访问:
敏感信息:
HTTPS:
| 版本 | 日期 | 作者 | 变更内容 |
|---|---|---|---|
| 1.0.0 | 2025-12-19 | AI Assistant | 初始版本,完成功能设计和文档编写 |
本文档详细设计了二维码扫码录入病人信息的完整功能,包括:
该功能将极大提升患者注册的效率,特别是在批量患者登记和急诊场景下。通过二维码扫描,可以减少手动输入错误,加快工作流程,提高用户满意度。
📌 下一步行动: