Ver Fonte

style: lint-staged 自动格式化

Co-Authored-By: Claude <noreply@anthropic.com>
yourname há 3 dias atrás
pai
commit
90f4ea107c

+ 119 - 85
allin-packages/disability-person-management-ui/src/components/DisabledPersonSelector.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
 import { useQuery } from '@tanstack/react-query';
 import {
   Dialog,
@@ -31,6 +31,51 @@ import type {
   AreaSelection,
 } from '../api/types';
 
+// 黑名单状态常量
+const BLACKLIST_STATUS = {
+  IN_BLACKLIST: 1,
+  NOT_IN_BLACKLIST: 0,
+} as const;
+
+// 默认分页配置
+const DEFAULT_PAGINATION = {
+  page: 1,
+  pageSize: 10,
+} as const;
+
+// 默认搜索参数
+const createDefaultSearchParams = () => ({
+  name: '',
+  gender: '',
+  disabilityId: '',
+  phone: '',
+  disabilityType: '',
+  disabilityLevel: '',
+  ...DEFAULT_PAGINATION,
+});
+
+// 残疾类型选项
+const DISABILITY_TYPES = [
+  '视力残疾',
+  '听力残疾',
+  '言语残疾',
+  '肢体残疾',
+  '智力残疾',
+  '精神残疾',
+  '多重残疾',
+] as const;
+
+// 残疾等级选项
+const DISABILITY_LEVELS = ['一级', '二级', '三级', '四级'] as const;
+
+// 表格列定义类型
+type ColumnKey = 'name' | 'gender' | 'idCard' | 'disabilityId' | 'phone' | 'province' | 'city' | 'disabilityType' | 'disabilityLevel' | 'blacklist';
+
+interface ColumnDef {
+  key: ColumnKey;
+  label: string;
+}
+
 /**
  * 身份证号脱敏处理
  * 只显示前6位和后4位,中间用****代替
@@ -42,6 +87,18 @@ const maskIdCard = (idCard: string): string => {
   return `${idCard.slice(0, 6)}****${idCard.slice(-4)}`;
 };
 
+// 搜索参数类型
+interface SearchParams {
+  name: string;
+  gender: string;
+  disabilityId: string;
+  phone: string;
+  disabilityType: string;
+  disabilityLevel: string;
+  page: number;
+  pageSize: number;
+}
+
 interface DisabledPersonSelectorProps {
   open: boolean;
   onOpenChange: (open: boolean) => void;
@@ -50,6 +107,17 @@ interface DisabledPersonSelectorProps {
   disabledIds?: number[];
 }
 
+/**
+ * 残疾人选择器组件
+ *
+ * 功能:
+ * - 支持单选和多选模式
+ * - 多字段组合搜索(姓名、性别、残疾证号、电话、残疾类型/等级、地区)
+ * - 分页展示残疾人列表
+ * - 黑名单人员二次确认
+ * - 身份证号脱敏显示
+ * - 响应式设计,支持移动端
+ */
 const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
   open,
   onOpenChange,
@@ -58,25 +126,7 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
   disabledIds = [],
 }) => {
   // 多字段搜索状态管理
-  const [searchParams, setSearchParams] = useState<{
-    name: string;
-    gender: string;
-    disabilityId: string;
-    phone: string;
-    disabilityType: string;
-    disabilityLevel: string;
-    page: number;
-    pageSize: number;
-  }>({
-    name: '',
-    gender: '',
-    disabilityId: '',
-    phone: '',
-    disabilityType: '',
-    disabilityLevel: '',
-    page: 1,
-    pageSize: 10,
-  });
+  const [searchParams, setSearchParams] = useState<SearchParams>(createDefaultSearchParams);
   const [selectedPersons, setSelectedPersons] = useState<DisabledPersonData[]>([]);
 
   const [areaSelection, setAreaSelection] = useState<AreaSelection>({});
@@ -88,8 +138,8 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
   // 为AreaSelect创建表单上下文
   const form = useForm();
 
-  // 搜索残疾人列表
-  const { data, isLoading } = useQuery({
+  // 搜索残疾人列表(添加错误处理)
+  const { data, isLoading, error } = useQuery({
     queryKey: ['disabled-persons-search', searchParams, searchAreaNames],
     queryFn: async () => {
       // 组合多个搜索字段为keyword
@@ -110,8 +160,8 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
       const response = await disabilityClientManager.get().searchDisabledPersons.$get({
         query: {
           keyword: keyword || '',
-          skip: ((searchParams.page || 1) - 1) * (searchParams.pageSize || 10),
-          take: searchParams.pageSize || 10,
+          skip: ((searchParams.page || DEFAULT_PAGINATION.page) - 1) * (searchParams.pageSize || DEFAULT_PAGINATION.pageSize),
+          take: searchParams.pageSize || DEFAULT_PAGINATION.pageSize,
         },
       });
       if (response.status !== 200) throw new Error('搜索残疾人失败');
@@ -123,19 +173,10 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
   const personsData = data;
   const isLoadingData = isLoading;
 
-  // 重置选择器状态
+  // 重置选择器状态(使用 useCallback 优化)
   useEffect(() => {
     if (!open) {
-      setSearchParams({
-        name: '',
-        gender: '',
-        disabilityId: '',
-        phone: '',
-        disabilityType: '',
-        disabilityLevel: '',
-        page: 1,
-        pageSize: 10,
-      });
+      setSearchParams(createDefaultSearchParams());
       setSelectedPersons([]);
       setAreaSelection({});
       setSearchAreaNames({});
@@ -144,34 +185,25 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
     }
   }, [open]);
 
-  // 处理搜索
-  const handleSearch = () => {
-    setSearchParams(prev => ({ ...prev, page: 1 }));
-  };
-
-  // 处理重置搜索
-  const handleResetSearch = () => {
-    setSearchParams({
-      name: '',
-      gender: '',
-      disabilityId: '',
-      phone: '',
-      disabilityType: '',
-      disabilityLevel: '',
-      page: 1,
-      pageSize: 10,
-    });
+  // 处理搜索(使用 useCallback 优化)
+  const handleSearch = useCallback(() => {
+    setSearchParams(prev => ({ ...prev, page: DEFAULT_PAGINATION.page }));
+  }, []);
+
+  // 处理重置搜索(使用 useCallback 优化)
+  const handleResetSearch = useCallback(() => {
+    setSearchParams(createDefaultSearchParams());
     setAreaSelection({});
     setSearchAreaNames({});
-  };
+  }, []);
 
-  // 处理选择人员
-  const handleSelectPerson = (person: DisabledPersonData) => {
+  // 处理选择人员(使用 useCallback 优化)
+  const handleSelectPerson = useCallback((person: DisabledPersonData) => {
     if (disabledIds.includes(person.id)) {
       return; // 跳过禁用的人员
     }
 
-    if (person.isInBlackList === 1) {
+    if (person.isInBlackList === BLACKLIST_STATUS.IN_BLACKLIST) {
       // 黑名单人员需要二次确认
       setPendingSelection(person);
       setShowBlacklistConfirm(true);
@@ -193,10 +225,10 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
 
       setSelectedPersons(newSelectedPersons);
     }
-  };
+  }, [disabledIds, mode, onSelect, onOpenChange, selectedPersons]);
 
-  // 处理批量选择
-  const handleBatchSelect = () => {
+  // 处理批量选择(使用 useCallback 优化)
+  const handleBatchSelect = useCallback(() => {
     console.debug('[DisabledPersonSelector] handleBatchSelect 被调用, selectedPersons:', selectedPersons.length);
     if (selectedPersons.length === 0) {
       console.debug('[DisabledPersonSelector] 没有选中人员,返回');
@@ -204,7 +236,7 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
     }
 
     // 检查是否有黑名单人员
-    const blacklistPersons = selectedPersons.filter(p => p.isInBlackList === 1);
+    const blacklistPersons = selectedPersons.filter(p => p.isInBlackList === BLACKLIST_STATUS.IN_BLACKLIST);
     if (blacklistPersons.length > 0) {
       console.debug('[DisabledPersonSelector] 检测到黑名单人员');
       setPendingSelection(selectedPersons);
@@ -215,31 +247,31 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
     console.debug('[DisabledPersonSelector] 调用 onSelect, 人员数量:', selectedPersons.length);
     onSelect(selectedPersons);
     onOpenChange(false);
-  };
+  }, [onSelect, onOpenChange, selectedPersons]);
 
-  // 确认选择黑名单人员
-  const handleConfirmBlacklistSelection = () => {
+  // 确认选择黑名单人员(使用 useCallback 优化)
+  const handleConfirmBlacklistSelection = useCallback(() => {
     if (pendingSelection) {
       onSelect(pendingSelection);
       onOpenChange(false);
     }
     setShowBlacklistConfirm(false);
     setPendingSelection(null);
-  };
+  }, [onSelect, onOpenChange, pendingSelection]);
 
-  // 取消选择黑名单人员
-  const handleCancelBlacklistSelection = () => {
+  // 取消选择黑名单人员(使用 useCallback 优化)
+  const handleCancelBlacklistSelection = useCallback(() => {
     setShowBlacklistConfirm(false);
     setPendingSelection(null);
-  };
+  }, []);
 
-  // 处理分页变化
-  const handlePageChange = (page: number, pageSize: number) => {
+  // 处理分页变化(使用 useCallback 优化)
+  const handlePageChange = useCallback((page: number, pageSize: number) => {
     setSearchParams(prev => ({ ...prev, page, pageSize }));
-  };
+  }, []);
 
-  // 表格列定义
-  const columns = [
+  // 表格列定义(使用 useMemo 优化)
+  const columns: ColumnDef[] = useMemo(() => [
     { key: 'name', label: '姓名' },
     { key: 'gender', label: '性别' },
     { key: 'idCard', label: '身份证号' },
@@ -250,7 +282,7 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
     { key: 'disabilityType', label: '残疾类型' },
     { key: 'disabilityLevel', label: '残疾等级' },
     { key: 'blacklist', label: '黑名单' },
-  ];
+  ], []);
 
   return (
     <>
@@ -359,13 +391,9 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
                     <SelectValue placeholder="选择残疾类型" />
                   </SelectTrigger>
                   <SelectContent>
-                    <SelectItem value="视力残疾">视力残疾</SelectItem>
-                    <SelectItem value="听力残疾">听力残疾</SelectItem>
-                    <SelectItem value="言语残疾">言语残疾</SelectItem>
-                    <SelectItem value="肢体残疾">肢体残疾</SelectItem>
-                    <SelectItem value="智力残疾">智力残疾</SelectItem>
-                    <SelectItem value="精神残疾">精神残疾</SelectItem>
-                    <SelectItem value="多重残疾">多重残疾</SelectItem>
+                    {DISABILITY_TYPES.map(type => (
+                      <SelectItem key={type} value={type}>{type}</SelectItem>
+                    ))}
                   </SelectContent>
                 </Select>
               </div>
@@ -382,10 +410,9 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
                     <SelectValue placeholder="选择残疾等级" />
                   </SelectTrigger>
                   <SelectContent>
-                    <SelectItem value="一级">一级</SelectItem>
-                    <SelectItem value="二级">二级</SelectItem>
-                    <SelectItem value="三级">三级</SelectItem>
-                    <SelectItem value="四级">四级</SelectItem>
+                    {DISABILITY_LEVELS.map(level => (
+                      <SelectItem key={level} value={level}>{level}</SelectItem>
+                    ))}
                   </SelectContent>
                 </Select>
               </div>
@@ -409,6 +436,13 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
             <div className="flex items-center justify-center h-64">
               <div className="text-center">加载中...</div>
             </div>
+          ) : error ? (
+            <Alert variant="destructive">
+              <AlertCircle className="h-4 w-4" />
+              <AlertDescription>
+                加载残疾人数据失败,请稍后重试
+              </AlertDescription>
+            </Alert>
           ) : personsData?.data && personsData.data.length > 0 ? (
             <>
               {/* 移动端提示 */}
@@ -487,7 +521,7 @@ const DisabledPersonSelector: React.FC<DisabledPersonSelectorProps> = ({
                         <TableCell className="hidden lg:table-cell">{person.city}</TableCell>
                         <TableCell className="hidden lg:table-cell">{person.disabilityType}</TableCell>
                         <TableCell className="hidden lg:table-cell">{person.disabilityLevel}</TableCell>
-                        <TableCell className="hidden lg:table-cell">{person.isInBlackList === 1 ? '是' : '否'}</TableCell>
+                        <TableCell className="hidden lg:table-cell">{person.isInBlackList === BLACKLIST_STATUS.IN_BLACKLIST ? '是' : '否'}</TableCell>
                       </TableRow>
                     ))}
                   </TableBody>

+ 17 - 11
web/tests/e2e/specs/cross-platform/order-create-sync.spec.ts

@@ -16,24 +16,30 @@ import { test, expect } from '../../utils/test-setup';
  * - 使用 data-testid 选择器
  * - 遵循项目测试规范
  */
+
+// 测试常量
+const TEST_SYNC_TIMEOUT = 3000; // 数据同步等待时间(ms)
+const TEST_COMPANY_NAME = '测试公司_1768346782396'; // 测试公司名称
+const MINI_LOGIN_PHONE = '13800001111'; // 小程序登录手机号
+const MINI_LOGIN_PASSWORD = 'password123'; // 小程序登录密码
 test.describe('跨端数据同步测试 - 后台创建订单到企业小程序', () => {
   let orderName: string;
 
   test.describe.serial('后台创建订单', () => {
-    test('应该成功登录后台并创建订单', async ({ page: adminPage }) => {
+    test('应该成功登录后台并创建订单', async ({ page: adminPage, testUsers }) => {
       // 记录开始时间
       const startTime = Date.now();
 
       // 1. 后台登录
-      await adminPage.goto('http://localhost:8080/admin/login');
-      await adminPage.getByPlaceholder('请输入用户名').fill('admin');
-      await adminPage.getByPlaceholder('请输入密码').fill('admin123');
+      await adminPage.goto('/admin/login');
+      await adminPage.getByPlaceholder('请输入用户名').fill(testUsers.admin.username);
+      await adminPage.getByPlaceholder('请输入密码').fill(testUsers.admin.password);
       await adminPage.getByRole('button', { name: '登录' }).click();
       await adminPage.waitForURL('**/admin/dashboard', { timeout: TIMEOUTS.PAGE_LOAD });
       console.debug('[后台] 登录成功');
 
       // 2. 导航到订单管理页面
-      await adminPage.goto('http://localhost:8080/admin/orders');
+      await adminPage.goto('/admin/orders');
       await adminPage.waitForSelector('table tbody tr', { state: 'visible', timeout: TIMEOUTS.PAGE_LOAD });
       console.debug('[后台] 导航到订单管理页面');
 
@@ -62,9 +68,9 @@ test.describe('跨端数据同步测试 - 后台创建订单到企业小程序',
       await adminPage.getByTestId('company-selector-create').click();
       await adminPage.waitForTimeout(TIMEOUTS.SHORT);
       // 选择测试公司_1768346782396(与 13800001111 用户关联的公司)
-      const companyOption = adminPage.getByRole('option', { name: '测试公司_1768346782396' });
+      const companyOption = adminPage.getByRole('option', { name: TEST_COMPANY_NAME });
       await companyOption.click();
-      console.debug('[后台] 选择公司: 测试公司_1768346782396');
+      console.debug(`[后台] 选择公司: ${TEST_COMPANY_NAME}`);
 
       // 5. 选择残疾人
       // 首先检查是否已经有残疾人被选择(可能在之前的测试中)
@@ -141,15 +147,15 @@ test.describe('跨端数据同步测试 - 后台创建订单到企业小程序',
       console.debug(`[小程序] 查找订单: ${testOrderName}`);
 
       // 等待一段时间,确保数据同步完成
-      await new Promise(resolve => setTimeout(resolve, 3000));
+      await new Promise(resolve => setTimeout(resolve, TEST_SYNC_TIMEOUT));
 
       // 1. 小程序登录
-      await miniPage.goto('http://localhost:8080/mini');
+      await miniPage.goto('/mini');
       await miniPage.waitForLoadState('networkidle');
 
       // 填写登录表单(使用企业账号)
-      await miniPage.getByTestId('mini-phone-input').getByPlaceholder('请输入手机号').fill('13800001111');
-      await miniPage.getByRole('textbox', { name: '请输入密码' }).fill('password123');
+      await miniPage.getByTestId('mini-phone-input').getByPlaceholder('请输入手机号').fill(MINI_LOGIN_PHONE);
+      await miniPage.getByRole('textbox', { name: '请输入密码' }).fill(MINI_LOGIN_PASSWORD);
       await miniPage.getByTestId('mini-login-button').click();
       console.debug('[小程序] 登录请求已发送');