| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755 |
- import React, { useEffect, useRef, useMemo, useState } from 'react';
- import {
- Form,
- Input,
- DatePicker,
- InputNumber,
- Select,
- Radio,
- FormInstance,
- message,
- } from 'antd';
- import { useIntl, FormattedMessage } from 'react-intl';
- import { registerFormFields } from '@/validation/patient/registerSchema';
- import NumberWithUnit from '@/components/NumberWithUnit';
- import dayjs, { Dayjs } from 'dayjs';
- import { useSelector, useDispatch } from 'react-redux';
- import { RootState } from '@/states/store';
- import { setFormData } from '@/states/patient/register/formSlice';
- import {
- PregnancyStatus,
- pregnancyStatusOptions,
- } from '@/domain/patient/pregnancyStatus';
- import { getGenderOptions } from '@/domain/patient/genderOptions';
- import { processQRCodeData, transformToFormData } from '@/domain/qrcode/qrCodeDataProcessor';
- interface BasicInfoFormProps {
- style?: React.CSSProperties;
- form?: FormInstance;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- onValuesChange?: (changedValues: any, allValues: any) => void;
- }
- const BasicInfoForm: React.FC<BasicInfoFormProps> = ({
- style,
- form,
- onValuesChange,
- }) => {
- // 使用 ref 记录更新源,防止循环触发
- const updateSourceRef = useRef<'age' | 'dob' | null>(null);
- const patient_age = Form.useWatch('patient_age', form);
- const patient_dob = Form.useWatch('patient_dob', form);
- const productName = useSelector(
- (state: RootState) => state.product.productName
- );
- const formData = useSelector((state: RootState) => state.form.formData);
- const dispatch = useDispatch();
- const [pregnancyStatusFieldVisible, setPregnancyStatusFieldVisible] =
- useState(false);
- const initializedRef = useRef(false);
- // 扫码枪扫码相关状态
- const scanInputRef = useRef<HTMLInputElement>(null);
- const [isComposing, setIsComposing] = useState(false);
- const bufferRef = useRef('');
- const timerRef = useRef<number | null>(null);
- // 同步初始表单值到 Redux store,并生成登记号
- useEffect(() => {
- if (!initializedRef.current) {
- // 生成登记号(如果不存在)
- let updatedFormData = { ...formData };
- if (!updatedFormData.accession_number) {
- const now = dayjs();
- updatedFormData.accession_number = now.format('YYYYMMDDHHmmss');
- }
- const allValues = form?.getFieldsValue();
- if (allValues && Object.keys(allValues).length > 0) {
- const merged = { ...allValues ,...updatedFormData};
- dispatch(setFormData(merged));
- form?.setFieldsValue(merged)
- }
- initializedRef.current = true;
- }
- }, [form, dispatch, formData]);
- const intl = useIntl();
- // 动态获取性别选项
- const genderOptions = useMemo(() => {
- return getGenderOptions(productName);
- }, [productName]);
- // 根据年龄计算出生日期
- 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' };
- };
- // 监听年龄变化
- useEffect(() => {
- // 如果是出生日期联动更新的年龄,跳过
- if (updateSourceRef.current === 'dob') {
- updateSourceRef.current = null;
- return;
- }
- // 如果年龄有效,计算出生日期
- if (patient_age && patient_age.number >= 0 && form) {
- updateSourceRef.current = 'age';
- const newDob = calculateDobFromAge(patient_age);
- form.setFieldsValue({ patient_dob: newDob });
- //人工同步更新 Redux store
- dispatch(setFormData(form.getFieldsValue()))
- console.log('年龄变化,更新出生日期:', newDob.format('YYYY-MM-DD'));
- }
- }, [patient_age, form]);
- // 监听出生日期变化
- useEffect(() => {
- // 如果是年龄联动更新的出生日期,跳过
- if (updateSourceRef.current === 'age') {
- updateSourceRef.current = null;
- return;
- }
- // 如果出生日期有效,计算年龄
- if (patient_dob && dayjs.isDayjs(patient_dob) && form) {
- updateSourceRef.current = 'dob';
- const newAge = calculateAgeFromDob(patient_dob);
- form.setFieldsValue({ patient_age: newAge });
- console.log('出生日期变化,更新年龄:', newAge);
- }
- }, [patient_dob, form]);
- // 处理扫码枪扫码后的数据:解析文本并自动填充到注册表单
- const processScannedData = React.useCallback(async (scannedText: string) => {
- try {
- // 添加调试日志,输出实际接收到的扫码文本
- console.log('[扫码枪调试] 得到的扫码数据是:', JSON.stringify(scannedText));
- console.log('[扫码枪调试] 扫码数据长度:', scannedText.length);
- // 使用二维码数据处理器解析扫码文本(复用现有逻辑)
- const result = processQRCodeData(scannedText);
- if (!result.success) {
- message.error(result.error || '扫码枪数据处理失败');
- return;
- }
- // 将解析后的数据转换为表单格式
- const formData = transformToFormData(result.data!);
- // 自动填充表单
- form?.setFieldsValue(formData);
- // 同步更新 Redux store
- dispatch(setFormData(formData));
- // 显示成功提示
- message.success('扫码枪数据已自动填充到表单');
- } catch (error) {
- console.error('扫码枪数据处理失败:', error);
- message.error('扫码枪数据处理失败');
- }
- }, [form, dispatch]);
- // 扫码枪扫码监听器:页面级 keydown 监听,自动切换焦点到隐藏input
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- const target = e.target as HTMLElement;
- const isEditable =
- target &&
- (target.tagName === 'INPUT' ||
- target.tagName === 'TEXTAREA' ||
- target.isContentEditable);
- // 如果当前没有可编辑元素获得焦点,认为是扫码枪输入
- if (!isEditable) {
- scanInputRef.current?.focus();
- }
- // 扫码枪通常以 Enter 结束
- if (e.key === 'Enter') {
- if (bufferRef.current) {
- processScannedData(bufferRef.current);
- bufferRef.current = '';
- if (scanInputRef.current) {
- scanInputRef.current.value = '';
- }
- }
- // 阻止Enter键的默认行为(如表单提交)
- e.preventDefault();
- e.stopPropagation();
- } else if (e.key === 'Tab') {
- // 完全控制Tab键行为,防止焦点切换
- e.preventDefault();
- e.stopPropagation();
- // 手动将Tab字符插入到隐藏input中
- const input = scanInputRef.current;
- if (input) {
- const start = input.selectionStart || 0;
- const end = input.selectionEnd || 0;
- const value = input.value;
- // 在光标位置插入Tab字符
- input.value = value.substring(0, start) + '\t' + value.substring(end);
- // 更新光标位置
- input.selectionStart = input.selectionEnd = start + 1;
- // 手动触发input事件,确保React能检测到变化
- input.dispatchEvent(new Event('input', { bubbles: true }));
- }
- }
- // 其他字符不阻止,让它们自然输入到隐藏input中
- // input会自动过滤掉无效字符,只保留有效的文本
- };
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [processScannedData]);
- /** input 事件:真正可靠地拿中文 */
- const handleScanInput = (e: React.ChangeEvent<HTMLInputElement>) => {
- if (isComposing) return;
- bufferRef.current = e.target.value;
- // 防止没有 Enter 的扫码枪(用时间判断)
- if (timerRef.current) {
- clearTimeout(timerRef.current);
- }
- timerRef.current = window.setTimeout(() => {
- if (bufferRef.current) {
- processScannedData(bufferRef.current);
- bufferRef.current = '';
- e.target.value = '';
- }
- }, 300); // 300ms 内无新输入,认为扫码结束
- };
- return (
- <Form
- form={form}
- layout="vertical"
- style={style}
- className="px-2"
- initialValues={
- {
- // patient_dob: dayjs(),
- }
- }
- onValuesChange={onValuesChange}
- >
- {/** 宠物专用 */}
- {productName === 'VETDROS' && (
- <Form.Item
- label={
- <FormattedMessage
- id="register.owner_name"
- defaultMessage="register.owner_name"
- />
- }
- name="owner_name"
- required={registerFormFields.owner_name.required}
- validateTrigger={registerFormFields.owner_name.trigger}
- rules={registerFormFields.owner_name.validation}
- >
- <Input
- placeholder={intl.formatMessage({
- id: 'register.owner_name.placeholder',
- defaultMessage: 'register.owner_name.placeholder',
- })}
- />
- </Form.Item>
- )}
- <Form.Item
- label={
- <FormattedMessage
- id={
- productName === 'VETDROS'
- ? 'animal.register.patientName'
- : 'register.patientName'
- }
- defaultMessage={
- productName === 'VETDROS'
- ? 'animal.register.patientName'
- : 'register.patientName'
- }
- />
- }
- name="patient_name"
- required={registerFormFields.patient_name.required}
- validateTrigger={registerFormFields.patient_name.trigger}
- rules={registerFormFields.patient_name.validation}
- >
- <Input
- placeholder={intl.formatMessage({
- id:
- productName === 'VETDROS'
- ? 'animal.register.patientName.placeholder'
- : 'register.patientName.placeholder',
- defaultMessage:
- productName === 'VETDROS'
- ? 'animal.register.patientName.placeholder'
- : 'register.patientName.placeholder',
- })}
- />
- </Form.Item>
- <Form.Item
- label={
- <FormattedMessage
- id="register.accessionNumber"
- defaultMessage="register.accessionNumber"
- />
- }
- name="accession_number"
- required={registerFormFields.accession_number.required}
- validateTrigger={registerFormFields.accession_number.trigger}
- rules={registerFormFields.accession_number.validation}
- >
- <Input
- placeholder={intl.formatMessage({
- id: 'register.accessionNumber.placeholder',
- defaultMessage: 'register.accessionNumber.placeholder',
- })}
- />
- </Form.Item>
- <Form.Item
- label={
- <FormattedMessage
- id={
- productName === 'VETDROS'
- ? 'animal.register.patientId'
- : 'register.patientId'
- }
- defaultMessage={
- productName === 'VETDROS'
- ? 'animal.register.patientId'
- : 'register.patientId'
- }
- />
- }
- name="patient_id"
- required={registerFormFields.patient_id.required}
- validateTrigger={registerFormFields.patient_id.trigger}
- rules={registerFormFields.patient_id.validation}
- >
- <Input
- placeholder={intl.formatMessage({
- id:
- productName === 'VETDROS'
- ? 'animal.register.patientId.placeholder'
- : 'register.patientId.placeholder',
- defaultMessage:
- productName === 'VETDROS'
- ? 'animal.register.patientId.placeholder'
- : 'register.patientId.placeholder',
- })}
- />
- </Form.Item>
- <Form.Item
- label={
- <FormattedMessage
- id={
- productName === 'VETDROS'
- ? 'animal.register.patientSize'
- : 'register.patientSize'
- }
- defaultMessage={
- productName === 'VETDROS'
- ? 'animal.register.patientSize'
- : 'register.patientSize'
- }
- />
- }
- name="patient_size"
- required={registerFormFields.patient_size.required}
- validateTrigger={registerFormFields.patient_size.trigger}
- rules={registerFormFields.patient_size.validation}
- initialValue="Medium"
- >
- <Select
- placeholder={intl.formatMessage({
- id:
- productName === 'VETDROS'
- ? 'animal.register.patientSize.placeholder'
- : 'register.patientSize.placeholder',
- defaultMessage:
- productName === 'VETDROS'
- ? 'animal.register.patientSize.placeholder'
- : 'register.patientSize.placeholder',
- })}
- defaultValue="Medium"
- >
- <Select.Option value="Large">
- {intl.formatMessage({ id: 'Large', defaultMessage: 'Large' })}
- </Select.Option>
- <Select.Option value="Medium">
- {intl.formatMessage({ id: 'Medium', defaultMessage: 'Medium' })}
- </Select.Option>
- <Select.Option value="Small">
- {intl.formatMessage({ id: 'Small', defaultMessage: 'Small' })}
- </Select.Option>
- </Select>
- </Form.Item>
- <Form.Item
- label={
- <FormattedMessage id="register.age" defaultMessage="register.age" />
- }
- name="patient_age"
- required={registerFormFields.patient_age.required}
- validateTrigger={registerFormFields.patient_age.trigger}
- rules={registerFormFields.patient_age.validation}
- initialValue={{ number: 0, unit: 'Y' }}
- >
- <NumberWithUnit
- align="baseline"
- defaultUnit="Y"
- defaultNumber={0}
- unitClassName="w-full"
- numberClassName="w-full"
- className="w-full"
- options={[
- { label: '天', value: 'D' },
- { label: '月', value: 'M' },
- { label: '年', value: 'Y' },
- ]}
- />
- </Form.Item>
- <Form.Item
- label={
- <FormattedMessage
- id="register.dateOfBirth"
- defaultMessage="register.dateOfBirth"
- />
- }
- name="patient_dob"
- required={registerFormFields.patient_dob.required}
- validateTrigger={registerFormFields.patient_dob.trigger}
- rules={registerFormFields.patient_dob.validation}
- initialValue={dayjs()}
- >
- <DatePicker
- format="YYYY-MM-DD"
- style={{ width: '100%' }}
- defaultValue={dayjs()}
- disabledDate={(current) => current && current > dayjs().endOf('day')}
- />
- </Form.Item>
- <Form.Item
- label={
- <FormattedMessage
- id="register.gender"
- defaultMessage="register.gender"
- />
- }
- name="patient_sex"
- required={registerFormFields.patient_sex.required}
- validateTrigger={registerFormFields.patient_sex.trigger}
- rules={registerFormFields.patient_sex.validation}
- >
- <Select
- options={genderOptions}
- onChange={(e) => {
- const _PregnancyStatusFieldVisible = e === 'M';
- setPregnancyStatusFieldVisible(_PregnancyStatusFieldVisible);
- if (_PregnancyStatusFieldVisible) {
- // 更新表单
- form?.setFieldValue('pregnancy_status', PregnancyStatus.UNKNOWN);
- // 手动同步更新 Redux store
- const currentValues = form?.getFieldsValue();
- dispatch(
- setFormData({
- ...currentValues,
- pregnancy_status: PregnancyStatus.UNKNOWN,
- })
- );
- }
- }}
- />
- </Form.Item>
- {/** 宠物专用 */}
- {productName === 'VETDROS' && (
- <Form.Item
- label={
- <FormattedMessage
- id="register.sexNeutered"
- defaultMessage="register.sexNeutered"
- />
- }
- name="sex_neutered"
- required={registerFormFields.sex_neutered.required}
- validateTrigger={registerFormFields.sex_neutered.trigger}
- rules={registerFormFields.sex_neutered.validation}
- >
- <Select
- defaultValue={'UNALTERED'}
- placeholder={intl.formatMessage({
- id: 'register.sexNeutered.placeholder',
- defaultMessage: 'register.sexNeutered.placeholder',
- })}
- >
- <Select.Option value="ALTERED">
- {intl.formatMessage({
- id: 'register.sexNeutered.altered',
- defaultMessage: 'ALTERED',
- })}
- </Select.Option>
- <Select.Option value="UNALTERED">
- {intl.formatMessage({
- id: 'register.sexNeutered.unaltered',
- defaultMessage: 'UNALTERED',
- })}
- </Select.Option>
- </Select>
- </Form.Item>
- )}
- {/** 人类专用 */}
- {productName === 'DROS' && form?.getFieldValue('patient_sex') && (
- <Form.Item
- hidden={pregnancyStatusFieldVisible}
- label={
- <FormattedMessage
- id="register.pregnancyStatus"
- defaultMessage="register.pregnancyStatus"
- />
- }
- name="pregnancy_status"
- required={registerFormFields.pregnancy_status.required}
- validateTrigger={registerFormFields.pregnancy_status.trigger}
- rules={registerFormFields.pregnancy_status.validation}
- initialValue={PregnancyStatus.UNKNOWN}
- >
- <Radio.Group
- options={pregnancyStatusOptions}
- optionType="button"
- buttonStyle="solid"
- />
- </Form.Item>
- )}
- {/** 宠物专用 */}
- {productName === 'VETDROS' && (
- <Form.Item
- label={
- <FormattedMessage
- id="register.chipNumber"
- defaultMessage="register.chipNumber"
- />
- }
- name="chip_number"
- required={registerFormFields.chip_number.required}
- validateTrigger={registerFormFields.chip_number.trigger}
- rules={registerFormFields.chip_number.validation}
- >
- <Input
- placeholder={intl.formatMessage({
- id: 'register.chipNumber.placeholder',
- defaultMessage: 'register.chipNumber.placeholder',
- })}
- />
- </Form.Item>
- )}
- {/** 宠物专用 */}
- {productName === 'VETDROS' && (
- <Form.Item
- label={
- <FormattedMessage
- id="register.variety"
- defaultMessage="register.variety"
- />
- }
- name="variety"
- required={registerFormFields.variety.required}
- validateTrigger={registerFormFields.variety.trigger}
- rules={registerFormFields.variety.validation}
- >
- <Input
- placeholder={intl.formatMessage({
- id: 'register.variety.placeholder',
- defaultMessage: 'register.variety.placeholder',
- })}
- />
- </Form.Item>
- )}
- {/* <Form.Item
- label={
- <FormattedMessage
- id="register.patientType"
- defaultMessage="register.patientType"
- />
- }
- name="patient_type"
- required={registerFormFields.patient_type.required}
- validateTrigger={registerFormFields.patient_type.trigger}
- rules={registerFormFields.patient_type.validation}
- >
- <Input
- placeholder={intl.formatMessage({
- id: 'register.patientType.placeholder',
- defaultMessage: 'register.patientType.placeholder',
- })}
- />
- </Form.Item> */}
- <Form.Item
- label={
- <FormattedMessage
- id="register.referringPhysician"
- defaultMessage="register.referringPhysician"
- />
- }
- name="ref_physician"
- required={registerFormFields.ref_physician.required}
- validateTrigger={registerFormFields.ref_physician.trigger}
- rules={registerFormFields.ref_physician.validation}
- >
- <Input
- placeholder={intl.formatMessage({
- id: 'register.referringPhysician.placeholder',
- defaultMessage: 'register.referringPhysician.placeholder',
- })}
- />
- </Form.Item>
- <Form.Item
- label={
- <FormattedMessage
- id="register.operatorId"
- defaultMessage="register.operatorId"
- />
- }
- name="operator_id"
- required={registerFormFields.operator_id.required}
- validateTrigger={registerFormFields.operator_id.trigger}
- rules={registerFormFields.operator_id.validation}
- >
- <Input
- placeholder={intl.formatMessage({
- id: 'register.operatorId.placeholder',
- defaultMessage: 'register.operatorId.placeholder',
- })}
- />
- </Form.Item>
- <Form.Item
- label={
- <FormattedMessage
- id="register.weight"
- defaultMessage="register.weight"
- />
- }
- name="weight"
- required={registerFormFields.weight.required}
- validateTrigger={registerFormFields.weight.trigger}
- rules={registerFormFields.weight.validation}
- >
- <InputNumber min={0} addonAfter="kg" style={{ width: '100%' }} />
- </Form.Item>
- <Form.Item
- label={
- <FormattedMessage
- id="register.thickness"
- defaultMessage="register.thickness"
- />
- }
- name="thickness"
- required={registerFormFields.thickness.required}
- validateTrigger={registerFormFields.thickness.trigger}
- rules={registerFormFields.thickness.validation}
- >
- <InputNumber min={0} addonAfter="cm" style={{ width: '100%' }} />
- </Form.Item>
- <Form.Item
- label={
- <FormattedMessage
- id="register.height"
- defaultMessage="register.height"
- />
- }
- name="length"
- required={registerFormFields.length.required}
- validateTrigger={registerFormFields.length.trigger}
- rules={registerFormFields.length.validation}
- >
- <InputNumber min={0} addonAfter="cm" style={{ width: '100%' }} />
- </Form.Item>
- <Form.Item
- label={
- <FormattedMessage
- id="register.comment"
- defaultMessage="register.comment"
- />
- }
- name="comment"
- required={registerFormFields.comment.required}
- validateTrigger={registerFormFields.comment.trigger}
- rules={registerFormFields.comment.validation}
- >
- <Input
- placeholder={intl.formatMessage({
- id: 'register.comment.placeholder',
- defaultMessage: 'register.comment.placeholder',
- })}
- />
- </Form.Item>
- {/* 扫码枪兜底 input(隐藏) */}
- <input
- ref={scanInputRef}
- type="text"
- style={{
- position: 'fixed',
- opacity: 0,
- pointerEvents: 'none',
- left: -9999,
- }}
- onCompositionStart={() => setIsComposing(true)}
- onCompositionEnd={(e) => {
- setIsComposing(false);
- bufferRef.current = e.currentTarget.value;
- }}
- onInput={handleScanInput}
- autoComplete="off"
- />
- </Form>
- );
- };
- export default BasicInfoForm;
|