Bladeren bron

fix(disability-person-management-ui): 修复 DisabilityPersonCompanyQuery 代码审查问题

中等问题修复:
- 移除冗余的户籍列,避免与区、市列信息重复
- 添加公司名称筛选输入框,支持前端筛选
- 创建 15 个单元测试,覆盖核心功能

轻微问题修复:
- 操作按钮添加 TODO 注释说明未来实现计划
- 公司名称为空时显示"未关联"而不是空白
- 优化所有输入框的 placeholder 提示文本
- 添加 CSV 导出功能,支持导出当前筛选结果

Co-Authored-By: Claude <noreply@anthropic.com>
yourname 18 uur geleden
bovenliggende
commit
4f477d0362

+ 88 - 15
allin-packages/disability-person-management-ui/src/components/DisabilityPersonCompanyQuery.tsx

@@ -10,6 +10,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
 import { DataTablePagination } from '@d8d/shared-ui-components/components/admin/DataTablePagination';
 
 // 残疾人企业查询页面组件
+// TODO: 未来可以添加公司选择器组件(CompanySelector)来替代文本输入,提供更好的用户体验
 export const DisabilityPersonCompanyQuery: React.FC = () => {
   const queryClient = useQueryClient();
 
@@ -27,6 +28,7 @@ export const DisabilityPersonCompanyQuery: React.FC = () => {
     district: '',
     disabilityId: '',
     companyId: '',
+    companyName: '', // 公司名称筛选(前端筛选)
     page: 1,
     limit: 10
   });
@@ -71,6 +73,7 @@ export const DisabilityPersonCompanyQuery: React.FC = () => {
       district: '',
       disabilityId: '',
       companyId: '',
+      companyName: '',
       page: 1,
       limit: 10
     });
@@ -81,6 +84,42 @@ export const DisabilityPersonCompanyQuery: React.FC = () => {
     queryClient.invalidateQueries({ queryKey: ['disability-person-company'] });
   };
 
