3 Commits 1a0c7fdc0a ... c91533cbf8

Tác giả SHA1 Thông báo Ngày
  yourname c91533cbf8 ♻️ refactor(area-tree): 替换区域客户端调用方式 3 ngày trước cách đây
  yourname 94f68636e3 feat(salary-ui): 添加暂无数据显示功能 3 ngày trước cách đây
  yourname d8112bff42 fix(salary-ui): 修复表单上下文错误和搜索表单问题 3 ngày trước cách đây

+ 88 - 54
allin-packages/salary-management-ui/src/components/SalaryManagement.tsx

@@ -38,6 +38,11 @@ const SalaryManagement: React.FC = () => {
   const [salaryToDelete, setSalaryToDelete] = useState<number | null>(null);
   const [areaValue, setAreaValue] = useState<{ provinceId?: number; cityId?: number; districtId?: number }>({});
 
+  // 搜索表单
+  const searchForm = useForm<{ provinceId?: number; cityId?: number; districtId?: number }>({
+    defaultValues: {}
+  });
+
   // 表单实例 - 使用RPC推导的类型
   const createForm = useForm<CreateSalaryRequest>({
     resolver: zodResolver(CreateSalarySchema),
@@ -74,6 +79,10 @@ const SalaryManagement: React.FC = () => {
       const res = await salaryClientManager.get().list.$get({ query });
       if (res.status !== 200) throw new Error('获取薪资列表失败');
       return await res.json();
+    },
+    initialData: {
+      data: [],
+      total: 0
     }
   });
 
@@ -134,12 +143,12 @@ const SalaryManagement: React.FC = () => {
   });
 
   // 处理搜索
