Kaynağa Gözat

fix(disability-person-management-ui): 修复区域选择验证错误显示问题

- 创建 AreaSelectForm 组件用于 react-hook-form 集成
- 替换 DisabilityPersonManagement 中的 AreaSelect 为 AreaSelectForm
- 更新测试文件中的 mock 以支持 AreaSelectForm
- 修复省份和城市验证错误不显示的问题

AreaSelectForm 组件特性:
- 使用 watch 监听字段值变化
- 使用 setValue 更新字段并触发验证
- 使用 getFieldState 显示验证错误
- 支持 TypeScript 泛型确保类型安全

🤖 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 2 hafta önce
ebeveyn
işleme
8ab625f633

+ 17 - 23
allin-packages/disability-person-management-ui/src/components/DisabilityPersonManagement.tsx

@@ -19,7 +19,7 @@ import { CreateDisabledPersonSchema, UpdateDisabledPersonSchema } from '@d8d/all
 import type { CreateDisabledPersonRequest, UpdateDisabledPersonRequest } from '../api/disabilityClient';
 import { DISABILITY_TYPES, getDisabilityTypeLabel } from '@d8d/allin-enums';
 import { DISABILITY_LEVELS, getDisabilityLevelLabel } from '@d8d/allin-enums';
-import { AreaSelect } from '@d8d/area-management-ui/components';
+import { AreaSelectForm } from '@d8d/area-management-ui/components';
 import PhotoUploadField, { type PhotoItem } from './PhotoUploadField';
 import PhotoPreview from './PhotoPreview';
 import BankCardManagement, { type BankCardItem } from './BankCardManagement';
@@ -839,17 +839,14 @@ const DisabilityPersonManagement: React.FC = () => {
                     <div>
                       <FormLabel>居住地址 *</FormLabel>
                       <div className="space-y-4 mt-2">
-                        <AreaSelect
-                          value={{
-                            provinceId: createForm.watch('province') ? Number(createForm.watch('province')) : undefined,
-                            cityId: createForm.watch('city') ? Number(createForm.watch('city')) : undefined,
-                            districtId: createForm.watch('district') ? Number(createForm.watch('district')) : undefined
-                          }}
-                          onChange={(value) => {
-                            createForm.setValue('province', value.provinceId?.toString() || '');
-                            createForm.setValue('city', value.cityId?.toString() || '');
-                            createForm.setValue('district', value.districtId?.toString() || '');
-                          }}
+                        <AreaSelectForm<CreateDisabledPersonRequest>
+                          provinceName="province"
+                          cityName="city"
+                          districtName="district"
+                          label=""
+                          required={true}
+                          control={createForm.control}
+                          setValue={createForm.setValue}
                         />
 
                         <FormField
@@ -1207,17 +1204,14 @@ const DisabilityPersonManagement: React.FC = () => {
                   <div>
                     <FormLabel>居住地址 *</FormLabel>
                     <div className="space-y-4 mt-2">
-                      <AreaSelect
-                        value={{
-                          provinceId: updateForm.watch('province') ? Number(updateForm.watch('province')) : undefined,
-                          cityId: updateForm.watch('city') ? Number(updateForm.watch('city')) : undefined,
-                          districtId: updateForm.watch('district') ? Number(updateForm.watch('district')) : undefined
-                        }}
-                        onChange={(value) => {
-                          updateForm.setValue('province', value.provinceId?.toString() || '');
-                          updateForm.setValue('city', value.cityId?.toString() || '');
-                          updateForm.setValue('district', value.districtId?.toString() || '');
-                        }}
+                      <AreaSelectForm<UpdateDisabledPersonRequest>
+                        provinceName="province"
+                        cityName="city"
+                        districtName="district"
+                        label=""
+                        required={true}
+                        control={updateForm.control}
+                        setValue={updateForm.setValue}
                       />
 
                       <FormField

+ 81 - 6
allin-packages/disability-person-management-ui/tests/integration/disability-person.integration.test.tsx

@@ -310,6 +310,62 @@ vi.mock('@d8d/area-management-ui/components', () => ({
       </select>
     </div>
   )),
+  AreaSelectForm: vi.fn(({ provinceName, cityName, districtName, control, setValue }) => {
+    // 模拟 AreaSelectForm 的行为
+    // 使用简单的模拟值
+    const provinceValue = '1';
+    const cityValue = '2';
+    const districtValue = districtName ? '3' : '';
+
+    // 模拟 setValue 调用
+    const mockSetValue = vi.fn();
+
+    return (
+      <div data-testid="area-select-form">
+        <select
+          data-testid="province-select-form"
+          value={provinceValue}
+          onChange={(e) => {
+            if (setValue) {
+              setValue(provinceName, e.target.value, { shouldValidate: true });
+            }
+            mockSetValue(provinceName, e.target.value, { shouldValidate: true });
+          }}
+        >
+          <option value="">选择省份</option>
+          <option value="1">北京市</option>
+        </select>
+        <select
+          data-testid="city-select-form"
+          value={cityValue}
+          onChange={(e) => {
+            if (setValue) {
+              setValue(cityName, e.target.value, { shouldValidate: true });
+            }
+            mockSetValue(cityName, e.target.value, { shouldValidate: true });
+          }}
+        >
+          <option value="">选择城市</option>
+          <option value="2">北京市</option>
+        </select>
+        {districtName && (
+          <select
+            data-testid="district-select-form"
+            value={districtValue}
+            onChange={(e) => {
+              if (setValue) {
+                setValue(districtName, e.target.value, { shouldValidate: true });
+              }
+              mockSetValue(districtName, e.target.value, { shouldValidate: true });
+            }}
+          >
+            <option value="">选择区县</option>
+            <option value="3">东城区</option>
+          </select>
+        )}
+      </div>
+    );
+  }),
 }));
 
 // Mock 文件选择器组件
