Procházet zdrojové kódy

✨ feat(area-select): 重构地区选择组件为模块化架构

- 重命名旧组件文件为 `.old.tsx` 以保留历史版本
- 新增 `AreaProvider` 上下文组件,统一管理地区数据获取和缓存
- 新增基础组件:`ProvinceSelect`、`CitySelect`、`DistrictSelect`、`TownSelect`
- 新增组合组件:`AreaSelect`(3级)、`AreaSelect4Level`(4级)、`AreaSelectForm`(表单集成)
- 新增表单字段组件:`ProvinceSelectField`、`CitySelectField`、`DistrictSelectField`、`TownSelectField`
- 新增自定义 Hook:`useProvinces`、`useCities`、`useDistricts`、`useTowns`、`useAreas`
- 新增示例文件 `example-usage.tsx` 展示各种使用方式
- 更新主导出文件 `index.ts`,支持向后兼容
- 更新集成测试文件以使用新的组件路径

♻️ refactor(area-select): 优化组件结构和数据流

- 采用分层架构:基础组件 → 组合组件 → 表单组件
- 使用 React Query 进行数据缓存和状态管理
- 实现级联选择逻辑:省份变化清空城市和区县,城市变化清空区县
- 提供灵活的配置选项:是否显示标签、是否必填、占位符等
- 支持测试 ID 属性,便于自动化测试
- 统一错误处理和加载状态显示

📝 docs(area-select): 更新组件文档和示例

- 提供完整的示例代码展示各种使用场景
- 展示如何与 `react-hook-form` 集成
- 说明组件间的依赖关系和级联逻辑
- 保持向后兼容性,旧组件仍可通过新架构访问
yourname před 1 týdnem
rodič
revize
64190ae261
21 změnil soubory, kde provedl 1384 přidání a 6 odebrání
  1. 0 0
      packages/area-management-ui/src/components/AreaSelect.old.tsx
  2. 0 0
      packages/area-management-ui/src/components/AreaSelect4Level.old.tsx
  3. 0 0
      packages/area-management-ui/src/components/AreaSelectForm.old.tsx
  4. 154 0
      packages/area-management-ui/src/components/areas/AreaProvider.tsx
  5. 67 0
      packages/area-management-ui/src/components/areas/base/CitySelect.tsx
  6. 67 0
      packages/area-management-ui/src/components/areas/base/DistrictSelect.tsx
  7. 63 0
      packages/area-management-ui/src/components/areas/base/ProvinceSelect.tsx
  8. 67 0
      packages/area-management-ui/src/components/areas/base/TownSelect.tsx
  9. 118 0
      packages/area-management-ui/src/components/areas/composite/AreaSelect.tsx
  10. 131 0
      packages/area-management-ui/src/components/areas/composite/AreaSelect4Level.tsx
  11. 91 0
      packages/area-management-ui/src/components/areas/composite/AreaSelectForm.tsx
  12. 164 0
      packages/area-management-ui/src/components/areas/example-usage.tsx
  13. 77 0
      packages/area-management-ui/src/components/areas/form/CitySelectField.tsx
  14. 72 0
      packages/area-management-ui/src/components/areas/form/DistrictSelectField.tsx
  15. 64 0
      packages/area-management-ui/src/components/areas/form/ProvinceSelectField.tsx
  16. 72 0
      packages/area-management-ui/src/components/areas/form/TownSelectField.tsx
  17. 144 0
      packages/area-management-ui/src/components/areas/hooks/useAreas.ts
  18. 22 0
      packages/area-management-ui/src/components/areas/index.ts
  19. 8 3
      packages/area-management-ui/src/components/index.ts
  20. 1 1
      packages/area-management-ui/tests/integration/area-select-form.integration.test.tsx
  21. 2 2
      packages/area-management-ui/tests/integration/area-select.integration.test.tsx

+ 0 - 0
packages/area-management-ui/src/components/AreaSelect.tsx → packages/area-management-ui/src/components/AreaSelect.old.tsx


+ 0 - 0
packages/area-management-ui/src/components/AreaSelect4Level.tsx → packages/area-management-ui/src/components/AreaSelect4Level.old.tsx


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


+ 154 - 0
packages/area-management-ui/src/components/areas/AreaProvider.tsx

