Prechádzať zdrojové kódy

feat: 残疾人管理表单优化 - 身份证和残疾证号自动解析

实现功能:
- 身份证号自动解析:输入身份证号后自动填充性别和出生日期字段
- 残疾证号自动解析:输入残疾证号后自动填充残疾类别和等级字段
- 新增 idCardParser.ts 工具:支持15位和18位身份证号解析
- 新增 disabilityIdParser.ts 工具:支持从残疾证号提取残疾类别和等级
- 添加完整的单元测试覆盖

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 1 deň pred
rodič
commit
3f69c044c1

+ 86 - 1
allin-packages/disability-person-management-ui/src/components/DisabilityPersonManagement.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
 import { useQuery, useMutation } from '@tanstack/react-query';
 import { Plus, Edit, Trash2, Search, Eye } from 'lucide-react';
 import { format } from 'date-fns';
@@ -28,6 +28,8 @@ import PhotoPreview from './PhotoPreview';
 import BankCardManagement, { type BankCardItem } from './BankCardManagement';
 import RemarkManagement, { type RemarkItem } from './RemarkManagement';
 import VisitManagement, { type VisitItem } from './VisitManagement';
+import { parseIdCard } from '../utils/idCardParser';
+import { parseDisabilityId } from '../utils/disabilityIdParser';
 
 interface DisabilityPersonSearchParams {
   page: number;
@@ -93,6 +95,89 @@ const DisabilityPersonManagement: React.FC = () => {
     defaultValues: {}
   });
 
