Explorar el Código

♻️ refactor(area-select-form): 简化组件依赖并优化表单字段更新逻辑

- 移除冗余的 `control` prop,统一从 `useFormContext` 获取表单控制
- 移除对 `react-hook-form` 中 `Control` 类型的显式导入
- 优化省份、城市、区县字段的联动清空逻辑,优先使用 `setValue` API,保留内部方法作为向后兼容的 fallback
- 更新错误提示信息,更准确地反映依赖关系

🗑️ chore(hooks): 移除已弃用的 hooks 模块及其相关测试

- 删除 `src/hooks/index.ts` 导出文件
- 删除 `src/hooks/useAreas.ts` 及其所有 CRUD 和状态切换 hook 实现
- 删除对应的单元测试文件 `tests/unit/useAreas.test.tsx`
- 更新 `src/index.ts` 的导出,不再包含 hooks 模块

🗑️ chore(tests): 清理测试文件并移除过时的单元测试

- 删除已弃用的 `AreaSelect` 组件单元测试文件 `tests/unit/AreaSelect.test.tsx`
- 更新集成测试 `area-select-form.integration.test.tsx`,移除对已删除 `control` prop 的引用
- 移除对 `@testing-library/user-event` 的未使用导入
yourname hace 2 semanas
padre
commit
d81deb89cf

+ 26 - 12
packages/area-management-ui/src/components/AreaSelectForm.tsx

@@ -1,4 +1,4 @@
-import { Controller, useFormContext, Control, FieldValues, Path, useWatch } from 'react-hook-form';
+import { Controller, useFormContext, FieldValues, Path, useWatch } from 'react-hook-form';
 import { useQuery } from '@tanstack/react-query';
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
 import { FormDescription, FormLabel } from '@d8d/shared-ui-components/components/ui/form';
@@ -14,7 +14,6 @@ interface AreaSelectFormProps<T extends FieldValues = FieldValues> {
   required?: boolean;
   disabled?: boolean;
   className?: string;
-  control?: Control<T>;
   testIdPrefix?: string;
 }
 
