记录 @ant-design/pro-form 表单身份证号联动
这只是我在学习使用 umi 过程中的一次记录,以下代码并没有跑在生产环境,仅供参考。当然,并不是完全没有试验,我们线上项目是使用 vue2 写的,部分逻辑判断还是跑在了生产环境。
1. 需求
在用户输入正确的身份证号码后,自动生成年龄,性别和出生日期。
2. 需求分析
我们知道,身份证号码包含有用户的部分信息,我们可以将这部分信息提取出来,让用户少填写一些字段,简化用户的输入。
何为正确的身份证号码?按照常规来分析(只分析二代身份证,一代身份证不探究)。可能考虑不是很全,如果还有其他的,可以自己尝试去添加。(如果不太好添加,可以留言一起讨论)
- 长度为 18
- 由 18 位数字或 17 位数字 + X 组成
- 关联身份证生日部分,月份不能大于 12
- 关联身份证生日部分,当前月的号数不能大于当前月的天数
当然,你还可以加判断,例如年龄计算出来有几百岁了,也可以提示一下用户身份证号码是不是写错了。
3. 一些零零碎碎的提示
因为是第一次使用 umi 直接创建的项目,里面封装好的 pro-form 可能使用起来并不是很熟练,毕竟我在上一次使用 react 写项目的时候,那时候的 react 最新版本还不是 16.8。
如果有不好的地方请指正,感谢。
4. 开始编码
4.1 表单部分
src/pages/testForm/index.tsx
import { Card, Space, Popconfirm } from 'antd';
import { PageContainer } from '@ant-design/pro-layout';
import type {
ProFormInstance
} from '@ant-design/pro-form';
import ProForm, {
ProFormText,
ProFormSelect,
ProFormDatePicker,
ProFormUploadButton,
ProFormCascader,
ProFormDigit
} from '@ant-design/pro-form';
import { Row, Col, Button } from 'antd';
import { useRef } from 'react';
import useSiteList from '@/hooks/useSiteList';
import styles from './index.less';
import useDictData from '@/hooks/useDictData';
import { validateIdCard } from '@/utils/rules';
const fieldLabels = {
// 其他字段省略...
cardId: '身份证号',
sex: '患者性别',
birthday: '出生日期',
age: '患者年龄'
};
const TestForm = () => {
const formRef = useRef<
ProFormInstance<{
// 其他字段省略...
cardId: string;
sex: string;
birthday: string;
age: number;
}>
>();
const { siteList } = useSiteList(false, 'workstation,site,center');
const { dictArr: insuranceArr } = useDictData('insured_situation');
const { dictArr: employmentStatusArr } = useDictData('employment_status');
const { dictArr: educationalLevelArr } = useDictData('culture_degree');
// 提交表单
const onFinish = async (values: Record<string, any>) => {
console.log(values);
// 代码省略...
}
// 重置表单
const onReset = () => {
formRef.current?.resetFields();
}
// 生成生日
const genBirthday = (birthday: string) => {
formRef.current?.setFieldsValue({ birthday });
}
// 生成年龄
const genAge = (birthday: string) => {
const birth = Date.parse(birthday.replace('/-/g', '/'));
const year = 1000 * 60 * 60 * 24 * 365;
const now = new Date().valueOf();
const _birthday = new Date(birth).valueOf();
const age = parseInt(((now - _birthday) / year) + '');
formRef.current?.setFieldsValue({ age });
}
// 生成性别
const genSex = (num: number) => {
const sex = num % 2 === 0 ? '2' : '1';
formRef.current?.setFieldsValue({ sex });
}
// 联动生成函数
const linkage = () => {
return {
// 身份证号输入自动生成生日、年龄和性别的函数
cardId: (val: string) => {
const birthday = val.substring(6, 10) + '-' + val.substring(10, 12) + '-' + val.substring(12, 14);
genBirthday(birthday);
genAge(birthday);
genSex(parseInt(val[16]));
},
// 用户选择生日自动生成年龄函数
birthday: (val: string) => {
genAge(val);
}
}
}
// 基础信息表单输入变化时,部分字段要联动,
// 该函数是在用户输入的时候就调用,所以可以使用防抖
const onValuesChange = async (changeValues: Record<string, any>) => {
// 需要联动的字段数组,并不是每个字段都要联动,
// 所以使用一个数组来存储要联动的字段,
// 当然,如果你表单很大,联动字段很多,可以提出去单独维护
const linkageFields = ['cardId', 'birthday'];
const key = Object.keys(changeValues)[0];
// 如果字段不是需要联动的字段,则直接返回
if (linkageFields.indexOf(key) === -1) return;
const val = changeValues[key];
// 将获取表单错误信息的逻辑加入到异步宏任务中
// 因为底层封装表单的错误信息是通过异步更改的
// 当然你可以使用其他方式,或者说 pro-form 封装了更好的方法,只是我没有找到
setTimeout(() => {
// 获取表单提示的错误信息数组的长度
// 如果长度为 0,则表示基础验证通过,可以提交表单
const errLen = formRef.current?.getFieldError(key).length;
if (errLen === 0) {
// 调用联动生成函数,当然,你也可以使用对象的方式
// 使用函数的方式方便以后万一要对数据再做一些处理,预留起,方便以后罢了
linkage()[key](val);
}
})
}
return (
<PageContainer>
<ProForm
onFinish={onFinish}
submitter={false}
formRef={formRef}
onValuesChange={onValuesChange}
>
{/* 如果字段很多,可以参考官方文档中的 Schema Form,很简单的,这里就不作探讨 */}
<Card title="基本信息" className={styles.card} bordered={false}>
<Row gutter={24}>
<Col lg={16} sm={24}>
<Row gutter={24}>
{/* 其他字段代码省略... */}
<Col xl={12} md={12} sm={24}>
<ProFormText
label={fieldLabels.cardId}
rules={[{ required: true, validator: validateIdCardNum }]}
name="cardId"
placeholder="请输入身份证号"
/>
</Col>
</Row>
</Col>
</Row>
<Row gutter={24}>
<Col xl={8} md={12} sm={24}>
<ProFormSelect
label={fieldLabels.sex}
name="sex"
rules={[{ required: true, message: '请选择患者性别' }]}
options={[
{ value: '1', label: '男' },
{ value: '2', label: '女' },
]}
placeholder="请选择患者性别"
/>
</Col>
<Col xl={8} md={12} sm={24}>
<ProFormDatePicker
label={fieldLabels.birthday}
name="birthday"
fieldProps={{
style: {
width: '100%',
},
}}
rules={[{ required: true, message: '请选择出生日期' }]}
/>
</Col>
<Col xl={8} md={12} sm={24}>
<ProFormText
label={fieldLabels.age}
name="age"
placeholder="请输入患者年龄"
/>
</Col>
{/* 其他字段省略... */}
<Col xl={8} md={12} sm={24}>
<ProFormText label=" ">
<Space>
<Popconfirm title="是否重置表单?重置后表单所填写内容将完全清空!" onConfirm={onReset}>
<Button htmlType="button" key="reset">重置</Button>
</Popconfirm>
<Button type="primary" htmlType="submit">提交</Button>
</Space>
</ProFormText>
</Col>
</Row>
</Card>
</ProForm>
</PageContainer>
)
}
export default TestForm;
4.2 验证身份证号部分
src/utils/rules.ts
import { getLastDayOfMonth } from '@/utils/utils';
import type { RuleObject } from 'rc-field-form/lib/interface';
// 主要是为了防止编码过程中 TS 报错,又不想写 any,
// 所以就去看了 pro-form 底层的一些代码写出来的,
// 不懂也没关系,反正也不影响是不
type Validator = (rule: RuleObject, value: any) => Promise<void | any> | void;
/**
* 判断身份证号码
* 写得很乱,你可以继续优化,比如抽离优化代码结构,封装什么的
* @param {*} rule
* @param {*} value
*/
export const validateIdCard: Validator = (rule, value: string) => {
if (value.length !== 18) {
return Promise.reject(new Error('身份证号码长度为18位【二代身份证】'))
}
if (value.length === 18) {
if (!/[0-9]{17}[0-9xX]$/.test(value)) {
return Promise.reject(new Error('请输入正确格式的身份证号'))
}
const _yearStr = value.substring(6, 10)
const _year = _yearStr && parseInt(_yearStr) || 0
const _monthStr = value.substring(10, 12)
const _month = _monthStr && parseInt(_monthStr) || 0
if (_month > 12) {
return Promise.reject(new Error('请输入正确格式的身份证号【生日部分月份不能大于12】'))
}
const _lastDay = getLastDayOfMonth(_year, _month)
const _maxDay = _lastDay.substring(_lastDay.length - 2, _lastDay.length)
if (value.substring(12, 14) > _maxDay) {
return Promise.reject(new Error(`请输入正确格式的身份证号【生日部分当月天数不能大于${_maxDay}】`))
}
}
return Promise.resolve()
}
4.3 一个封装的工具函数,获取某年某月的最后一天
src/utils/utils.ts
/**
* 获取某年某月的最后一天
* 当然是随便写的,没有经过大量的测试
* @param {Number} year 年份
* @param {Number} month 月份
* @returns {String} 返回当前传入年传入月的最后一天
*/
export function getLastDayOfMonth (year: number, month: number) {
const date = new Date(year, month - 1, 1)
// 设置日期
date.setDate(1)
// 设置月份
date.setMonth(date.getMonth() + 1)
// 获取本月的最后一天
const lastDay = new Date(date.getTime() - 1000 * 60 * 60 * 24)
// 返回结果
return year + '-' + month + '-' + lastDay.getDate()
}