@@ -0,0 +1,154 @@
+import React, { createContext, useContext, ReactNode } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { areaClient, areaClientManager } from '../../api/areaClient';
+import type { InferResponseType } from 'hono/client';
+
+// 类型定义 - 使用areaClient进行类型推断
+type AreaResponse = InferResponseType<typeof areaClient.index.$get, 200>['data'][0];
+
+interface AreaContextValue {
+  // 数据获取函数
+  getProvinces: () => Promise<AreaResponse[]>;
+  getCities: (provinceId: number) => Promise<AreaResponse[]>;
+  getDistricts: (cityId: number) => Promise<AreaResponse[]>;
+  getTowns: (districtId: number) => Promise<AreaResponse[]>;
+
+  // 缓存数据
+  provinces?: AreaResponse[];
+  cities?: AreaResponse[];
+  districts?: AreaResponse[];
+  towns?: AreaResponse[];
+
+  // 加载状态
+  isLoadingProvinces: boolean;
+  isLoadingCities: boolean;
+  isLoadingDistricts: boolean;
+  isLoadingTowns: boolean;
+}
+
+const AreaContext = createContext<AreaContextValue | undefined>(undefined);
+
+interface AreaProviderProps {
+  children: ReactNode;
+}
+
+export const AreaProvider: React.FC<AreaProviderProps> = ({ children }) => {
+  // 查询省份列表
+  const { data: provinces, isLoading: isLoadingProvinces } = useQuery({
+    queryKey: ['areas', 'provinces'],
+    queryFn: async () => {
+      const res = await areaClientManager.get().index.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({
+            level: 1,
+            isDisabled: 0
+          }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取省份列表失败');
+      const json = await res.json();
+      return json.data;
+    },
+    staleTime: 10 * 60 * 1000,
+    gcTime: 30 * 60 * 1000,
+  });
+
+  // 获取城市列表的函数
+  const getCities = async (provinceId: number): Promise<AreaResponse[]> => {
+    if (!provinceId) return [];
+
+    const res = await areaClientManager.get().index.$get({
+      query: {
+        page: 1,
+        pageSize: 100,
+        filters: JSON.stringify({
+          level: 2,
+          parentId: provinceId,
+          isDisabled: 0
+        }),
+        sortBy: 'id',
+        sortOrder: 'ASC'
+      }
+    });
+
+    if (res.status !== 200) throw new Error('获取城市列表失败');
+    const json = await res.json();
+    return json.data;
+  };
+
+  // 获取区县列表的函数
+  const getDistricts = async (cityId: number): Promise<AreaResponse[]> => {
+    if (!cityId) return [];
+
+    const res = await areaClientManager.get().index.$get({
+      query: {
+        page: 1,
+        pageSize: 100,
+        filters: JSON.stringify({
+          level: 3,
+          parentId: cityId,
+          isDisabled: 0
+        }),
+        sortBy: 'id',
+        sortOrder: 'ASC'
+      }
+    });
+
+    if (res.status !== 200) throw new Error('获取区县列表失败');
+    const json = await res.json();
+    return json.data;
+  };
+
+  // 获取街道列表的函数
+  const getTowns = async (districtId: number): Promise<AreaResponse[]> => {
+    if (!districtId) return [];
+
+    const res = await areaClientManager.get().index.$get({
+      query: {
+        page: 1,
+        pageSize: 100,
+        filters: JSON.stringify({
+          level: 4,
+          parentId: districtId,
+          isDisabled: 0
+        }),
+        sortBy: 'id',
+        sortOrder: 'ASC'
+      }
+    });
+
+    if (res.status !== 200) throw new Error('获取街道列表失败');
+    const json = await res.json();
+    return json.data;
+  };
+
+  const value: AreaContextValue = {
+    getProvinces: async () => provinces || [],
+    getCities,
+    getDistricts,
+    getTowns,
+    provinces,
+    isLoadingProvinces,
+    isLoadingCities: false, // 这些状态由具体组件管理
+    isLoadingDistricts: false,
+    isLoadingTowns: false,
+  };
+
+  return (
+    <AreaContext.Provider value={value}>
+      {children}
+    </AreaContext.Provider>
+  );
+};
+
+export const useAreaContext = () => {
+  const context = useContext(AreaContext);
+  if (!context) {
+    throw new Error('useAreaContext必须在AreaProvider内部使用');
+  }
+  return context;
+};

+ 67 - 0
packages/area-management-ui/src/components/areas/base/CitySelect.tsx

@@ -0,0 +1,67 @@
+import React from 'react';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { Label } from '@d8d/shared-ui-components/components/ui/label';
+import { useCities } from '../hooks/useAreas';
+
+interface CitySelectProps {
+  provinceId?: string | number;
+  value?: string | number;
+  onChange?: (value: string) => void;
+  disabled?: boolean;
+  required?: boolean;
+  placeholder?: string;
+  className?: string;
+  showLabel?: boolean;
+  labelText?: string;
+  'data-testid'?: string;
+}
+
+export const CitySelect: React.FC<CitySelectProps> = ({
+  provinceId,
+  value,
+  onChange,
+  disabled = false,
+  required = false,
+  placeholder = '选择城市',
+  className,
+  showLabel = true,
+  labelText = '城市',
+  'data-testid': testId = 'city-select',
+}) => {
+  const provinceIdNum = provinceId ? Number(provinceId) : undefined;
+  const { data: cities, isLoading } = useCities(provinceIdNum);
+
+  const handleValueChange = (newValue: string) => {
+    onChange?.(newValue && newValue !== 'none' ? newValue : '');
+  };
+
+  const currentValue = value?.toString() || '';
+  const isDisabled = disabled || isLoading || !provinceId;
+
+  return (
+    <div className={className}>
+      {showLabel && (
+        <Label htmlFor={testId}>
+          {labelText}{required && provinceId && <span className="text-destructive">*</span>}
+        </Label>
+      )}
+      <Select
+        value={currentValue}
+        onValueChange={handleValueChange}
+        disabled={isDisabled}
+      >
+        <SelectTrigger id={testId} data-testid={testId} className={showLabel ? 'mt-1' : ''}>
+          <SelectValue placeholder={isLoading ? '加载中...' : isDisabled ? '请先选择省份' : placeholder} />
+        </SelectTrigger>
+        <SelectContent>
+          <SelectItem value="none">请选择城市</SelectItem>
+          {cities?.map((city) => (
+            <SelectItem key={city.id} value={city.id.toString()}>
+              {city.name}
+            </SelectItem>
+          ))}
+        </SelectContent>
+      </Select>
+    </div>
+  );
+};

+ 67 - 0
packages/area-management-ui/src/components/areas/base/DistrictSelect.tsx

@@ -0,0 +1,67 @@
+import React from 'react';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { Label } from '@d8d/shared-ui-components/components/ui/label';
+import { useDistricts } from '../hooks/useAreas';
+
+interface DistrictSelectProps {
+  cityId?: string | number;
+  value?: string | number;
+  onChange?: (value: string) => void;
+  disabled?: boolean;
+  // required?: boolean; // 区县字段通常不是必填的
+  placeholder?: string;
+  className?: string;
+  showLabel?: boolean;
+  labelText?: string;
+  'data-testid'?: string;
+}
+
+export const DistrictSelect: React.FC<DistrictSelectProps> = ({
+  cityId,
+  value,
+  onChange,
+  disabled = false,
+  // required = false,
+  placeholder = '选择区县',
+  className,
+  showLabel = true,
+  labelText = '区县',
+  'data-testid': testId = 'district-select',
+}) => {
+  const cityIdNum = cityId ? Number(cityId) : undefined;
+  const { data: districts, isLoading } = useDistricts(cityIdNum);
+
+  const handleValueChange = (newValue: string) => {
+    onChange?.(newValue && newValue !== 'none' ? newValue : '');
+  };
+
+  const currentValue = value?.toString() || '';
+  const isDisabled = disabled || isLoading || !cityId;
+
+  return (
+    <div className={className}>
+      {showLabel && (
+        <Label htmlFor={testId}>
+          {labelText}
+        </Label>
+      )}
+      <Select
+        value={currentValue}
+        onValueChange={handleValueChange}
+        disabled={isDisabled}
+      >
+        <SelectTrigger id={testId} data-testid={testId} className={showLabel ? 'mt-1' : ''}>
+          <SelectValue placeholder={isLoading ? '加载中...' : isDisabled ? '请先选择城市' : placeholder} />
+        </SelectTrigger>
+        <SelectContent>
+          <SelectItem value="none">请选区县</SelectItem>
+          {districts?.map((district) => (
+            <SelectItem key={district.id} value={district.id.toString()}>
+              {district.name}
+            </SelectItem>
+          ))}
+        </SelectContent>
+      </Select>
+    </div>
+  );
+};

+ 63 - 0
packages/area-management-ui/src/components/areas/base/ProvinceSelect.tsx

@@ -0,0 +1,63 @@
+import React from 'react';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { Label } from '@d8d/shared-ui-components/components/ui/label';
+import { useProvinces } from '../hooks/useAreas';
+
+interface ProvinceSelectProps {
+  value?: string | number;
+  onChange?: (value: string) => void;
+  disabled?: boolean;
+  required?: boolean;
+  placeholder?: string;
+  className?: string;
+  showLabel?: boolean;
+  labelText?: string;
+  'data-testid'?: string;
+}
+
+export const ProvinceSelect: React.FC<ProvinceSelectProps> = ({
+  value,
+  onChange,
+  disabled = false,
+  required = false,
+  placeholder = '选择省份',
+  className,
+  showLabel = true,
+  labelText = '省份',
+  'data-testid': testId = 'province-select',
+}) => {
+  const { data: provinces, isLoading } = useProvinces();
+
+  const handleValueChange = (newValue: string) => {
+    onChange?.(newValue && newValue !== 'none' ? newValue : '');
+  };
+
+  const currentValue = value?.toString() || '';
+
+  return (
+    <div className={className}>
+      {showLabel && (
+        <Label htmlFor={testId}>
+          {labelText}{required && <span className="text-destructive">*</span>}
+        </Label>
+      )}
+      <Select
+        value={currentValue}
+        onValueChange={handleValueChange}
+        disabled={disabled || isLoading}
+      >
+        <SelectTrigger id={testId} data-testid={testId} className={showLabel ? 'mt-1' : ''}>
+          <SelectValue placeholder={isLoading ? '加载中...' : placeholder} />
+        </SelectTrigger>
+        <SelectContent>
+          <SelectItem value="none">请选择省份</SelectItem>
+          {provinces?.map((province) => (
+            <SelectItem key={province.id} value={province.id.toString()}>
+              {province.name}
+            </SelectItem>
+          ))}
+        </SelectContent>
+      </Select>
+    </div>
+  );
+};

+ 67 - 0
packages/area-management-ui/src/components/areas/base/TownSelect.tsx

@@ -0,0 +1,67 @@
+import React from 'react';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { Label } from '@d8d/shared-ui-components/components/ui/label';
+import { useTowns } from '../hooks/useAreas';
+
+interface TownSelectProps {
+  districtId?: string | number;
+  value?: string | number;
+  onChange?: (value: string) => void;
+  disabled?: boolean;
+  // required?: boolean; // 街道字段通常不是必填的
+  placeholder?: string;
+  className?: string;
+  showLabel?: boolean;
+  labelText?: string;
+  'data-testid'?: string;
+}
+
+export const TownSelect: React.FC<TownSelectProps> = ({
+  districtId,
+  value,
+  onChange,
+  disabled = false,
+  // required = false,
+  placeholder = '选择街道',
+  className,
+  showLabel = true,
+  labelText = '街道',
+  'data-testid': testId = 'town-select',
+}) => {
+  const districtIdNum = districtId ? Number(districtId) : undefined;
+  const { data: towns, isLoading } = useTowns(districtIdNum);
+
+  const handleValueChange = (newValue: string) => {
+    onChange?.(newValue && newValue !== 'none' ? newValue : '');
+  };
+
+  const currentValue = value?.toString() || '';
+  const isDisabled = disabled || isLoading || !districtId;
+
+  return (
+    <div className={className}>
+      {showLabel && (
+        <Label htmlFor={testId}>
+          {labelText}
+        </Label>
+      )}
+      <Select
+        value={currentValue}
+        onValueChange={handleValueChange}
+        disabled={isDisabled}
+      >
+        <SelectTrigger id={testId} data-testid={testId} className={showLabel ? 'mt-1' : ''}>
+          <SelectValue placeholder={isLoading ? '加载中...' : isDisabled ? '请先选择区县' : placeholder} />
+        </SelectTrigger>
+        <SelectContent>
+          <SelectItem value="none">请选择街道</SelectItem>
+          {towns?.map((town) => (
+            <SelectItem key={town.id} value={town.id.toString()}>
+              {town.name}
+            </SelectItem>
+          ))}
+        </SelectContent>
+      </Select>
+    </div>
+  );
+};

+ 118 - 0
packages/area-management-ui/src/components/areas/composite/AreaSelect.tsx

@@ -0,0 +1,118 @@
+import React, { useState, useEffect } from 'react';
+import { ProvinceSelect } from '../base/ProvinceSelect';
+import { CitySelect } from '../base/CitySelect';
+import { DistrictSelect } from '../base/DistrictSelect';
+
+interface AreaSelectProps {
+  value?: {
+    provinceId?: number | string;
+    cityId?: number | string;
+    districtId?: number | string;
+  };
+  onChange?: (value: {
+    provinceId?: number | string;
+    cityId?: number | string;
+    districtId?: number | string;
+  }) => void;
+  disabled?: boolean;
+  required?: boolean;
+  className?: string;
+  'data-testid'?: string;
+}
+
+export const AreaSelect: React.FC<AreaSelectProps> = ({
+  value = {},
+  onChange,
+  disabled = false,
+  required = false,
+  className,
+  'data-testid': testId = 'area-select',
+}) => {
+  const [selectedProvince, setSelectedProvince] = useState<string>(value.provinceId?.toString() || '');
+  const [selectedCity, setSelectedCity] = useState<string>(value.cityId?.toString() || '');
+  const [selectedDistrict, setSelectedDistrict] = useState<string>(value.districtId?.toString() || '');
+
+  // 处理省份变化
+  const handleProvinceChange = (provinceId: string) => {
+    setSelectedProvince(provinceId);
+    setSelectedCity('');
+    setSelectedDistrict('');
+
+    onChange?.({
+      provinceId: provinceId || undefined,
+      cityId: undefined,
+      districtId: undefined
+    });
+  };
+
+  // 处理城市变化
+  const handleCityChange = (cityId: string) => {
+    setSelectedCity(cityId);
+    setSelectedDistrict('');
+
+    onChange?.({
+      provinceId: selectedProvince || undefined,
+      cityId: cityId || undefined,
+      districtId: undefined
+    });
+  };
+
+  // 处理区县变化
+  const handleDistrictChange = (districtId: string) => {
+    setSelectedDistrict(districtId);
+
+    onChange?.({
+      provinceId: selectedProvince || undefined,
+      cityId: selectedCity || undefined,
+      districtId: districtId || undefined
+    });
+  };
+
+  // 同步外部值变化
+  useEffect(() => {
+    setSelectedProvince(value.provinceId?.toString() || '');
+    setSelectedCity(value.cityId?.toString() || '');
+    setSelectedDistrict(value.districtId?.toString() || '');
+  }, [value.provinceId, value.cityId, value.districtId]);
+
+  return (
+    <div
+      className={`grid grid-cols-1 md:grid-cols-3 gap-4 ${className}`}
+      data-testid={testId}
+    >
+      <ProvinceSelect
+        value={selectedProvince}
+        onChange={handleProvinceChange}
+        disabled={disabled}
+        required={required}
+        placeholder="选择省份"
+        showLabel={true}
+        labelText="省份"
+        data-testid={`${testId}-province`}
+      />
+
+      <CitySelect
+        provinceId={selectedProvince}
+        value={selectedCity}
+        onChange={handleCityChange}
+        disabled={disabled}
+        required={required}
+        placeholder="选择城市"
+        showLabel={true}
+        labelText="城市"
+        data-testid={`${testId}-city`}
+      />
+
+      <DistrictSelect
+        cityId={selectedCity}
+        value={selectedDistrict}
+        onChange={handleDistrictChange}
+        disabled={disabled}
+        placeholder="选择区县"
+        showLabel={true}
+        labelText="区县"
+        data-testid={`${testId}-district`}
+      />
+    </div>
+  );
+};

+ 131 - 0
packages/area-management-ui/src/components/areas/composite/AreaSelect4Level.tsx

@@ -0,0 +1,131 @@
+import React, { useState, useEffect } from 'react';
+import { ProvinceSelect } from '../base/ProvinceSelect';
+import { CitySelect } from '../base/CitySelect';
+import { DistrictSelect } from '../base/DistrictSelect';
+import { TownSelect } from '../base/TownSelect';
+
+interface AreaSelect4LevelProps {
+  provinceValue?: number | string;
+  cityValue?: number | string;
+  districtValue?: number | string;
+  townValue?: number | string;
+  onProvinceChange?: (value: number) => void;
+  onCityChange?: (value: number) => void;
+  onDistrictChange?: (value: number) => void;
+  onTownChange?: (value: number) => void;
+  disabled?: boolean;
+  required?: boolean;
+  className?: string;
+  showLabels?: boolean;
+}
+
+export const AreaSelect4Level: React.FC<AreaSelect4LevelProps> = ({
+  provinceValue = 0,
+  cityValue = 0,
+  districtValue = 0,
+  townValue = 0,
+  onProvinceChange,
+  onCityChange,
+  onDistrictChange,
+  onTownChange,
+  disabled = false,
+  required = false,
+  className = '',
+  showLabels = true
+}) => {
+  const [selectedProvince, setSelectedProvince] = useState<string>(provinceValue?.toString() || '');
+  const [selectedCity, setSelectedCity] = useState<string>(cityValue?.toString() || '');
+  const [selectedDistrict, setSelectedDistrict] = useState<string>(districtValue?.toString() || '');
+  const [selectedTown, setSelectedTown] = useState<string>(townValue?.toString() || '');
+
+  // 处理省份变化
+  const handleProvinceChange = (provinceId: string) => {
+    const id = provinceId && provinceId !== 'none' ? Number(provinceId) : 0;
+    setSelectedProvince(provinceId);
+    setSelectedCity('');
+    setSelectedDistrict('');
+    setSelectedTown('');
+    onProvinceChange?.(id);
+  };
+
+  // 处理城市变化
+  const handleCityChange = (cityId: string) => {
+    const id = cityId && cityId !== 'none' ? Number(cityId) : 0;
+    setSelectedCity(cityId);
+    setSelectedDistrict('');
+    setSelectedTown('');
+    onCityChange?.(id);
+  };
+
+  // 处理区县变化
+  const handleDistrictChange = (districtId: string) => {
+    const id = districtId && districtId !== 'none' ? Number(districtId) : 0;
+    setSelectedDistrict(districtId);
+    setSelectedTown('');
+    onDistrictChange?.(id);
+  };
+
+  // 处理街道变化
+  const handleTownChange = (townId: string) => {
+    const id = townId && townId !== 'none' ? Number(townId) : 0;
+    setSelectedTown(townId);
+    onTownChange?.(id);
+  };
+
+  // 同步外部值变化
+  useEffect(() => {
+    setSelectedProvince(provinceValue?.toString() || '');
+    setSelectedCity(cityValue?.toString() || '');
+    setSelectedDistrict(districtValue?.toString() || '');
+    setSelectedTown(townValue?.toString() || '');
+  }, [provinceValue, cityValue, districtValue, townValue]);
+
+  return (
+    <div className={`grid grid-cols-1 md:grid-cols-4 gap-4 ${className}`}>
+      <ProvinceSelect
+        value={selectedProvince}
+        onChange={handleProvinceChange}
+        disabled={disabled}
+        required={required}
+        placeholder="选择省份"
+        showLabel={showLabels}
+        labelText="省份"
+        data-testid="area-select-4level-province"
+      />
+
+      <CitySelect
+        provinceId={selectedProvince}
+        value={selectedCity}
+        onChange={handleCityChange}
+        disabled={disabled}
+        required={required}
+        placeholder="选择城市"
+        showLabel={showLabels}
+        labelText="城市"
+        data-testid="area-select-4level-city"
+      />
+
+      <DistrictSelect
+        cityId={selectedCity}
+        value={selectedDistrict}
+        onChange={handleDistrictChange}
+        disabled={disabled}
+        placeholder="选择区县"
+        showLabel={showLabels}
+        labelText="区县"
+        data-testid="area-select-4level-district"
+      />
+
+      <TownSelect
+        districtId={selectedDistrict}
+        value={selectedTown}
+        onChange={handleTownChange}
+        disabled={disabled}
+        placeholder="选择街道"
+        showLabel={showLabels}
+        labelText="街道"
+        data-testid="area-select-4level-town"
+      />
+    </div>
+  );
+};

+ 91 - 0
packages/area-management-ui/src/components/areas/composite/AreaSelectForm.tsx

@@ -0,0 +1,91 @@
+import { useFormContext, FieldValues, Path } from 'react-hook-form';
+import { FormDescription, FormLabel } from '@d8d/shared-ui-components/components/ui/form';
+import { ProvinceSelectField } from '../form/ProvinceSelectField';
+import { CitySelectField } from '../form/CitySelectField';
+import { DistrictSelectField } from '../form/DistrictSelectField';
+
+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;
+  testIdPrefix?: string;
+}
+
+export const AreaSelectForm = <T extends FieldValues = FieldValues>({
+  provinceName,
+  cityName,
+  districtName,
+  label = '地区选择',
+  description,
+  required = false,
+  disabled = false,
+  className,
+  testIdPrefix = 'area-select'
+}: AreaSelectFormProps<T>) => {
+  // 从上下文中获取表单控制
+  const formContext = useFormContext<T>();
+  const control = formContext?.control;
+
+  if (!control) {
+    console.error('AreaSelectForm: 缺少 react-hook-form 上下文');
+    return null;
+  }
+
+  // 注意:清理逻辑现在由各个表单字段组件内部的Controller处理
+  // 省份变化时会自动清空城市和区县字段
+  // 城市变化时会自动清空区县字段
+
+  return (
+    <div className={className}>
+      {label && (
+        <FormLabel>
+          {label}{required && <span className="text-destructive">*</span>}
+        </FormLabel>
+      )}
+
+      <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
+        <ProvinceSelectField
+          name={provinceName}
+          label="省份"
+          required={required}
+          disabled={disabled}
+          placeholder="选择省份"
+          data-testid={`${testIdPrefix}-province`}
+          // 注意:这里不能直接传递onValueChange,因为Controller已经处理了
+        />
+
+        <CitySelectField
+          name={cityName}
+          provinceName={provinceName}
+          label="城市"
+          required={required}
+          disabled={disabled}
+          placeholder="选择城市"
+          data-testid={`${testIdPrefix}-city`}
+        />
+
+        {districtName && (
+          <DistrictSelectField
+            name={districtName}
+            cityName={cityName}
+            label="区县"
+            disabled={disabled}
+            placeholder="选择区县"
+            data-testid={`${testIdPrefix}-district`}
+          />
+        )}
+      </div>
+
+      {description && (
+        <FormDescription className="mt-2">
+          {description}
+        </FormDescription>
+      )}
+    </div>
+  );
+};