@@ -30,15 +29,15 @@ export const AreaSelectForm = <T extends FieldValues = FieldValues>({
   required = false,
   disabled = false,
   className,
-  control: propControl,
   testIdPrefix = 'area-select'
 }: AreaSelectFormProps<T>) => {
-  // 使用传入的 control,或者从上下文中获取
+  // 从上下文中获取表单控制
   const formContext = useFormContext<T>();
-  const control = propControl || formContext?.control;
+  const control = formContext?.control;
+  const setValue = formContext?.setValue;
 
   if (!control) {
-    console.error('AreaSelectForm: 缺少 react-hook-form 上下文或 control prop');
+    console.error('AreaSelectForm: 缺少 react-hook-form 上下文');
     return null;
   }
 
@@ -141,10 +140,19 @@ export const AreaSelectForm = <T extends FieldValues = FieldValues>({
                 value={provinceField.value || ''}
                 onValueChange={(value) => {
                   provinceField.onChange(value && value !== 'none' ? value : '');
-                  // 清空城市和区县字段 - 使用类型断言访问内部方法
-                  (control as any)._updateFormValues(cityName as any, '');
-                  if (districtName) {
-                    (control as any)._updateFormValues(districtName as any, '');
+                  // 清空城市和区县字段
+                  // 优先使用 setValue,如果不可用则使用内部方法作为 fallback
+                  if (setValue) {
+                    setValue(cityName, '' as any);
+                    if (districtName) {
+                      setValue(districtName, '' as any);
+                    }
+                  } else if ((control as any)._updateFormValues) {
+                    // Fallback: 使用内部方法(向后兼容)
+                    (control as any)._updateFormValues(cityName as any, '');
+                    if (districtName) {
+                      (control as any)._updateFormValues(districtName as any, '');
+                    }
                   }
                 }}
                 disabled={disabled || isLoadingProvinces}
@@ -180,9 +188,15 @@ export const AreaSelectForm = <T extends FieldValues = FieldValues>({
                 value={cityField.value || ''}
                 onValueChange={(value) => {
                   cityField.onChange(value && value !== 'none' ? value : '');
-                  // 清空区县字段 - 使用类型断言访问内部方法
+                  // 清空区县字段
+                  // 优先使用 setValue,如果不可用则使用内部方法作为 fallback
                   if (districtName) {
-                    (control as any)._updateFormValues(districtName as any, '');
+                    if (setValue) {
+                      setValue(districtName, '' as any);
+                    } else if ((control as any)._updateFormValues) {
+                      // Fallback: 使用内部方法(向后兼容)
+                      (control as any)._updateFormValues(districtName as any, '');
+                    }
                   }
                 }}
                 disabled={disabled || !provinceValue || isLoadingCities}

+ 0 - 8
packages/area-management-ui/src/hooks/index.ts

@@ -1,8 +0,0 @@
-export {
-  useAreas,
-  useAreaSubtree,
-  useCreateArea,
-  useUpdateArea,
-  useDeleteArea,
-  useToggleAreaStatus
-} from './useAreas';

+ 0 - 155
packages/area-management-ui/src/hooks/useAreas.ts

@@ -1,155 +0,0 @@
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { areaClient } from '../api/areaClient';
-import type { CreateAreaRequest, UpdateAreaRequest, AreaQueryParams } from '../types/area';
-import { toast } from 'sonner';
-
-// 获取区域列表的hook
-export const useAreas = (params?: AreaQueryParams) => {
-  return useQuery({
-    queryKey: ['areas', params],
-    queryFn: async () => {
-      const res = await areaClient.index.$get({
-        query: {
-          page: params?.page || 1,
-          pageSize: params?.pageSize || 100,
-          filters: params?.filters || '',
-          sortBy: params?.sortBy || 'id',
-          sortOrder: params?.sortOrder || 'ASC'
-        }
-      });
-      if (res.status !== 200) throw new Error('获取区域列表失败');
-      const response = await res.json();
-      return response.data;
-    },
-    staleTime: 5 * 60 * 1000,
-    gcTime: 10 * 60 * 1000,
-  });
-};
-
-// 获取区域子树的hook
-export const useAreaSubtree = (parentId: number) => {
-  return useQuery({
-    queryKey: ['areas-subtree', parentId],
-    queryFn: async () => {
-      const res = await areaClient.index.$get({
-        query: {
-          page: 1,
-          pageSize: 100,
-          filters: JSON.stringify({ parentId }),
-          sortBy: 'id',
-          sortOrder: 'ASC'
-        }
-      });
-      if (res.status !== 200) throw new Error('获取区域子树失败');
-      const response = await res.json();
-      return response.data;
-    },
-    enabled: !!parentId,
-    staleTime: 5 * 60 * 1000,
-    gcTime: 10 * 60 * 1000,
-  });
-};
-
-// 创建区域的hook
-export const useCreateArea = () => {
-  const queryClient = useQueryClient();
-
-  return useMutation({
-    mutationFn: async (data: CreateAreaRequest) => {
-      const res = await areaClient.index.$post({ json: data });
-      if (res.status !== 201) throw new Error('创建区域失败');
-    },
-    onSuccess: (_, variables) => {
-      // 更新根级缓存
-      queryClient.invalidateQueries({ queryKey: ['areas'] });
-      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
-
-      // 如果创建的是子节点,更新父节点的子树缓存
-      if (variables.parentId) {
-        queryClient.invalidateQueries({ queryKey: ['areas-subtree', variables.parentId] });
-      }
-
-      toast.success('区域创建成功');
-    },
-    onError: () => {
-      toast.error('创建失败,请重试');
-    }
-  });
-};
-
-// 更新区域的hook
-export const useUpdateArea = () => {
-  const queryClient = useQueryClient();
-
-  return useMutation({
-    mutationFn: async ({ id, data }: { id: number; data: UpdateAreaRequest }) => {
-      const res = await areaClient[':id'].$put({
-        param: { id },
-        json: data
-      });
-      if (res.status !== 200) throw new Error('更新区域失败');
-    },
-    onSuccess: () => {
-      // 更新所有相关缓存
-      queryClient.invalidateQueries({ queryKey: ['areas'] });
-      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
-      queryClient.invalidateQueries({ queryKey: ['areas-subtree'] });
-
-      toast.success('区域更新成功');
-    },
-    onError: () => {
-      toast.error('更新失败,请重试');
-    }
-  });
-};
-
-// 删除区域的hook
-export const useDeleteArea = () => {
-  const queryClient = useQueryClient();
-
-  return useMutation({
-    mutationFn: async (id: number) => {
-      const res = await areaClient[':id'].$delete({
-        param: { id }
-      });
-      if (res.status !== 204) throw new Error('删除区域失败');
-    },
-    onSuccess: () => {
-      // 更新所有相关缓存
-      queryClient.invalidateQueries({ queryKey: ['areas'] });
-      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
-      queryClient.invalidateQueries({ queryKey: ['areas-subtree'] });
-
-      toast.success('区域删除成功');
-    },
-    onError: () => {
-      toast.error('删除失败,请重试');
-    }
-  });
-};
-
-// 切换区域状态的hook
-export const useToggleAreaStatus = () => {
-  const queryClient = useQueryClient();
-
-  return useMutation({
-    mutationFn: async ({ id, isDisabled }: { id: number; isDisabled: number }) => {
-      const res = await areaClient[':id'].$put({
-        param: { id },
-        json: { isDisabled }
-      });
-      if (res.status !== 200) throw new Error('更新区域状态失败');
-    },
-    onSuccess: () => {
-      // 更新所有相关缓存
-      queryClient.invalidateQueries({ queryKey: ['areas'] });
-      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
-      queryClient.invalidateQueries({ queryKey: ['areas-subtree'] });
-
-      toast.success('区域状态更新成功');
-    },
-    onError: () => {
-      toast.error('状态更新失败,请重试');
-    }
-  });
-};

+ 0 - 3
packages/area-management-ui/src/index.ts

@@ -1,9 +1,6 @@
 // Export components
 export * from './components';
 
-// Export hooks
-export * from './hooks';
-
 // Export API
 export * from './api';
 

+ 0 - 4
packages/area-management-ui/tests/integration/area-select-form.integration.test.tsx

@@ -1,6 +1,5 @@
 import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
 import { useForm, FormProvider } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { z } from 'zod';
@@ -125,7 +124,6 @@ const TestForm = () => {
               districtName="district"
               label="地区选择"
               required={true}
-              control={form.control}
               testIdPrefix="test-area"
             />
             <Button type="submit" data-testid="submit-button">
@@ -182,7 +180,6 @@ describe('AreaSelectForm 集成测试', () => {
                   districtName="district"
                   label="地区选择"
                   required={true}
-                  control={form.control}
                   testIdPrefix="test-area"
                 />
                 <Button type="submit" data-testid="submit-button">
@@ -437,7 +434,6 @@ describe('AreaSelectForm 集成测试', () => {
                 label="地区选择"
                 required={true}
                 disabled={true}
-                control={form.control}
                 testIdPrefix="test-area"
               />
             </Form>

+ 0 - 160
packages/area-management-ui/tests/unit/AreaSelect.test.tsx

@@ -1,160 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { FormProvider, useForm } from 'react-hook-form';
-import { AreaSelect } from '../../src/components/AreaSelect';
-
-// Mock API客户端
-vi.mock('../../src/api/areaClient', () => ({
-  areaClientManager: {
-    get: vi.fn(() => ({
-      index: {
-        $get: vi.fn(() => Promise.resolve({
-          status: 200,
-          json: () => Promise.resolve({
-            data: [
-              { id: 1, name: '北京市', level: 1 },
-              { id: 2, name: '上海市', level: 1 }
-            ]
-          })
-        }))
-      }
-    }))
-  }
-}));
-
-// 测试组件包装器
-const TestWrapper = ({ children }: { children: React.ReactNode }) => {
-  const queryClient = new QueryClient({
-    defaultOptions: {
-      queries: {
-        retry: false,
-      },
-    },
-  });
-
-  // 创建一个简单的form context
-  const methods = useForm();
-
-  return (
-    <QueryClientProvider client={queryClient}>
-      <FormProvider {...methods}>
-        {children}
-      </FormProvider>
-    </QueryClientProvider>
-  );
-};
-
-describe('AreaSelect 组件测试', () => {
-  beforeEach(() => {
-    vi.clearAllMocks();
-  });
-
-  it('应该正确渲染AreaSelect组件', () => {
-    render(
-      <TestWrapper>
-        <AreaSelect />
-      </TestWrapper>
-    );
-
-    expect(screen.getByText('选择所在省份')).toBeInTheDocument();
-    expect(screen.getByText('选择所在城市')).toBeInTheDocument();
-    expect(screen.getByText('选择所在区县')).toBeInTheDocument();
-  });
-
-  it('当required=true时,省份应该显示必填标记,城市和区县不应该显示必填标记(未选择省份时)', () => {
-    render(
-      <TestWrapper>
-        <AreaSelect required={true} />
-      </TestWrapper>
-    );
-
-    // 检查省份标签是否包含星号
-    const provinceLabel = screen.getByText('省份');
-    expect(provinceLabel.innerHTML).toContain('*');
-
-    // 检查城市标签不应该包含星号(因为还没有选择省份)
-    const cityLabel = screen.getByText('城市');
-    expect(cityLabel.innerHTML).not.toContain('*');
-
-    // 检查区县标签不应该包含星号
-    const districtLabel = screen.getByText('区县');
-    expect(districtLabel.innerHTML).not.toContain('*');
-  });
-
-  it('当required=false时,所有字段都不应该显示必填标记', () => {
-    render(
-      <TestWrapper>
-        <AreaSelect required={false} />
-      </TestWrapper>
-    );
-
-    const provinceLabel = screen.getByText('省份');
-    expect(provinceLabel.innerHTML).not.toContain('*');
-
-    const cityLabel = screen.getByText('城市');
-    expect(cityLabel.innerHTML).not.toContain('*');
-
-    const districtLabel = screen.getByText('区县');
-    expect(districtLabel.innerHTML).not.toContain('*');
-  });
-
-  it('当选择了城市时,区县字段不应该显示必填标记(即使required=true)', () => {
-    // 这个测试验证区县字段永远不会显示必填标记
-    // 即使required=true且选择了城市,区县字段也不应该显示星号
-    render(
-      <TestWrapper>
-        <AreaSelect required={true} />
-      </TestWrapper>
-    );
-
-    // 检查区县标签不应该包含星号
-    const districtLabel = screen.getByText('区县');
-    expect(districtLabel.innerHTML).not.toContain('*');
-  });
-
-  it('应该正确处理onChange回调', () => {
-    const handleChange = vi.fn();
-
-    render(
-      <TestWrapper>
-        <AreaSelect onChange={handleChange} />
-      </TestWrapper>
-    );
-
-    // 验证组件渲染正常
-    expect(screen.getByText('选择所在省份')).toBeInTheDocument();
-    // onChange会在用户交互时被调用,这里我们只验证组件能接收onChange prop
-    expect(handleChange).not.toHaveBeenCalled();
-  });
-
-  it('应该支持禁用状态', () => {
-    render(
-      <TestWrapper>
-        <AreaSelect disabled={true} />
-      </TestWrapper>
-    );
-
-    // 验证组件渲染正常
-    expect(screen.getByText('选择所在省份')).toBeInTheDocument();
-    // 禁用状态会在UI中体现,这里我们只验证组件能接收disabled prop
-  });
-
-  it('应该正确处理初始值', () => {
-    const initialValue = {
-      provinceId: 1,
-      cityId: 3,
-      districtId: 5
-    };
-
-    render(
-      <TestWrapper>
-        <AreaSelect value={initialValue} />
-      </TestWrapper>
-    );
-
-    // 验证组件渲染正常 - 查找FormDescription中的文本
-    expect(screen.getByText('选择所在省份')).toBeInTheDocument();
-    // 初始值会在组件内部处理,这里我们只验证组件能接收value prop
-  });
-});

+ 0 - 212
packages/area-management-ui/tests/unit/useAreas.test.tsx

@@ -1,212 +0,0 @@
-import React from 'react';
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { renderHook, waitFor } from '@testing-library/react';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { useAreas, useCreateArea, useUpdateArea, useDeleteArea, useToggleAreaStatus } from '../../src/hooks/useAreas';
-import { areaClient } from '../../src/api/areaClient';
-
-// 完整的mock响应对象 - 按照用户UI包规范
-const createMockResponse = (status: number, data?: any) => ({
-  status,
-  ok: status >= 200 && status < 300,
-  body: null,
-  bodyUsed: false,
-  statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
-  headers: new Headers(),
-  url: '',
-  redirected: false,
-  type: 'basic' as ResponseType,
-  json: async () => data || {},
-  text: async () => '',
-  blob: async () => new Blob(),
-  arrayBuffer: async () => new ArrayBuffer(0),
-  formData: async () => new FormData(),
-  clone: function() { return this; }
-});
-
-// Mock areaClient - 按照用户UI包规范
-vi.mock('../../src/api/areaClient', () => {
-  const mockAreaClient = {
-    index: {
-      $get: vi.fn(() => Promise.resolve({ status: 200, body: null })),
-      $post: vi.fn(() => Promise.resolve({ status: 201, body: null })),
-    },
-    ':id': {
-      $put: vi.fn(() => Promise.resolve({ status: 200, body: null })),
-      $delete: vi.fn(() => Promise.resolve({ status: 204, body: null })),
-    },
-  };
-
-  const mockAreaClientManager = {
-    get: vi.fn(() => mockAreaClient),
-  };
-
-  return {
-    areaClientManager: mockAreaClientManager,
-    areaClient: mockAreaClient,
-  };
-});
-
-// Mock sonner toast
-vi.mock('sonner', () => ({
-  toast: {
-    success: vi.fn(),
-    error: vi.fn()
-  }
-}));
-
-// Test wrapper component
-const createWrapper = () => {
-  const queryClient = new QueryClient({
-    defaultOptions: {
-      queries: {
-        retry: false,
-      },
-    },
-  });
-
-  return ({ children }: { children: React.ReactNode }) => (
-    <QueryClientProvider client={queryClient}>
-      {children}
-    </QueryClientProvider>
-  );
-};
-
-describe('useAreas Hook', () => {
-  beforeEach(() => {
-    vi.clearAllMocks();
-  });
-
-  it('should fetch areas successfully', async () => {
-    const mockAreas = [
-      {
-        id: 1,
-        name: '北京市',
-        code: '110000',
-        level: 1,
-        parentId: null,
-        isDisabled: 0
-      }
-    ];
-
-    (areaClient.index.$get as any).mockResolvedValueOnce(createMockResponse(200, { data: mockAreas }));
-
-    const { result } = renderHook(() => useAreas(), {
-      wrapper: createWrapper()
-    });
-
-    // Initial loading state
-    expect(result.current.isLoading).toBe(true);
-
-    // Wait for data to load
-    await waitFor(() => {
-      expect(result.current.isSuccess).toBe(true);
-    });
-
-    expect(result.current.data).toEqual(mockAreas);
-    expect(areaClient.index.$get).toHaveBeenCalledWith({
-      query: {
-        page: 1,
-        pageSize: 100,
-        filters: '',
-        sortBy: 'id',
-        sortOrder: 'ASC'
-      }
-    });
-  });
-
-  it('should handle fetch areas error', async () => {
-    (areaClient.index.$get as any).mockRejectedValueOnce(new Error('API Error'));
-
-    const { result } = renderHook(() => useAreas(), {
-      wrapper: createWrapper()
-    });
-
-    // Wait for error
-    await waitFor(() => {
-      expect(result.current.isError).toBe(true);
-    });
-
-    expect(result.current.error).toBeDefined();
-  });
-
-  it('should create area successfully', async () => {
-    const mockAreaData = {
-      name: '北京市',
-      code: '110000',
-      level: 1,
-      parentId: null,
-      isDisabled: 0
-    };
-
-    (areaClient.index.$post as any).mockResolvedValueOnce(createMockResponse(201));
-
-    const { result } = renderHook(() => useCreateArea(), {
-      wrapper: createWrapper()
-    });
-
-    await result.current.mutateAsync(mockAreaData);
-
-    expect(areaClient.index.$post).toHaveBeenCalledWith({
-      json: mockAreaData
-    });
-  });
-
-  it('should update area successfully', async () => {
-    const mockUpdateData = {
-      name: '北京市更新',
-      code: '110000',
-      level: 1,
-      parentId: null,
-      isDisabled: 0
-    };
-
-    (areaClient[':id'].$put as any).mockResolvedValueOnce(createMockResponse(200));
-
-    const { result } = renderHook(() => useUpdateArea(), {
-      wrapper: createWrapper()
-    });
-
-    await result.current.mutateAsync({
-      id: 1,
-      data: mockUpdateData
-    });
-
-    expect(areaClient[':id'].$put).toHaveBeenCalledWith({
-      param: { id: 1 },
-      json: mockUpdateData
-    });
-  });
-
-  it('should delete area successfully', async () => {
-    (areaClient[':id'].$delete as any).mockResolvedValueOnce(createMockResponse(204));
-
-    const { result } = renderHook(() => useDeleteArea(), {
-      wrapper: createWrapper()
-    });
-
-    await result.current.mutateAsync(1);
-
-    expect(areaClient[':id'].$delete).toHaveBeenCalledWith({
-      param: { id: 1 }
-    });
-  });
-
-  it('should toggle area status successfully', async () => {
-    (areaClient[':id'].$put as any).mockResolvedValueOnce(createMockResponse(200));
-
-    const { result } = renderHook(() => useToggleAreaStatus(), {
-      wrapper: createWrapper()
-    });
-
-    await result.current.mutateAsync({
-      id: 1,
-      isDisabled: 1
-    });
-
-    expect(areaClient[':id'].$put).toHaveBeenCalledWith({
-      param: { id: 1 },
-      json: { isDisabled: 1 }
-    });
-  });
-});