+  // 身份证号自动解析 - 创建表单
+  const idCardWatchValue = createForm.watch('idCard');
+  useEffect(() => {
+    if (idCardWatchValue && idCardWatchValue.length >= 15) {
+      const result = parseIdCard(idCardWatchValue);
+      if (result.isValid) {
+        // 只有在字段为空或值不同时才更新(避免覆盖用户输入)
+        const currentGender = createForm.getValues('gender');
+        const currentBirthDate = createForm.getValues('birthDate');
+
+        if (result.gender && (!currentGender || currentGender === '')) {
+          createForm.setValue('gender', result.gender);
+        }
+        if (result.birthDate && (!currentBirthDate || currentBirthDate === undefined)) {
+          // 将字符串日期转换为 Date 对象
+          const birthDateObj = result.birthDate === null ? undefined : new Date(result.birthDate);
+          createForm.setValue('birthDate', birthDateObj);
+        }
+      }
+    }
+  }, [idCardWatchValue, createForm]);
+
+  // 身份证号自动解析 - 更新表单
+  const updateIdCardWatchValue = updateForm.watch('idCard');
+  useEffect(() => {
+    if (updateIdCardWatchValue && updateIdCardWatchValue.length >= 15) {
+      const result = parseIdCard(updateIdCardWatchValue);
+      if (result.isValid) {
+        // 只有在字段为空或值不同时才更新(避免覆盖用户输入)
+        const currentGender = updateForm.getValues('gender');
+        const currentBirthDate = updateForm.getValues('birthDate');
+
+        if (result.gender && (!currentGender || currentGender === '')) {
+          updateForm.setValue('gender', result.gender);
+        }
+        if (result.birthDate && (!currentBirthDate || currentBirthDate === undefined)) {
+          // 将字符串日期转换为 Date 对象
+          const birthDateObj = result.birthDate === null ? undefined : new Date(result.birthDate);
+          updateForm.setValue('birthDate', birthDateObj);
+        }
+      }
+    }
+  }, [updateIdCardWatchValue, updateForm]);
+
+  // 残疾证号自动解析 - 创建表单
+  const disabilityIdWatchValue = createForm.watch('disabilityId');
+  useEffect(() => {
+    if (disabilityIdWatchValue && disabilityIdWatchValue.length > 0) {
+      const result = parseDisabilityId(disabilityIdWatchValue);
+      if (result.isValid) {
+        // 只有在字段为空或值不同时才更新(避免覆盖用户输入)
+        const currentDisabilityType = createForm.getValues('disabilityType');
+        const currentDisabilityLevel = createForm.getValues('disabilityLevel');
+
+        if (result.disabilityType && (!currentDisabilityType || currentDisabilityType === '')) {
+          createForm.setValue('disabilityType', result.disabilityType);
+        }
+        if (result.disabilityLevel && (!currentDisabilityLevel || currentDisabilityLevel === '')) {
+          createForm.setValue('disabilityLevel', result.disabilityLevel);
+        }
+      }
+    }
+  }, [disabilityIdWatchValue, createForm]);
+
+  // 残疾证号自动解析 - 更新表单
+  const updateDisabilityIdWatchValue = updateForm.watch('disabilityId');
+  useEffect(() => {
+    if (updateDisabilityIdWatchValue && updateDisabilityIdWatchValue.length > 0) {
+      const result = parseDisabilityId(updateDisabilityIdWatchValue);
+      if (result.isValid) {
+        // 只有在字段为空或值不同时才更新(避免覆盖用户输入)
+        const currentDisabilityType = updateForm.getValues('disabilityType');
+        const currentDisabilityLevel = updateForm.getValues('disabilityLevel');
+
+        if (result.disabilityType && (!currentDisabilityType || currentDisabilityType === '')) {
+          updateForm.setValue('disabilityType', result.disabilityType);
+        }
+        if (result.disabilityLevel && (!currentDisabilityLevel || currentDisabilityLevel === '')) {
+          updateForm.setValue('disabilityLevel', result.disabilityLevel);
+        }
+      }
+    }
+  }, [updateDisabilityIdWatchValue, updateForm]);
 
   // 数据查询
   const { data, isLoading, refetch } = useQuery({

+ 154 - 0
allin-packages/disability-person-management-ui/src/utils/disabilityIdParser.ts

@@ -0,0 +1,154 @@
+/**
+ * 残疾证号解析工具函数
+ * 用于从残疾证号中提取残疾类别和等级信息
+ */
+
+/**
+ * 残疾证解析结果接口
+ */
+export interface DisabilityIdParseResult {
+  /** 残疾类别 */
+  disabilityType: string | null;
+  /** 残疾等级 */
+  disabilityLevel: string | null;
+  /** 是否成功解析 */
+  isValid: boolean;
+}
+
+/**
+ * 支持的残疾类别列表
+ */
+export const DISABILITY_TYPES = [
+  '视力残疾',
+  '听力残疾',
+  '言语残疾',
+  '肢体残疾',
+  '智力残疾',
+  '精神残疾',
+  '多重残疾'
+] as const;
+
+/**
+ * 支持的残疾等级列表
+ */
+export const DISABILITY_LEVELS = [
+  '一级',
+  '二级',
+  '三级',
+  '四级'
+] as const;
+
+/**
+ * 从残疾证号中解析残疾类别和等级
+ *
+ * 残疾证号常见格式:
+ * - [类别]残疾[序列号][等级] 如: "视力残疾123456一级"
+ * - [类别][序列号][等级] 如: "肢体123456二级"
+ *
+ * @param disabilityId 残疾证号
+ * @returns 解析结果
+ */
+export function parseDisabilityId(disabilityId: string): DisabilityIdParseResult {
+  if (!disabilityId || disabilityId.trim().length === 0) {
+    return { disabilityType: null, disabilityLevel: null, isValid: false };
+  }
+
+  const cleanedId = disabilityId.trim();
+
+  // 解析残疾类别
+  const disabilityType = extractDisabilityType(cleanedId);
+
+  // 解析残疾等级
+  const disabilityLevel = extractDisabilityLevel(cleanedId);
+
+  // 至少解析出一个字段才算成功
+  const isValid = disabilityType !== null || disabilityLevel !== null;
+
+  return { disabilityType, disabilityLevel, isValid };
+}
+
+/**
+ * 从残疾证号中提取残疾类别
+ *
+ * @param disabilityId 残疾证号
+ * @returns 残疾类别,未找到返回 null
+ */
+function extractDisabilityType(disabilityId: string): string | null {
+  // 按优先级匹配,先匹配完整类别名称
+  // 完整格式: "视力残疾"
+  const fullMatch = disabilityId.match(/(视力残疾|听力残疾|言语残疾|肢体残疾|智力残疾|精神残疾|多重残疾)/);
+  if (fullMatch) {
+    return fullMatch[1];
+  }
+
+  // 简写格式: "视力"、"听力"等(后面可能跟"残"或其他字符)
+  const shortMatch = disabilityId.match(/(视力|听力|言语|肢体|智力|精神|多重)/);
+  if (shortMatch) {
+    return `${shortMatch[1]}残疾`;
+  }
+
+  return null;
+}
+
+/**
+ * 从残疾证号中提取残疾等级
+ *
+ * @param disabilityId 残疾证号
+ * @returns 残疾等级,未找到返回 null
+ */
+function extractDisabilityLevel(disabilityId: string): string | null {
+  // 匹配等级: "一级"、"二级"、"三级"、"四级"
+  // 注意:不能使用\b边界,因为中文不支持单词边界
+  const levelPattern = /([一二三四]级)(?=[^一二三四]|$)/;
+
+  const match = disabilityId.match(levelPattern);
+  if (match) {
+    return match[1];
+  }
+
+  // 尝试匹配数字格式: "1级"、"2级"等
+  const digitPattern = /([1-4])级(?=[^0-9]|$)/;
+  const digitMatch = disabilityId.match(digitPattern);
+
+  if (digitMatch) {
+    const digit = digitMatch[1];
+    const chineseNums = ['一', '二', '三', '四'];
+    return `${chineseNums[parseInt(digit, 10) - 1]}级`;
+  }
+
+  return null;
+}
+
+/**
+ * 获取标准化的残疾类别列表
+ * @returns 所有支持的残疾类别
+ */
+export function getStandardDisabilityTypes(): readonly string[] {
+  return DISABILITY_TYPES;
+}
+
+/**
+ * 获取标准化的残疾等级列表
+ * @returns 所有支持的残疾等级
+ */
+export function getStandardDisabilityLevels(): readonly string[] {
+  return DISABILITY_LEVELS;
+}
+
+/**
+ * 验证残疾类别是否有效
+ * @param type 残疾类别
+ * @returns 是否为有效的残疾类别
+ */
+export function isValidDisabilityType(type: string): boolean {
+  return DISABILITY_TYPES.includes(type as any);
+}
+
+/**
+ * 验证残疾等级是否有效
+ * @param level 残疾等级
+ * @returns 是否为有效的残疾等级
+ */
+export function isValidDisabilityLevel(level: string): boolean {
+  return DISABILITY_LEVELS.includes(level as any);
+}

+ 180 - 0
allin-packages/disability-person-management-ui/src/utils/idCardParser.ts

@@ -0,0 +1,180 @@
+/**
+ * 身份证解析工具函数
+ * 用于从中国居民身份证号中提取性别和出生日期信息
+ */
+
+/**
+ * 身份证解析结果接口
+ */
+export interface IdCardParseResult {
+  /** 性别: 男/女 */
+  gender: '男' | '女' | null;
+  /** 出生日期 YYYY-MM-DD 格式 */
+  birthDate: string | null;
+  /** 是否成功解析 */
+  isValid: boolean;
+}
+
+/**
+ * 解析身份证号,提取性别和出生日期
+ * @param idCard 身份证号(支持15位或18位)
+ * @returns 解析结果
+ */
+export function parseIdCard(idCard: string): IdCardParseResult {
+  // 清理输入:移除空格和非数字字符(X保留)
+  const cleanedId = idCard.replace(/\s/g, '').toUpperCase();
+
+  // 基本验证:必须是15位或18位
+  if (!cleanedId) {
+    return { gender: null, birthDate: null, isValid: false };
+  }
+
+  // 18位身份证解析
+  if (cleanedId.length === 18) {
+    return parse18DigitIdCard(cleanedId);
+  }
+
+  // 15位身份证解析
+  if (cleanedId.length === 15) {
+    return parse15DigitIdCard(cleanedId);
+  }
+
+  // 长度不符合标准
+  return { gender: null, birthDate: null, isValid: false };
+}
+
+/**
+ * 解析18位身份证号
+ * 格式: 6位地区码 + 8位出生日期(YYYYMMDD) + 3位顺序码 + 1位校验码
+ * 第17位奇数=男,偶数=女
+ */
+function parse18DigitIdCard(idCard: string): IdCardParseResult {
+  // 验证是否全为数字(最后一位可以是X)
+  const body = idCard.substring(0, 17);
+  const checkBit = idCard[17];
+
+  if (!/^\d{17}$/.test(body) || !/^[\dX]$/.test(checkBit)) {
+    return { gender: null, birthDate: null, isValid: false };
+  }
+
+  // 提取出生日期: 第7-14位 (YYYYMMDD)
+  const birthDateStr = idCard.substring(6, 14);
+  const birthDate = formatBirthDate(birthDateStr, 'YYYYMMDD');
+
+  // 如果出生日期无效,整体解析失败
+  if (birthDate === null) {
+    return { gender: null, birthDate: null, isValid: false };
+  }
+
+  // 提取性别: 第17位,奇数=男,偶数=女
+  const genderCode = parseInt(idCard[16], 10);
+  const gender = genderCode % 2 === 1 ? '男' : '女';
+
+  return { gender, birthDate, isValid: true };
+}
+
+/**
+ * 解析15位身份证号
+ * 格式: 6位地区码 + 6位出生日期(YYMMDD) + 3位顺序码
+ * 第15位奇数=男,偶数=女
+ */
+function parse15DigitIdCard(idCard: string): IdCardParseResult {
+  // 验证是否全为数字
+  if (!/^\d{15}$/.test(idCard)) {
+    return { gender: null, birthDate: null, isValid: false };
+  }
+
+  // 提取出生日期: 第7-12位 (YYMMDD)
+  // 15位身份证的年份是2位,需要转换为4位(假设19XX年)
+  const birthDateStr = idCard.substring(6, 12);
+  const birthDate = formatBirthDate(birthDateStr, 'YYMMDD');
+
+  // 如果出生日期无效,整体解析失败
+  if (birthDate === null) {
+    return { gender: null, birthDate: null, isValid: false };
+  }
+
+  // 提取性别: 第15位,奇数=男,偶数=女
+  const genderCode = parseInt(idCard[14], 10);
+  const gender = genderCode % 2 === 1 ? '男' : '女';
+
+  return { gender, birthDate, isValid: true };
+}
+
+/**
+ * 格式化出生日期字符串为 YYYY-MM-DD 格式
+ * @param dateStr 日期字符串
+ * @param format 输入格式: 'YYYYMMDD' 或 'YYMMDD'
+ * @returns 格式化后的日期字符串,无效时返回 null
+ */
+function formatBirthDate(dateStr: string, format: 'YYYYMMDD' | 'YYMMDD'): string | null {
+  let year: string;
+  let month: string;
+  let day: string;
+
+  if (format === 'YYYYMMDD') {
+    year = dateStr.substring(0, 4);
+    month = dateStr.substring(4, 6);
+    day = dateStr.substring(6, 8);
+  } else {
+    // YYMMDD: 假设19XX年(15位身份证主要是老版)
+    const twoDigitYear = dateStr.substring(0, 2);
+    year = `19${twoDigitYear}`;
+    month = dateStr.substring(2, 4);
+    day = dateStr.substring(4, 6);
+  }
+
+  // 验证日期的有效性 - 构造实际日期对象进行验证
+  const monthNum = parseInt(month, 10);
+  const dayNum = parseInt(day, 10);
+  const yearNum = parseInt(year, 10);
+
+  // 验证月份范围
+  if (monthNum < 1 || monthNum > 12) {
+    return null;
+  }
+
+  // 验证日期范围(根据月份)
+  const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+
+  // 处理闰年二月
+  const isLeapYear = (yearNum % 4 === 0 && yearNum % 100 !== 0) || (yearNum % 400 === 0);
+  if (isLeapYear && monthNum === 2) {
+    if (dayNum < 1 || dayNum > 29) {
+      return null;
+    }
+  } else {
+    if (dayNum < 1 || dayNum > daysInMonth[monthNum - 1]) {
+      return null;
+    }
+  }
+
+  // 验证年份的合理性(1900-当前年份,不大于当前年份)
+  const currentYear = new Date().getFullYear();
+  if (yearNum < 1900 || yearNum > currentYear) {
+    return null;
+  }
+
+  return `${year}-${month}-${day}`;
+}
+
+/**
+ * 验证身份证号格式是否正确
+ * @param idCard 身份证号
+ * @returns 是否有效
+ */
+export function isValidIdCardFormat(idCard: string): boolean {
+  const cleanedId = idCard.replace(/\s/g, '').toUpperCase();
+
+  if (cleanedId.length === 18) {
+    const body = cleanedId.substring(0, 17);
+    const checkBit = cleanedId[17];
+    return /^\d{17}$/.test(body) && /^[\dX]$/.test(checkBit);
+  }
+
+  if (cleanedId.length === 15) {
+    return /^\d{15}$/.test(cleanedId);
+  }
+
+  return false;
+}

+ 200 - 0
allin-packages/disability-person-management-ui/tests/utils/disabilityIdParser.test.ts

@@ -0,0 +1,200 @@
+/**
+ * 残疾证号解析工具函数单元测试
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+  parseDisabilityId,
+  getStandardDisabilityTypes,
+  getStandardDisabilityLevels,
+  isValidDisabilityType,
+  isValidDisabilityLevel
+} from '../../src/utils/disabilityIdParser';
+
+describe('disabilityIdParser', () => {
+  describe('parseDisabilityId - 标准格式解析', () => {
+    it('应该正确解析"视力残疾123456一级"格式', () => {
+      const result = parseDisabilityId('视力残疾123456一级');
+      expect(result.disabilityType).toBe('视力残疾');
+      expect(result.disabilityLevel).toBe('一级');
+      expect(result.isValid).toBe(true);
+    });
+
+    it('应该正确解析"肢体残疾987654二级"格式', () => {
+      const result = parseDisabilityId('肢体残疾987654二级');
+      expect(result.disabilityType).toBe('肢体残疾');
+      expect(result.disabilityLevel).toBe('二级');
+      expect(result.isValid).toBe(true);
+    });
+
+    it('应该正确解析"听力残疾111111三级"格式', () => {
+      const result = parseDisabilityId('听力残疾111111三级');
+      expect(result.disabilityType).toBe('听力残疾');
+      expect(result.disabilityLevel).toBe('三级');
+      expect(result.isValid).toBe(true);
+    });
+
+    it('应该正确解析"精神残疾222222四级"格式', () => {
+      const result = parseDisabilityId('精神残疾222222四级');
+      expect(result.disabilityType).toBe('精神残疾');
+      expect(result.disabilityLevel).toBe('四级');
+      expect(result.isValid).toBe(true);
+    });
+  });
+
+  describe('parseDisabilityId - 简写格式解析', () => {
+    it('应该正确解析"视力123456一级"简写格式', () => {
+      const result = parseDisabilityId('视力123456一级');
+      expect(result.disabilityType).toBe('视力残疾');
+      expect(result.disabilityLevel).toBe('一级');
+      expect(result.isValid).toBe(true);
+    });
+
+    it('应该正确解析"肢体987654二级"简写格式', () => {
+      const result = parseDisabilityId('肢体987654二级');
+      expect(result.disabilityType).toBe('肢体残疾');
+      expect(result.disabilityLevel).toBe('二级');
+      expect(result.isValid).toBe(true);
+    });
+
+    it('应该正确解析"言语111111三级"简写格式', () => {
+      const result = parseDisabilityId('言语111111三级');
+      expect(result.disabilityType).toBe('言语残疾');
+      expect(result.disabilityLevel).toBe('三级');
+      expect(result.isValid).toBe(true);
+    });
+  });
+
+  describe('parseDisabilityId - 数字等级格式', () => {
+    it('应该正确解析"视力1234561级"格式', () => {
+      const result = parseDisabilityId('视力1234561级');
+      expect(result.disabilityType).toBe('视力残疾');
+      expect(result.disabilityLevel).toBe('一级');
+      expect(result.isValid).toBe(true);
+    });
+
+    it('应该正确解析"肢体9876542级"格式', () => {
+      const result = parseDisabilityId('肢体9876542级');
+      expect(result.disabilityType).toBe('肢体残疾');
+      expect(result.disabilityLevel).toBe('二级');
+      expect(result.isValid).toBe(true);
+    });
+  });
+
+  describe('parseDisabilityId - 部分匹配', () => {
+    it('应该只解析残疾类别,不解析等级', () => {
+      const result = parseDisabilityId('视力残疾123456');
+      expect(result.disabilityType).toBe('视力残疾');
+      expect(result.disabilityLevel).toBeNull();
+      expect(result.isValid).toBe(true); // 至少解析出一个字段
+    });
+
+    it('应该只解析等级,不解析类别', () => {
+      const result = parseDisabilityId('123456一级');
+      expect(result.disabilityType).toBeNull();
+      expect(result.disabilityLevel).toBe('一级');
+      expect(result.isValid).toBe(true);
+    });
+  });
+
+  describe('parseDisabilityId - 边界情况', () => {
+    it('应该处理空字符串', () => {
+      const result = parseDisabilityId('');
+      expect(result.disabilityType).toBeNull();
+      expect(result.disabilityLevel).toBeNull();
+      expect(result.isValid).toBe(false);
+    });
+
+    it('应该处理只有空格的字符串', () => {
+      const result = parseDisabilityId('   ');
+      expect(result.disabilityType).toBeNull();
+      expect(result.disabilityLevel).toBeNull();
+      expect(result.isValid).toBe(false);
+    });
+
+    it('应该处理无法匹配的格式', () => {
+      const result = parseDisabilityId('ABC123');
+      expect(result.disabilityType).toBeNull();
+      expect(result.disabilityLevel).toBeNull();
+      expect(result.isValid).toBe(false);
+    });
+
+    it('应该处理带空格的残疾证号', () => {
+      const result = parseDisabilityId(' 视力残疾 123456 一级 ');
+      expect(result.disabilityType).toBe('视力残疾');
+      expect(result.disabilityLevel).toBe('一级');
+      expect(result.isValid).toBe(true);
+    });
+  });
+
+  describe('parseDisabilityId - 所有残疾类别', () => {
+    it('应该正确解析所有七种残疾类别', () => {
+      const types = [
+        '视力残疾',
+        '听力残疾',
+        '言语残疾',
+        '肢体残疾',
+        '智力残疾',
+        '精神残疾',
+        '多重残疾'
+      ];
+
+      types.forEach(type => {
+        const result = parseDisabilityId(`${type}123456一级`);
+        expect(result.disabilityType).toBe(type);
+        expect(result.isValid).toBe(true);
+      });
+    });
+  });
+
+  describe('getStandardDisabilityTypes', () => {
+    it('应该返回所有七种标准残疾类别', () => {
+      const types = getStandardDisabilityTypes();
+      expect(types).toEqual([
+        '视力残疾',
+        '听力残疾',
+        '言语残疾',
+        '肢体残疾',
+        '智力残疾',
+        '精神残疾',
+        '多重残疾'
+      ]);
+    });
+  });
+
+  describe('getStandardDisabilityLevels', () => {
+    it('应该返回所有四种标准残疾等级', () => {
+      const levels = getStandardDisabilityLevels();
+      expect(levels).toEqual(['一级', '二级', '三级', '四级']);
+    });
+  });
+
+  describe('isValidDisabilityType', () => {
+    it('应该验证有效的残疾类别', () => {
+      expect(isValidDisabilityType('视力残疾')).toBe(true);
+      expect(isValidDisabilityType('肢体残疾')).toBe(true);
+      expect(isValidDisabilityType('多重残疾')).toBe(true);
+    });
+
+    it('应该拒绝无效的残疾类别', () => {
+      expect(isValidDisabilityType('其他残疾')).toBe(false);
+      expect(isValidDisabilityType('视力')).toBe(false);
+      expect(isValidDisabilityType('')).toBe(false);
+    });
+  });
+
+  describe('isValidDisabilityLevel', () => {
+    it('应该验证有效的残疾等级', () => {
+      expect(isValidDisabilityLevel('一级')).toBe(true);
+      expect(isValidDisabilityLevel('二级')).toBe(true);
+      expect(isValidDisabilityLevel('三级')).toBe(true);
+      expect(isValidDisabilityLevel('四级')).toBe(true);
+    });
+
+    it('应该拒绝无效的残疾等级', () => {
+      expect(isValidDisabilityLevel('五级')).toBe(false);
+      expect(isValidDisabilityLevel('1级')).toBe(false);
+      expect(isValidDisabilityLevel('')).toBe(false);
+    });
+  });
+});

+ 135 - 0
allin-packages/disability-person-management-ui/tests/utils/idCardParser.test.ts

@@ -0,0 +1,135 @@
+/**
+ * 身份证解析工具函数单元测试
+ */
+
+import { describe, it, expect } from 'vitest';
+import { parseIdCard, isValidIdCardFormat } from '../../src/utils/idCardParser';
+
+describe('idCardParser', () => {
+  describe('parseIdCard - 18位身份证号', () => {
+    it('应该正确解析男性身份证号', () => {
+      const result = parseIdCard('110101199001011234');
+      expect(result.gender).toBe('男');
+      expect(result.birthDate).toBe('1990-01-01');
+      expect(result.isValid).toBe(true);
+    });
+
+    it('应该正确解析女性身份证号', () => {
+      // 17位是2,偶数=女
+      const result = parseIdCard('110101199002022244');
+      expect(result.gender).toBe('女');
+      expect(result.birthDate).toBe('1990-02-02');
+      expect(result.isValid).toBe(true);
+    });
+
+    it('应该处理带有X校验码的身份证号', () => {
+      const result = parseIdCard('11010119900101123X');
+      expect(result.isValid).toBe(true);
+      expect(result.gender).toBe('男');
+    });
+
+    it('应该拒绝无效的出生日期', () => {
+      const result = parseIdCard('110101199013011234'); // 月份13无效
+      expect(result.birthDate).toBeNull();
+      expect(result.isValid).toBe(false);
+    });
+
+    it('应该拒绝非数字字符(除X校验码外)', () => {
+      const result = parseIdCard('110101A99001011234');
+      expect(result.isValid).toBe(false);
+    });
+  });
+
+  describe('parseIdCard - 15位身份证号', () => {
+    it('应该正确解析15位男性身份证号', () => {
+      const result = parseIdCard('110101900101123');
+      expect(result.gender).toBe('男'); // 第15位是3,奇数
+      expect(result.birthDate).toBe('1990-01-01');
+      expect(result.isValid).toBe(true);
+    });
+
+    it('应该正确解析15位女性身份证号', () => {
+      const result = parseIdCard('110101900101224');
+      expect(result.gender).toBe('女'); // 第15位是4,偶数
+      expect(result.birthDate).toBe('1990-01-01');
+      expect(result.isValid).toBe(true);
+    });
+  });
+
+  describe('parseIdCard - 边界情况', () => {
+    it('应该处理空字符串', () => {
+      const result = parseIdCard('');
+      expect(result.gender).toBeNull();
+      expect(result.birthDate).toBeNull();
+      expect(result.isValid).toBe(false);
+    });
+
+    it('应该处理16位(无效长度)', () => {
+      const result = parseIdCard('11010119900101123');
+      expect(result.isValid).toBe(false);
+    });
+
+    it('应该处理19位(无效长度)', () => {
+      const result = parseIdCard('1101011990010112345');
+      expect(result.isValid).toBe(false);
+    });
+
+    it('应该处理带空格的身份证号', () => {
+      const result = parseIdCard('110101 1990 0101 1234');
+      expect(result.isValid).toBe(true);
+      expect(result.gender).toBe('男');
+    });
+
+    it('应该处理小写x校验码', () => {
+      const result = parseIdCard('11010119900101123x');
+      expect(result.isValid).toBe(true);
+      expect(result.gender).toBe('男');
+    });
+  });
+
+  describe('isValidIdCardFormat', () => {
+    it('应该验证有效的18位身份证号', () => {
+      expect(isValidIdCardFormat('110101199001011234')).toBe(true);
+    });
+
+    it('应该验证有效的15位身份证号', () => {
+      expect(isValidIdCardFormat('110101900101123')).toBe(true);
+    });
+
+    it('应该拒绝无效长度的身份证号', () => {
+      expect(isValidIdCardFormat('11010119900101123')).toBe(false);
+      expect(isValidIdCardFormat('1101011990010112345')).toBe(false);
+    });
+
+    it('应该拒绝包含非法字符的身份证号', () => {
+      expect(isValidIdCardFormat('110101A99001011234')).toBe(false);
+    });
+
+    it('应该接受带X校验码的18位身份证号', () => {
+      expect(isValidIdCardFormat('11010119900101123X')).toBe(true);
+    });
+  });
+
+  describe('出生日期边界值测试', () => {
+    it('应该拒绝年份小于1900的日期', () => {
+      const result = parseIdCard('110101180001011234');
+      expect(result.birthDate).toBeNull();
+      expect(result.isValid).toBe(false);
+    });
+
+    it('应该接受当前年份的日期', () => {
+      const currentYear = new Date().getFullYear();
+      const idCard = `110101${currentYear}01011234`;
+      const result = parseIdCard(idCard);
+      expect(result.isValid).toBe(true);
+    });
+
+    it('应该拒绝大于当前年份的日期', () => {
+      const nextYear = new Date().getFullYear() + 1;
+      const idCard = `110101${nextYear}01011234`;
+      const result = parseIdCard(idCard);
+      expect(result.birthDate).toBeNull();
+      expect(result.isValid).toBe(false);
+    });
+  });
+});