@@ -409,8 +465,8 @@ describe('残疾人个人管理集成测试', () => {
     fireEvent.change(disabilityLevelSelect, { target: { value: '二级' } });
 
     // 选择省份和城市
-    const provinceSelect = screen.getByTestId('province-select');
-    const citySelect = screen.getByTestId('city-select');
+    const provinceSelect = screen.getByTestId('province-select-form');
+    const citySelect = screen.getByTestId('city-select-form');
 
     await act(async () => {
       fireEvent.change(provinceSelect, { target: { value: '1' } });
@@ -428,6 +484,25 @@ describe('残疾人个人管理集成测试', () => {
       fireEvent.click(submitButton);
     });
 
+    // 等待一下,看看是否有验证错误
+    await new Promise(resolve => setTimeout(resolve, 100));
+
+    // 检查是否有验证错误显示
+    const validationErrors = screen.queryAllByText(/不能为空/);
+    if (validationErrors.length > 0) {
+      console.debug('验证错误:', validationErrors.map(err => err.textContent));
+    }
+
+    // 检查表单字段值
+    console.debug('表单提交状态 - 检查字段值:');
+    const nameInput2 = screen.getByPlaceholderText('请输入姓名');
+    const provinceSelect2 = screen.getByTestId('province-select-form');
+    const citySelect2 = screen.getByTestId('city-select-form');
+
+    console.debug('name:', (nameInput2 as HTMLInputElement).value);
+    console.debug('province select value:', (provinceSelect2 as HTMLSelectElement).value);
+    console.debug('city select value:', (citySelect2 as HTMLSelectElement).value);
+
     // 验证API调用 - 增加等待时间
     await waitFor(() => {
       const mockClient = (disabilityClientManager.get as any)();
@@ -580,18 +655,18 @@ describe('残疾人个人管理集成测试', () => {
     });
 
     // 验证区域选择器存在
-    expect(screen.getByTestId('area-select')).toBeInTheDocument();
+    expect(screen.getByTestId('area-select-form')).toBeInTheDocument();
 
     // 选择省份
-    const provinceSelect = screen.getByTestId('province-select');
+    const provinceSelect = screen.getByTestId('province-select-form');
     fireEvent.change(provinceSelect, { target: { value: '1' } });
 
     // 选择城市
-    const citySelect = screen.getByTestId('city-select');
+    const citySelect = screen.getByTestId('city-select-form');
     fireEvent.change(citySelect, { target: { value: '2' } });
 
     // 选择区县
-    const districtSelect = screen.getByTestId('district-select');
+    const districtSelect = screen.getByTestId('district-select-form');
     fireEvent.change(districtSelect, { target: { value: '3' } });
   });
 

+ 62 - 0
packages/area-management-ui/src/components/AreaSelectField.tsx

@@ -0,0 +1,62 @@
+import React from 'react';
+import { Controller, useFormContext } from 'react-hook-form';
+import { AreaSelect } from './AreaSelect';
+import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
+
+interface AreaSelectFieldProps {
+  name: string;
+  label?: string;
+  description?: string;
+  required?: boolean;
+  disabled?: boolean;
+  className?: string;
+}
+
+export const AreaSelectField: React.FC<AreaSelectFieldProps> = ({
+  name,
+  label = '地区选择',
+  description,
+  required = false,
+  disabled = false,
+  className
+}) => {
+  const { control } = useFormContext();
+
+  return (
+    <Controller
+      name={name}
+      control={control}
+      render={({ field, fieldState }) => {
+        // 将字段值转换为 AreaSelect 需要的格式
+        const value = {
+          provinceId: field.value?.provinceId || undefined,
+          cityId: field.value?.cityId || undefined,
+          districtId: field.value?.districtId || undefined
+        };
+
+        return (
+          <FormItem className={className}>
+            {label && (
+              <FormLabel>
+                {label}{required && <span className="text-destructive">*</span>}
+              </FormLabel>
+            )}
+            <FormControl>
+              <AreaSelect
+                value={value}
+                onChange={(newValue) => {
+                  // 更新表单字段值
+                  field.onChange(newValue);
+                }}
+                disabled={disabled}
+                required={required}
+              />
+            </FormControl>
+            {description && <FormDescription>{description}</FormDescription>}
+            <FormMessage>{fieldState.error?.message}</FormMessage>
+          </FormItem>
+        );
+      }}
+    />
+  );
+};

+ 102 - 0
packages/area-management-ui/src/components/AreaSelectForm.tsx

@@ -0,0 +1,102 @@
+import { Controller, useFormContext, Control, UseFormSetValue, FieldValues, Path } from 'react-hook-form';
+import { AreaSelect } from './AreaSelect';
+import { FormDescription, FormLabel } from '@d8d/shared-ui-components/components/ui/form';
+
+interface AreaSelectFormProps<T extends FieldValues = FieldValues> {
+  provinceName: Path<T>;
+  cityName: Path<T>;
+  districtName?: Path<T>;
+  label?: string;
+  description?: string;
+  required?: boolean;
+  disabled?: boolean;
+  className?: string;
+  control?: Control<T>;
+  setValue?: UseFormSetValue<T>;
+}
+
+export const AreaSelectForm = <T extends FieldValues = FieldValues>({
+  provinceName,
+  cityName,
+  districtName,
+  label = '地区选择',
+  description,
+  required = false,
+  disabled = false,
+  className,
+  control: propControl,
+  setValue: propSetValue
+}: AreaSelectFormProps<T>) => {
+  // 使用传入的 control 和 setValue,或者从上下文中获取
+  const formContext = useFormContext<T>();
+  const control = propControl || formContext?.control;
+  const setValue = propSetValue || formContext?.setValue;
+  const watch = formContext?.watch;
+
+  if (!control || !setValue || !watch) {
+    console.error('AreaSelectForm: 缺少 react-hook-form 上下文或 props');
+    return null;
+  }
+
+  // 监听表单字段值
+  const provinceValue = watch(provinceName as any);
+  const cityValue = watch(cityName as any);
+  const districtValue = districtName ? watch(districtName as any) : undefined;
+
+  return (
+    <div className={className}>
+      {label && (
+        <FormLabel>
+          {label}{required && <span className="text-destructive">*</span>}
+        </FormLabel>
+      )}
+
+      <AreaSelect
+        value={{
+          provinceId: provinceValue ? Number(provinceValue) : undefined,
+          cityId: cityValue ? Number(cityValue) : undefined,
+          districtId: districtValue ? Number(districtValue) : undefined
+        }}
+        onChange={(value) => {
+          console.debug('AreaSelectForm onChange:', {
+            provinceName,
+            cityName,
+            districtName,
+            value,
+            provinceValue: value.provinceId?.toString() || '',
+            cityValue: value.cityId?.toString() || '',
+            districtValue: value.districtId?.toString() || ''
+          });
+          // 更新所有相关字段
+          setValue(provinceName, (value.provinceId?.toString() || '') as any, { shouldValidate: true });
+          setValue(cityName, (value.cityId?.toString() || '') as any, { shouldValidate: true });
+          if (districtName) {
+            setValue(districtName, (value.districtId?.toString() || '') as any, { shouldValidate: true });
+          }
+        }}
+        disabled={disabled}
+        required={required}
+      />
+
+      {/* 显示省份验证错误 */}
+      {control?.getFieldState(provinceName as any).error && (
+        <div className="text-sm font-medium text-destructive mt-1">
+          {control.getFieldState(provinceName as any).error?.message}
+        </div>
+      )}
+
+      {/* 显示城市验证错误 */}
+      {control?.getFieldState(cityName as any).error && (
+        <div className="text-sm font-medium text-destructive mt-1">
+          {control.getFieldState(cityName as any).error?.message}
+        </div>
+      )}
+
+      {description && (
+        <FormDescription className="mt-2">
+          {description}
+        </FormDescription>
+      )}
+    </div>
+  );
+};

+ 2 - 1
packages/area-management-ui/src/components/index.ts

@@ -2,4 +2,5 @@ export { AreaManagement } from './AreaManagement';
 export { AreaForm } from './AreaForm';
 export { AreaTreeAsync } from './AreaTreeAsync';
 export { AreaSelect } from './AreaSelect';
-export { AreaSelect4Level } from './AreaSelect4Level';
+export { AreaSelect4Level } from './AreaSelect4Level';
+export { AreaSelectForm } from './AreaSelectForm';