-  const handleSearch = () => {
+  const handleSearch = (data: { provinceId?: number; cityId?: number; districtId?: number }) => {
     setSearchParams(prev => ({
       ...prev,
       page: 1,
-      provinceId: areaValue.provinceId,
-      cityId: areaValue.cityId
+      provinceId: data.provinceId,
+      cityId: data.cityId
     }));
   };
 
@@ -207,6 +216,10 @@ const SalaryManagement: React.FC = () => {
   // 处理区域选择变化
   const handleAreaChange = (value: { provinceId?: number; cityId?: number; districtId?: number }) => {
     setAreaValue(value);
+    searchForm.setValue('provinceId', value.provinceId);
+    searchForm.setValue('cityId', value.cityId);
+    searchForm.setValue('districtId', value.districtId);
+
     if (isCreateForm) {
       createForm.setValue('provinceId', value.provinceId!);
       createForm.setValue('cityId', value.cityId!);
@@ -232,24 +245,37 @@ const SalaryManagement: React.FC = () => {
       </CardHeader>
       <CardContent>
         {/* 搜索区域 */}
-        <div className="flex flex-wrap items-center gap-4 mb-6">
-          <div className="flex-1 min-w-[300px]">
-            <AreaSelect
-              value={areaValue}
-              onChange={handleAreaChange}
-              disabled={false}
-              required={false}
-            />
-          </div>
-          <Button onClick={handleSearch}>
-            <Search className="h-4 w-4 mr-2" />
-            搜索
-          </Button>
-          <Button onClick={showCreateModal} data-testid="add-salary-button">
-            <Plus className="h-4 w-4 mr-2" />
-            添加薪资
-          </Button>
-        </div>
+        <Form {...searchForm}>
+          <form onSubmit={searchForm.handleSubmit(handleSearch)} className="flex flex-wrap items-center gap-4 mb-6">
+            <div className="flex-1 min-w-[300px]">
+              <FormField
+                control={searchForm.control}
+                name="provinceId"
+                render={({ field: _field }) => (
+                  <FormItem>
+                    <FormControl>
+                      <AreaSelect
+                        value={areaValue}
+                        onChange={handleAreaChange}
+                        disabled={false}
+                        required={false}
+                      />
+                    </FormControl>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+            </div>
+            <Button type="submit">
+              <Search className="h-4 w-4 mr-2" />
+              搜索
+            </Button>
+            <Button onClick={showCreateModal} data-testid="add-salary-button" type="button">
+              <Plus className="h-4 w-4 mr-2" />
+              添加薪资
+            </Button>
+          </form>
+        </Form>
 
         {/* 数据表格 */}
         {isLoading ? (
@@ -277,45 +303,53 @@ const SalaryManagement: React.FC = () => {
                 </TableRow>
               </TableHeader>
               <TableBody>
-                {data?.data?.map((salary: SalaryResponse) => (
-                  <TableRow key={salary.id} data-testid={`salary-row-${salary.id}`}>
-                    <TableCell>{salary.id}</TableCell>
-                    <TableCell>{salary.province?.name || salary.provinceId}</TableCell>
-                    <TableCell>{salary.city?.name || salary.cityId}</TableCell>
-                    <TableCell>{salary.district?.name || salary.districtId || '-'}</TableCell>
-                    <TableCell>¥{salary.basicSalary.toFixed(2)}</TableCell>
-                    <TableCell>¥{(salary.allowance || 0).toFixed(2)}</TableCell>
-                    <TableCell>¥{(salary.insurance || 0).toFixed(2)}</TableCell>
-                    <TableCell>¥{(salary.housingFund || 0).toFixed(2)}</TableCell>
-                    <TableCell className="font-semibold">¥{salary.totalSalary.toFixed(2)}</TableCell>
-                    <TableCell>{format(new Date(salary.updateTime), 'yyyy-MM-dd HH:mm')}</TableCell>
-                    <TableCell>
-                      <div className="flex gap-2">
-                        <Button
-                          variant="ghost"
-                          size="sm"
-                          onClick={() => showEditModal(salary)}
-                          data-testid={`edit-salary-${salary.id}`}
-                        >
-                          <Edit className="h-4 w-4" />
-                        </Button>
-                        <Button
-                          variant="ghost"
-                          size="sm"
-                          onClick={() => showDeleteDialog(salary.id)}
-                          data-testid={`delete-salary-${salary.id}`}
-                        >
-                          <Trash2 className="h-4 w-4" />
-                        </Button>
-                      </div>
+                {data?.data && data.data.length > 0 ? (
+                  data.data.map((salary: SalaryResponse) => (
+                    <TableRow key={salary.id} data-testid={`salary-row-${salary.id}`}>
+                      <TableCell>{salary.id}</TableCell>
+                      <TableCell>{salary.province?.name || salary.provinceId}</TableCell>
+                      <TableCell>{salary.city?.name || salary.cityId}</TableCell>
+                      <TableCell>{salary.district?.name || salary.districtId || '-'}</TableCell>
+                      <TableCell>¥{salary.basicSalary.toFixed(2)}</TableCell>
+                      <TableCell>¥{(salary.allowance || 0).toFixed(2)}</TableCell>
+                      <TableCell>¥{(salary.insurance || 0).toFixed(2)}</TableCell>
+                      <TableCell>¥{(salary.housingFund || 0).toFixed(2)}</TableCell>
+                      <TableCell className="font-semibold">¥{salary.totalSalary.toFixed(2)}</TableCell>
+                      <TableCell>{format(new Date(salary.updateTime), 'yyyy-MM-dd HH:mm')}</TableCell>
+                      <TableCell>
+                        <div className="flex gap-2">
+                          <Button
+                            variant="ghost"
+                            size="sm"
+                            onClick={() => showEditModal(salary)}
+                            data-testid={`edit-salary-${salary.id}`}
+                          >
+                            <Edit className="h-4 w-4" />
+                          </Button>
+                          <Button
+                            variant="ghost"
+                            size="sm"
+                            onClick={() => showDeleteDialog(salary.id)}
+                            data-testid={`delete-salary-${salary.id}`}
+                          >
+                            <Trash2 className="h-4 w-4" />
+                          </Button>
+                        </div>
+                      </TableCell>
+                    </TableRow>
+                  ))
+                ) : (
+                  <TableRow>
+                    <TableCell colSpan={11} className="text-center py-8">
+                      <p className="text-muted-foreground">暂无数据</p>
                     </TableCell>
                   </TableRow>
-                ))}
+                )}
               </TableBody>
             </Table>
 
             {/* 分页 */}
-            {data?.total && (
+            {data.total > 0 && (
               <DataTablePagination
                 currentPage={searchParams.page}
                 pageSize={searchParams.limit}

+ 7 - 6
allin-packages/salary-management-ui/tests/integration/salary.integration.test.tsx

@@ -288,12 +288,13 @@ describe('薪资管理集成测试', () => {
       expect(screen.getByText('填写薪资信息,支持实时计算总薪资')).toBeInTheDocument();
     });
 
-    // 检查表单字段
-    expect(screen.getByText('区域选择')).toBeInTheDocument();
-    expect(screen.getByText('基本工资')).toBeInTheDocument();
-    expect(screen.getByText('津贴补贴')).toBeInTheDocument();
-    expect(screen.getByText('保险费用')).toBeInTheDocument();
-    expect(screen.getByText('住房公积金')).toBeInTheDocument();
+    // 检查表单字段 - 在模态框内查找
+    const dialog = screen.getByRole('dialog');
+    expect(within(dialog).getByText('区域选择')).toBeInTheDocument();
+    expect(within(dialog).getByText('基本工资')).toBeInTheDocument();
+    expect(within(dialog).getByText('津贴补贴')).toBeInTheDocument();
+    expect(within(dialog).getByText('保险费用')).toBeInTheDocument();
+    expect(within(dialog).getByText('住房公积金')).toBeInTheDocument();
   });
 
   it('应该计算总薪资', async () => {

+ 2 - 2
packages/area-management-ui/src/components/AreaTreeAsync.tsx

@@ -4,7 +4,7 @@ import { Button } from '@d8d/shared-ui-components/components/ui/button';
 import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
 import { cn } from '@d8d/shared-ui-components/utils';
 import { useQuery } from '@tanstack/react-query';
-import { areaClient } from '../api/areaClient';
+import { areaClient, areaClientManager } from '../api/areaClient';
 import type { AreaNode } from '../types/area';
 
 interface AreaTreeAsyncProps {
@@ -46,7 +46,7 @@ const SubTreeLoader: React.FC<SubTreeLoaderProps> = ({
   const { data: subTreeData, isLoading: isSubTreeLoading } = useQuery({
     queryKey: ['areas-subtree', nodeId],
     queryFn: async () => {
-      const res = await areaClient.index.$get({
+      const res = await areaClientManager.get().index.$get({
         query: {
           page: 1,
           pageSize: 100 ,