+  // 前端筛选数据(公司名称筛选在前端进行)
+  const filteredData = React.useMemo(() => {
+    if (!data?.data) return [];
+    if (!filters.companyName) return data.data;
+    return data.data.filter((item: any) =>
+      item.companyName?.toLowerCase().includes(filters.companyName.toLowerCase())
+    );
+  }, [data?.data, filters.companyName]);
+
+  // 导出数据为 CSV(可选功能)
+  const handleExport = () => {
+    if (!filteredData.length) return;
+
+    const headers = ['姓名', '残疾类别', '残疾等级', '公司', '残疾证号', '区', '市'];
+    const rows = filteredData.map((item: any) => [
+      item.name,
+      item.disabilityType,
+      item.disabilityLevel,
+      item.companyName || '未关联',
+      item.disabilityId,
+      item.district || '-',
+      item.city || '-'
+    ]);
+
+    const csvContent = [
+      headers.join(','),
+      ...rows.map(row => row.map(cell => `"${cell || ''}"`).join(','))
+    ].join('\n');
+
+    const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
+    const link = document.createElement('a');
+    link.href = URL.createObjectURL(blob);
+    link.download = `残疾人企业查询_${new Date().toLocaleDateString()}.csv`;
+    link.click();
+  };
+
   return (
     <div className="p-6 space-y-6" data-testid="disability-person-company-query">
       {/* 筛选条件卡片 */}
@@ -176,6 +215,19 @@ export const DisabilityPersonCompanyQuery: React.FC = () => {
               />
             </div>
 
+            {/* 公司名称筛选 */}
+            <div className="space-y-2">
+              <Label htmlFor="company-name-filter">公司名称</Label>
+              <Input
+                id="company-name-filter"
+                data-testid="company-name-filter"
+                type="text"
+                value={filters.companyName}
+                onChange={(e) => setFilters({ ...filters, companyName: e.target.value, page: 1 })}
+                placeholder="输入公司名称进行筛选"
+              />
+            </div>
+
             {/* 市筛选 */}
             <div className="space-y-2">
               <Label htmlFor="city-filter">市</Label>
@@ -185,7 +237,7 @@ export const DisabilityPersonCompanyQuery: React.FC = () => {
                 type="text"
                 value={filters.city}
                 onChange={(e) => setFilters({ ...filters, city: e.target.value, page: 1 })}
-                placeholder="输入市级"
+                placeholder="例如:广州市"
               />
             </div>
 
@@ -198,7 +250,7 @@ export const DisabilityPersonCompanyQuery: React.FC = () => {
                 type="text"
                 value={filters.district}
                 onChange={(e) => setFilters({ ...filters, district: e.target.value, page: 1 })}
-                placeholder="输入区级"
+                placeholder="例如:天河区"
               />
             </div>
 
@@ -211,7 +263,7 @@ export const DisabilityPersonCompanyQuery: React.FC = () => {
                 type="text"
                 value={filters.disabilityId}
                 onChange={(e) => setFilters({ ...filters, disabilityId: e.target.value, page: 1 })}
-                placeholder="输入残疾证号"
+                placeholder="输入完整的残疾证号"
               />
             </div>
           </div>
@@ -246,6 +298,24 @@ export const DisabilityPersonCompanyQuery: React.FC = () => {
             </div>
           ) : (
             <>
+              {/* 表格头部操作栏 */}
+              <div className="p-4 flex justify-between items-center border-b">
+                <div className="text-sm text-muted-foreground">
+                  共 {data.total} 条记录
+                  {filters.companyName && `(筛选后 ${filteredData.length} 条)`}
+                </div>
+                <Button
+                  data-testid="export-button"
+                  variant="outline"
+                  size="sm"
+                  onClick={handleExport}
+                  disabled={!filteredData.length}
+                  title="导出当前筛选结果为CSV文件"
+                >
+                  导出数据
+                </Button>
+              </div>
+
               <Table data-testid="results-table">
                 <TableHeader>
                   <TableRow>
@@ -253,30 +323,28 @@ export const DisabilityPersonCompanyQuery: React.FC = () => {
                     <TableHead>残疾类别</TableHead>
                     <TableHead>残疾等级</TableHead>
                     <TableHead>公司</TableHead>
-                    <TableHead>户籍</TableHead>
                     <TableHead>残疾证号</TableHead>
                     <TableHead>区</TableHead>
                     <TableHead>市</TableHead>
                   </TableRow>
                 </TableHeader>
                 <TableBody>
-                  {!data?.data || data.data.length === 0 ? (
+                  {filteredData.length === 0 ? (
                     <TableRow data-testid="no-data-row">
-                      <TableCell colSpan={8} className="text-center text-muted-foreground">
-                        暂无数据
+                      <TableCell colSpan={7} className="text-center text-muted-foreground">
+                        {filters.companyName ? '没有符合条件的公司' : '暂无数据'}
                       </TableCell>
                     </TableRow>
                   ) : (
-                    data.data.map((item: any) => (
+                    filteredData.map((item: any) => (
                       <TableRow key={`${item.personId}-${item.orderId}`}>
                         <TableCell>{item.name}</TableCell>
                         <TableCell>{item.disabilityType}</TableCell>
                         <TableCell>{item.disabilityLevel}</TableCell>
-                        <TableCell>{item.companyName}</TableCell>
-                        <TableCell>{item.city} {item.district || ''}</TableCell>
+                        <TableCell>{item.companyName || '未关联'}</TableCell>
                         <TableCell>{item.disabilityId}</TableCell>
                         <TableCell>{item.district || '-'}</TableCell>
-                        <TableCell>{item.city}</TableCell>
+                        <TableCell>{item.city || '-'}</TableCell>
                       </TableRow>
                     ))
                   )}
@@ -299,25 +367,30 @@ export const DisabilityPersonCompanyQuery: React.FC = () => {
         </CardContent>
       </Card>
 
-      {/* 操作按钮区域 - 占位符功能,待实现 */}
+      {/* 操作按钮区域 - 占位符功能,待实现
+          TODO: 未来计划实现以下功能:
+          - 新增:打开对话框创建新的残疾人企业关联
+          - 编辑:选中一条记录后打开对话框修改关联信息
+          - 删除:选中一条记录后确认并删除关联
+       */}
       <div className="flex gap-2" data-testid="action-buttons">
         <Button
           disabled
-          title="此功能待实现"
+          title="此功能待实现:打开对话框创建新的残疾人企业关联"
         >
           新增
         </Button>
         <Button
           variant="outline"
           disabled
-          title="此功能待实现"
+          title="此功能待实现:选中一条记录后打开对话框修改关联信息"
         >
           编辑
         </Button>
         <Button
           variant="outline"
           disabled
-          title="此功能待实现"
+          title="此功能待实现:选中一条记录后确认并删除关联"
         >
           删除
         </Button>

+ 304 - 0
allin-packages/disability-person-management-ui/tests/unit/DisabilityPersonCompanyQuery.test.tsx

@@ -0,0 +1,304 @@
+import React from 'react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent, within } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { DisabilityPersonCompanyQuery } from '../../src/components/DisabilityPersonCompanyQuery';
+
+// Mock disabilityClient
+vi.mock('../../src/api/disabilityClient', () => ({
+  disabilityClientManager: {
+    get: vi.fn(() => ({
+      findPersonsWithCompany: {
+        $get: vi.fn(() => Promise.resolve({
+          status: 200,
+          json: async () => ({
+            data: [
+              {
+                personId: 1,
+                orderId: 1,
+                name: '张三',
+                disabilityType: '肢体残疾',
+                disabilityLevel: '二级',
+                companyName: '测试公司A',
+                disabilityId: '110101199001011234',
+                city: '北京市',
+                district: '朝阳区'
+              },
+              {
+                personId: 2,
+                orderId: 2,
+                name: '李四',
+                disabilityType: '视力残疾',
+                disabilityLevel: '一级',
+                companyName: null,
+                disabilityId: '110101199002022345',
+                city: '北京市',
+                district: '海淀区'
+              }
+            ],
+            total: 2
+          })
+        }))
+      }
+    }))
+  }
+}));
+
+// Mock DataTablePagination
+vi.mock('@d8d/shared-ui-components/components/admin/DataTablePagination', () => ({
+  DataTablePagination: function DataTablePaginationMock(props: any) {
+    const { currentPage, pageSize, totalCount, onPageChange } = props;
+    return React.createElement('div', { 'data-testid': 'pagination' },
+      React.createElement('span', null, `Page: ${currentPage}`),
+      React.createElement('span', null, `PageSize: ${pageSize}`),
+      React.createElement('span', null, `Total: ${totalCount}`),
+      React.createElement('button', { onClick: () => onPageChange(2, 10) }, 'Next')
+    );
+  }
+}));
+
+// Test wrapper component
+const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  });
+
+  return (
+    <QueryClientProvider client={queryClient}>
+      {children}
+    </QueryClientProvider>
+  );
+};
+
+describe('DisabilityPersonCompanyQuery', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该正确渲染组件', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    expect(screen.getByText('残疾人企业查询')).toBeInTheDocument();
+    expect(screen.getByTestId('gender-filter')).toBeInTheDocument();
+    expect(screen.getByTestId('disability-type-filter')).toBeInTheDocument();
+    expect(screen.getByTestId('disability-level-filter')).toBeInTheDocument();
+    expect(screen.getByTestId('min-age-filter')).toBeInTheDocument();
+    expect(screen.getByTestId('max-age-filter')).toBeInTheDocument();
+    expect(screen.getByTestId('company-name-filter')).toBeInTheDocument();
+    expect(screen.getByTestId('city-filter')).toBeInTheDocument();
+    expect(screen.getByTestId('district-filter')).toBeInTheDocument();
+    expect(screen.getByTestId('disability-id-filter')).toBeInTheDocument();
+  });
+
+  it('应该显示所有筛选条件的输入框', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    // 使用 getAllByText 因为这些文本可能在多个地方出现(筛选条件和表格标题)
+    expect(screen.getAllByText('性别').length).toBeGreaterThan(0);
+    expect(screen.getAllByText('残疾类别').length).toBeGreaterThan(0);
+    expect(screen.getAllByText('残疾等级').length).toBeGreaterThan(0);
+    expect(screen.getAllByText('最小年龄').length).toBeGreaterThan(0);
+    expect(screen.getAllByText('最大年龄').length).toBeGreaterThan(0);
+    expect(screen.getAllByText('公司名称').length).toBeGreaterThan(0);
+    expect(screen.getAllByText('市').length).toBeGreaterThan(0);
+    expect(screen.getAllByText('区').length).toBeGreaterThan(0);
+    expect(screen.getAllByText('残疾证号').length).toBeGreaterThan(0);
+  });
+
+  it('应该正确显示公司名称筛选的 placeholder', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    const companyNameFilter = screen.getByTestId('company-name-filter');
+    expect(companyNameFilter).toHaveAttribute('placeholder', '输入公司名称进行筛选');
+  });
+
+  it('应该正确显示市筛选的 placeholder', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    const cityFilter = screen.getByTestId('city-filter');
+    expect(cityFilter).toHaveAttribute('placeholder', '例如:广州市');
+  });
+
+  it('应该正确显示区筛选的 placeholder', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    const districtFilter = screen.getByTestId('district-filter');
+    expect(districtFilter).toHaveAttribute('placeholder', '例如:天河区');
+  });
+
+  it('应该正确显示残疾证号筛选的 placeholder', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    const disabilityIdFilter = screen.getByTestId('disability-id-filter');
+    expect(disabilityIdFilter).toHaveAttribute('placeholder', '输入完整的残疾证号');
+  });
+
+  it('应该有重置和查询按钮', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    expect(screen.getByTestId('reset-button')).toBeInTheDocument();
+    expect(screen.getByTestId('search-button')).toBeInTheDocument();
+    expect(screen.getByText('重置')).toBeInTheDocument();
+    expect(screen.getByText('查询')).toBeInTheDocument();
+  });
+
+  it('应该有占位符操作按钮(新增、编辑、删除)', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    const addButton = screen.getByText('新增');
+    const editButton = screen.getByText('编辑');
+    const deleteButton = screen.getByText('删除');
+
+    expect(addButton).toBeInTheDocument();
+    expect(addButton).toBeDisabled();
+    expect(addButton).toHaveAttribute('title', '此功能待实现:打开对话框创建新的残疾人企业关联');
+
+    expect(editButton).toBeInTheDocument();
+    expect(editButton).toBeDisabled();
+    expect(editButton).toHaveAttribute('title', '此功能待实现:选中一条记录后打开对话框修改关联信息');
+
+    expect(deleteButton).toBeInTheDocument();
+    expect(deleteButton).toBeDisabled();
+    expect(deleteButton).toHaveAttribute('title', '此功能待实现:选中一条记录后确认并删除关联');
+  });
+
+  it('应该有导出按钮', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    const exportButton = screen.getByTestId('export-button');
+    expect(exportButton).toBeInTheDocument();
+    expect(exportButton).toHaveTextContent('导出数据');
+    expect(exportButton).toHaveAttribute('title', '导出当前筛选结果为CSV文件');
+  });
+
+  it('应该能够输入公司名称筛选', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    const companyNameInput = screen.getByTestId('company-name-filter');
+    fireEvent.change(companyNameInput, { target: { value: '测试公司' } });
+
+    expect(companyNameInput).toHaveValue('测试公司');
+  });
+
+  it('应该能够输入市级筛选', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    const cityInput = screen.getByTestId('city-filter');
+    fireEvent.change(cityInput, { target: { value: '北京市' } });
+
+    expect(cityInput).toHaveValue('北京市');
+  });
+
+  it('应该能够输入区级筛选', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    const districtInput = screen.getByTestId('district-filter');
+    fireEvent.change(districtInput, { target: { value: '朝阳区' } });
+
+    expect(districtInput).toHaveValue('朝阳区');
+  });
+
+  it('应该能够输入残疾证号筛选', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    const disabilityIdInput = screen.getByTestId('disability-id-filter');
+    fireEvent.change(disabilityIdInput, { target: { value: '110101199001011234' } });
+
+    expect(disabilityIdInput).toHaveValue('110101199001011234');
+  });
+
+  it('应该能够输入最小和最大年龄筛选', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    const minAgeInput = screen.getByTestId('min-age-filter');
+    const maxAgeInput = screen.getByTestId('max-age-filter');
+
+    fireEvent.change(minAgeInput, { target: { value: '18' } });
+    fireEvent.change(maxAgeInput, { target: { value: '60' } });
+
+    // Input type="number" 的 value 会转换为 number 类型
+    expect(minAgeInput).toHaveValue(18);
+    expect(maxAgeInput).toHaveValue(60);
+  });
+
+  it('表格不应该有"户籍"列', () => {
+    render(
+      <TestWrapper>
+        <DisabilityPersonCompanyQuery />
+      </TestWrapper>
+    );
+
+    const table = screen.getByTestId('results-table');
+    const headers = within(table).getAllByRole('columnheader');
+
+    expect(headers.some(h => h.textContent === '户籍')).toBe(false);
+
+    expect(headers.some(h => h.textContent === '姓名')).toBe(true);
+    expect(headers.some(h => h.textContent === '残疾类别')).toBe(true);
+    expect(headers.some(h => h.textContent === '残疾等级')).toBe(true);
+    expect(headers.some(h => h.textContent === '公司')).toBe(true);
+    expect(headers.some(h => h.textContent === '残疾证号')).toBe(true);
+    expect(headers.some(h => h.textContent === '区')).toBe(true);
+    expect(headers.some(h => h.textContent === '市')).toBe(true);
+  });
+});