+ 164 - 0
packages/area-management-ui/src/components/areas/example-usage.tsx

@@ -0,0 +1,164 @@
+// 示例文件,展示新组件架构的使用方式
+// 这个文件不需要导出,仅用于演示
+
+import React, { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { Form } from '@d8d/shared-ui-components/components/ui/form';
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
+
+// 导入新的组件
+import { AreaProvider } from './AreaProvider';
+import { ProvinceSelect } from './base/ProvinceSelect';
+import { CitySelect } from './base/CitySelect';
+import { DistrictSelect } from './base/DistrictSelect';
+import { TownSelect } from './base/TownSelect';
+import { AreaSelect } from './composite/AreaSelect';
+import { AreaSelectForm } from './composite/AreaSelectForm';
+import { AreaSelect4Level } from './composite/AreaSelect4Level';
+
+// 示例1: 使用AreaProvider包装应用
+export const AppWithAreaProvider = () => {
+  return (
+    <AreaProvider>
+      <YourComponent />
+    </AreaProvider>
+  );
+};
+
+// 示例2: 单独使用基础组件
+export const IndividualComponentsExample = () => {
+  const [provinceId, setProvinceId] = useState<string>('');
+  const [cityId, setCityId] = useState<string>('');
+  const [districtId, setDistrictId] = useState<string>('');
+  const [townId, setTownId] = useState<string>('');
+
+  return (
+    <div className="space-y-4 p-4">
+      <h2 className="text-lg font-bold">单独使用基础组件</h2>
+
+      <ProvinceSelect
+        value={provinceId}
+        onChange={setProvinceId}
+        labelText="选择省份"
+        required={true}
+      />
+
+      <CitySelect
+        provinceId={provinceId}
+        value={cityId}
+        onChange={setCityId}
+        labelText="选择城市"
+        required={true}
+      />
+
+      <DistrictSelect
+        cityId={cityId}
+        value={districtId}
+        onChange={setDistrictId}
+        labelText="选择区县"
+      />
+
+      <TownSelect
+        districtId={districtId}
+        value={townId}
+        onChange={setTownId}
+        labelText="选择街道"
+      />
+
+      <div className="mt-4 p-2 bg-gray-100 rounded">
+        <p>当前选择:</p>
+        <p>省份: {provinceId}</p>
+        <p>城市: {cityId}</p>
+        <p>区县: {districtId}</p>
+        <p>街道: {townId}</p>
+      </div>
+    </div>
+  );
+};
+
+// 示例3: 使用组合组件
+export const CompositeComponentsExample = () => {
+  const [areaValue, setAreaValue] = useState({
+    provinceId: '',
+    cityId: '',
+    districtId: ''
+  });
+
+  return (
+    <div className="space-y-4 p-4">
+      <h2 className="text-lg font-bold">使用3级组合组件</h2>
+
+      <AreaSelect
+        value={areaValue}
+        onChange={setAreaValue}
+        required={true}
+      />
+
+      <h2 className="text-lg font-bold mt-8">使用4级组合组件</h2>
+
+      <AreaSelect4Level
+        provinceValue={areaValue.provinceId}
+        cityValue={areaValue.cityId}
+        districtValue={areaValue.districtId}
+        townValue=""
+        onProvinceChange={(id) => setAreaValue({...areaValue, provinceId: id.toString()})}
+        onCityChange={(id) => setAreaValue({...areaValue, cityId: id.toString()})}
+        onDistrictChange={(id) => setAreaValue({...areaValue, districtId: id.toString()})}
+        onTownChange={() => {}}
+      />
+    </div>
+  );
+};
+
+// 示例4: 使用表单组件
+export const FormComponentsExample = () => {
+  const form = useForm({
+    defaultValues: {
+      province: '',
+      city: '',
+      district: '',
+      town: ''
+    }
+  });
+
+  const onSubmit = (data: any) => {
+    console.log('表单提交:', data);
+  };
+
+  return (
+    <Form {...form}>
+      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 p-4">
+        <h2 className="text-lg font-bold">使用表单组件</h2>
+
+        {/* 使用AreaSelectForm组合组件 */}
+        <AreaSelectForm
+          provinceName="province"
+          cityName="city"
+          districtName="district"
+          label="地区选择"
+          required={true}
+        />
+
+        <Button type="submit">提交表单</Button>
+      </form>
+    </Form>
+  );
+};
+
+// 主示例组件
+const YourComponent = () => {
+  return (
+    <div className="p-4">
+      <h1 className="text-2xl font-bold mb-6">地区选择组件示例</h1>
+
+      <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
+        <IndividualComponentsExample />
+        <CompositeComponentsExample />
+      </div>
+
+      <div className="mt-8">
+        <FormComponentsExample />
+      </div>
+    </div>
+  );
+};

+ 77 - 0
packages/area-management-ui/src/components/areas/form/CitySelectField.tsx

@@ -0,0 +1,77 @@
+import { Controller, useFormContext, FieldValues, Path, useWatch } from 'react-hook-form';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { FormControl, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
+import { useCities } from '../hooks/useAreas';
+
+interface CitySelectFieldProps<T extends FieldValues = FieldValues> {
+  name: Path<T>;
+  provinceName: Path<T>;
+  label?: string;
+  required?: boolean;
+  disabled?: boolean;
+  placeholder?: string;
+  className?: string;
+  'data-testid'?: string;
+}
+
+export const CitySelectField = <T extends FieldValues = FieldValues>({
+  name,
+  provinceName,
+  label = '城市',
+  required = false,
+  disabled = false,
+  placeholder = '选择城市',
+  className,
+  'data-testid': testId = 'city-select-field',
+}: CitySelectFieldProps<T>) => {
+  const { control } = useFormContext<T>();
+  const provinceValue = useWatch({ control, name: provinceName });
+  const provinceId = provinceValue ? Number(provinceValue) : undefined;
+
+  const { data: cities, isLoading } = useCities(provinceId);
+
+  return (
+    <Controller
+      name={name}
+      control={control}
+      render={({ field, fieldState }) => {
+        const isDisabled = disabled || isLoading || !provinceValue;
+
+        return (
+          <FormItem className={className}>
+            {label && (
+              <FormLabel>
+                {label}{required && provinceValue && <span className="text-destructive">*</span>}
+              </FormLabel>
+            )}
+            <Select
+              value={field.value || ''}
+              onValueChange={(value) => {
+                field.onChange(value && value !== 'none' ? value : '');
+                // 注意:清空依赖字段的逻辑由父组件处理
+              }}
+              disabled={isDisabled}
+            >
+              <FormControl>
+                <SelectTrigger data-testid={testId}>
+                  <SelectValue placeholder={isLoading ? '加载中...' : isDisabled ? '请先选择省份' : placeholder} />
+                </SelectTrigger>
+              </FormControl>
+              <SelectContent>
+                <SelectItem value="none">请选择城市</SelectItem>
+                {cities?.map((city) => (
+                  <SelectItem key={city.id} value={city.id.toString()}>
+                    {city.name}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+            {fieldState.error && (
+              <FormMessage>{fieldState.error.message}</FormMessage>
+            )}
+          </FormItem>
+        );
+      }}
+    />
+  );
+};

+ 72 - 0
packages/area-management-ui/src/components/areas/form/DistrictSelectField.tsx

@@ -0,0 +1,72 @@
+import { Controller, useFormContext, FieldValues, Path, useWatch } from 'react-hook-form';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { FormControl, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
+import { useDistricts } from '../hooks/useAreas';
+
+interface DistrictSelectFieldProps<T extends FieldValues = FieldValues> {
+  name: Path<T>;
+  cityName: Path<T>;
+  label?: string;
+  // required?: boolean; // 区县字段通常不是必填的
+  disabled?: boolean;
+  placeholder?: string;
+  className?: string;
+  'data-testid'?: string;
+}
+
+export const DistrictSelectField = <T extends FieldValues = FieldValues>({
+  name,
+  cityName,
+  label = '区县',
+  // required = false,
+  disabled = false,
+  placeholder = '选择区县',
+  className,
+  'data-testid': testId = 'district-select-field',
+}: DistrictSelectFieldProps<T>) => {
+  const { control } = useFormContext<T>();
+  const cityValue = useWatch({ control, name: cityName });
+  const cityId = cityValue ? Number(cityValue) : undefined;
+
+  const { data: districts, isLoading } = useDistricts(cityId);
+
+  return (
+    <Controller
+      name={name}
+      control={control}
+      render={({ field, fieldState }) => {
+        const isDisabled = disabled || isLoading || !cityValue;
+
+        return (
+          <FormItem className={className}>
+            {label && <FormLabel>{label}</FormLabel>}
+            <Select
+              value={field.value || ''}
+              onValueChange={(value) => {
+                field.onChange(value && value !== 'none' ? value : '');
+              }}
+              disabled={isDisabled}
+            >
+              <FormControl>
+                <SelectTrigger data-testid={testId}>
+                  <SelectValue placeholder={isLoading ? '加载中...' : isDisabled ? '请先选择城市' : placeholder} />
+                </SelectTrigger>
+              </FormControl>
+              <SelectContent>
+                <SelectItem value="none">请选区县</SelectItem>
+                {districts?.map((district) => (
+                  <SelectItem key={district.id} value={district.id.toString()}>
+                    {district.name}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+            {fieldState.error && (
+              <FormMessage>{fieldState.error.message}</FormMessage>
+            )}
+          </FormItem>
+        );
+      }}
+    />
+  );
+};

+ 64 - 0
packages/area-management-ui/src/components/areas/form/ProvinceSelectField.tsx

@@ -0,0 +1,64 @@
+import { Controller, useFormContext, FieldValues, Path } from 'react-hook-form';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { FormControl, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
+import { useProvinces } from '../hooks/useAreas';
+
+interface ProvinceSelectFieldProps<T extends FieldValues = FieldValues> {
+  name: Path<T>;
+  label?: string;
+  required?: boolean;
+  disabled?: boolean;
+  placeholder?: string;
+  className?: string;
+  'data-testid'?: string;
+}
+
+export const ProvinceSelectField = <T extends FieldValues = FieldValues>({
+  name,
+  label = '省份',
+  required = false,
+  disabled = false,
+  placeholder = '选择省份',
+  className,
+  'data-testid': testId = 'province-select-field',
+}: ProvinceSelectFieldProps<T>) => {
+  const { control } = useFormContext<T>();
+  const { data: provinces, isLoading } = useProvinces();
+
+  return (
+    <Controller
+      name={name}
+      control={control}
+      render={({ field, fieldState }) => (
+        <FormItem className={className}>
+          {label && <FormLabel>{label}{required && <span className="text-destructive">*</span>}</FormLabel>}
+          <Select
+            value={field.value || ''}
+            onValueChange={(value) => {
+              field.onChange(value && value !== 'none' ? value : '');
+              // 注意:清空依赖字段的逻辑由父组件处理
+            }}
+            disabled={disabled || isLoading}
+          >
+            <FormControl>
+              <SelectTrigger data-testid={testId}>
+                <SelectValue placeholder={isLoading ? '加载中...' : placeholder} />
+              </SelectTrigger>
+            </FormControl>
+            <SelectContent>
+              <SelectItem value="none">请选择省份</SelectItem>
+              {provinces?.map((province) => (
+                <SelectItem key={province.id} value={province.id.toString()}>
+                  {province.name}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+          {fieldState.error && (
+            <FormMessage>{fieldState.error.message}</FormMessage>
+          )}
+        </FormItem>
+      )}
+    />
+  );
+};

+ 72 - 0
packages/area-management-ui/src/components/areas/form/TownSelectField.tsx

@@ -0,0 +1,72 @@
+import { Controller, useFormContext, FieldValues, Path, useWatch } from 'react-hook-form';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { FormControl, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
+import { useTowns } from '../hooks/useAreas';
+
+interface TownSelectFieldProps<T extends FieldValues = FieldValues> {
+  name: Path<T>;
+  districtName: Path<T>;
+  label?: string;
+  // required?: boolean; // 街道字段通常不是必填的
+  disabled?: boolean;
+  placeholder?: string;
+  className?: string;
+  'data-testid'?: string;
+}
+
+export const TownSelectField = <T extends FieldValues = FieldValues>({
+  name,
+  districtName,
+  label = '街道',
+  // required = false,
+  disabled = false,
+  placeholder = '选择街道',
+  className,
+  'data-testid': testId = 'town-select-field',
+}: TownSelectFieldProps<T>) => {
+  const { control } = useFormContext<T>();
+  const districtValue = useWatch({ control, name: districtName });
+  const districtId = districtValue ? Number(districtValue) : undefined;
+
+  const { data: towns, isLoading } = useTowns(districtId);
+
+  return (
+    <Controller
+      name={name}
+      control={control}
+      render={({ field, fieldState }) => {
+        const isDisabled = disabled || isLoading || !districtValue;
+
+        return (
+          <FormItem className={className}>
+            {label && <FormLabel>{label}</FormLabel>}
+            <Select
+              value={field.value || ''}
+              onValueChange={(value) => {
+                field.onChange(value && value !== 'none' ? value : '');
+              }}
+              disabled={isDisabled}
+            >
+              <FormControl>
+                <SelectTrigger data-testid={testId}>
+                  <SelectValue placeholder={isLoading ? '加载中...' : isDisabled ? '请先选择区县' : placeholder} />
+                </SelectTrigger>
+              </FormControl>
+              <SelectContent>
+                <SelectItem value="none">请选择街道</SelectItem>
+                {towns?.map((town) => (
+                  <SelectItem key={town.id} value={town.id.toString()}>
+                    {town.name}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+            {fieldState.error && (
+              <FormMessage>{fieldState.error.message}</FormMessage>
+            )}
+          </FormItem>
+        );
+      }}
+    />
+  );
+};

+ 144 - 0
packages/area-management-ui/src/components/areas/hooks/useAreas.ts

@@ -0,0 +1,144 @@
+import { useQuery } from '@tanstack/react-query';
+import { areaClient, areaClientManager } from '../../../api/areaClient';
+import type { InferResponseType } from 'hono/client';
+
+// 正确的类型推断
+type AreaResponse = InferResponseType<typeof areaClient.index.$get, 200>['data'][0];
+
+interface UseAreasOptions {
+  enabled?: boolean;
+}
+
+export const useProvinces = (options?: UseAreasOptions) => {
+  return useQuery({
+    queryKey: ['areas', 'provinces'],
+    queryFn: async () => {
+      const res = await areaClientManager.get().index.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({
+            level: 1,
+            isDisabled: 0
+          }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取省份列表失败');
+      const json = await res.json();
+      return json.data as AreaResponse[];
+    },
+    staleTime: 10 * 60 * 1000,
+    gcTime: 30 * 60 * 1000,
+    enabled: options?.enabled ?? true,
+  });
+};
+
+export const useCities = (provinceId?: number, options?: UseAreasOptions) => {
+  return useQuery({
+    queryKey: ['areas', 'cities', provinceId],
+    queryFn: async () => {
+      if (!provinceId) return [];
+
+      const res = await areaClientManager.get().index.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({
+            level: 2,
+            parentId: provinceId,
+            isDisabled: 0
+          }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取城市列表失败');
+      const json = await res.json();
+      return json.data as AreaResponse[];
+    },
+    staleTime: 10 * 60 * 1000,
+    gcTime: 30 * 60 * 1000,
+    enabled: !!provinceId && (options?.enabled ?? true),
+  });
+};
+
+export const useDistricts = (cityId?: number, options?: UseAreasOptions) => {
+  return useQuery({
+    queryKey: ['areas', 'districts', cityId],
+    queryFn: async () => {
+      if (!cityId) return [];
+
+      const res = await areaClientManager.get().index.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({
+            level: 3,
+            parentId: cityId,
+            isDisabled: 0
+          }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取区县列表失败');
+      const json = await res.json();
+      return json.data as AreaResponse[];
+    },
+    staleTime: 10 * 60 * 1000,
+    gcTime: 30 * 60 * 1000,
+    enabled: !!cityId && (options?.enabled ?? true),
+  });
+};
+
+export const useTowns = (districtId?: number, options?: UseAreasOptions) => {
+  return useQuery({
+    queryKey: ['areas', 'towns', districtId],
+    queryFn: async () => {
+      if (!districtId) return [];
+
+      const res = await areaClientManager.get().index.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({
+            level: 4,
+            parentId: districtId,
+            isDisabled: 0
+          }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取街道列表失败');
+      const json = await res.json();
+      return json.data as AreaResponse[];
+    },
+    staleTime: 10 * 60 * 1000,
+    gcTime: 30 * 60 * 1000,
+    enabled: !!districtId && (options?.enabled ?? true),
+  });
+};
+
+// 组合Hook,方便一次性获取所有需要的地区数据
+export const useAreas = (provinceId?: number, cityId?: number, districtId?: number) => {
+  const provincesQuery = useProvinces();
+  const citiesQuery = useCities(provinceId);
+  const districtsQuery = useDistricts(cityId);
+  const townsQuery = useTowns(districtId);
+
+  return {
+    provinces: provincesQuery.data || [],
+    cities: citiesQuery.data || [],
+    districts: districtsQuery.data || [],
+    towns: townsQuery.data || [],
+    isLoadingProvinces: provincesQuery.isLoading,
+    isLoadingCities: citiesQuery.isLoading,
+    isLoadingDistricts: districtsQuery.isLoading,
+    isLoadingTowns: townsQuery.isLoading,
+    isError: provincesQuery.isError || citiesQuery.isError || districtsQuery.isError || townsQuery.isError,
+    error: provincesQuery.error || citiesQuery.error || districtsQuery.error || townsQuery.error,
+  };
+};

+ 22 - 0
packages/area-management-ui/src/components/areas/index.ts

@@ -0,0 +1,22 @@
+// 数据提供者
+export { AreaProvider, useAreaContext } from './AreaProvider';
+
+// Hooks
+export { useProvinces, useCities, useDistricts, useTowns, useAreas } from './hooks/useAreas';
+
+// 基础组件
+export { ProvinceSelect } from './base/ProvinceSelect';
+export { CitySelect } from './base/CitySelect';
+export { DistrictSelect } from './base/DistrictSelect';
+export { TownSelect } from './base/TownSelect';
+
+// 表单字段组件
+export { ProvinceSelectField } from './form/ProvinceSelectField';
+export { CitySelectField } from './form/CitySelectField';
+export { DistrictSelectField } from './form/DistrictSelectField';
+export { TownSelectField } from './form/TownSelectField';
+
+// 组合组件(向后兼容)
+export { AreaSelect } from './composite/AreaSelect';
+export { AreaSelectForm } from './composite/AreaSelectForm';
+export { AreaSelect4Level } from './composite/AreaSelect4Level';

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

@@ -1,6 +1,11 @@
 export { AreaManagement } from './AreaManagement';
 export { AreaForm } from './AreaForm';
 export { AreaTreeAsync } from './AreaTreeAsync';
-export { AreaSelect } from './AreaSelect';
-export { AreaSelect4Level } from './AreaSelect4Level';
-export { AreaSelectForm } from './AreaSelectForm';
+
+// 新的组合式地区选择组件
+export * from './areas';
+
+// 向后兼容的导出(使用新的组合组件)
+export { AreaSelect } from './areas/composite/AreaSelect';
+export { AreaSelectForm } from './areas/composite/AreaSelectForm';
+export { AreaSelect4Level } from './areas/composite/AreaSelect4Level';

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

@@ -4,7 +4,7 @@ import { useForm, FormProvider } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { z } from 'zod';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { AreaSelectForm } from '../../src/components/AreaSelectForm';
+import { AreaSelectForm } from '../../src/components/areas/composite/AreaSelectForm';
 import { Button } from '@d8d/shared-ui-components/components/ui/button';
 import { Form } from '@d8d/shared-ui-components/components/ui/form';
 import userEvent from '@testing-library/user-event';

+ 2 - 2
packages/area-management-ui/tests/integration/area-select.integration.test.tsx

@@ -2,9 +2,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { render, screen, fireEvent, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { AreaSelect } from '../../src/components/AreaSelect';
+import { AreaSelect } from '../../src/components/areas/composite/AreaSelect';
 
-// Mock API 调用
+// Mock API 调用 - 注意:新的组件使用相对路径导入api
 vi.mock('../../src/api/areaClient', () => ({
   areaClientManager: {
     get: vi.fn(() => ({