Browse Source

✨ feat(admin): 添加区域管理菜单项并清理相关组件

- 在管理员菜单中添加区域管理入口,支持权限控制
- 删除已废弃的区域管理相关组件【AreaForm、AreaSelect、AreaSelect4Level、AreasTreePage】
- 更新PRD文档进度标记,故事4标记为已完成
yourname 1 month ago
parent
commit
7ada9850f4

+ 2 - 2
docs/prd/epic-008-server-web-multi-tenant-integration.md

@@ -102,7 +102,7 @@ packages/
 
 3. **[x] Story 3:** 租户模块集成到server - 将租户模块包(@d8d/tenant-module-mt)集成到server中,包括租户管理路由、超级管理员认证和租户数据隔离功能,确保server能够支持租户管理操作
 
-4. **Story 4:** 租户UI包集成到Web - 复制`web/src/client/admin`目录为`web/src/client/tenant`,在tenant目录中集成`@d8d/tenant-management-ui-mt`租户管理UI包,使用超级管理员认证系统(superadmin/admin123),添加租户管理路由和超级管理员认证逻辑,确保Web应用能够支持租户管理操作
+4. **[x] Story 4:** 租户UI包集成到Web - 复制`web/src/client/admin`目录为`web/src/client/tenant`,在tenant目录中集成`@d8d/tenant-management-ui-mt`租户管理UI包,使用超级管理员认证系统(superadmin/admin123),添加租户管理路由和超级管理员认证逻辑,确保Web应用能够支持租户管理操作
 
 ### 阶段 3: 系统集成和验证
 
@@ -136,7 +136,7 @@ packages/
 
 ## Definition of Done
 
-- [ ] 所有故事完成且验收标准满足 (3/5 故事已完成)
+- [ ] 所有故事完成且验收标准满足 (4/5 故事已完成)
 - [ ] server支持单租户/多租户模式动态切换
 - [ ] 租户数据隔离验证通过
 - [ ] 租户管理界面功能完整

+ 0 - 212
web/src/client/admin/components/AreaForm.tsx

@@ -1,212 +0,0 @@
-import React from 'react';
-import { useForm } from 'react-hook-form';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { Button } from '@/client/components/ui/button';
-import { Input } from '@/client/components/ui/input';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
-import { createAreaSchema, updateAreaSchema, AreaLevel } from '@d8d/geo-areas/schemas';
-import type { CreateAreaInput, UpdateAreaInput } from '@d8d/geo-areas/schemas';
-import { DisabledStatus } from '@/share/types';
-
-interface AreaFormProps {
-  area?: UpdateAreaInput & { id?: number };
-  onSubmit: (data: CreateAreaInput | UpdateAreaInput) => Promise<void>;
-  onCancel: () => void;
-  isLoading?: boolean;
-  /** 智能预填的层级 */
-  smartLevel?: number;
-  /** 智能预填的父级ID */
-  smartParentId?: number;
-}
-
-// 辅助函数:根据层级值获取显示名称
-const getLevelDisplayName = (level: number | undefined): string => {
-  switch (level) {
-    case AreaLevel.PROVINCE:
-      return '省/直辖市';
-    case AreaLevel.CITY:
-      return '市';
-    case AreaLevel.DISTRICT:
-      return '区/县';
-    default:
-      return '未知层级';
-  }
-};
-
-export const AreaForm: React.FC<AreaFormProps> = ({
-  area,
-  onSubmit,
-  onCancel,
-  isLoading = false,
-  smartLevel,
-  smartParentId
-}) => {
-  const isEditing = !!area;
-
-  const form = useForm<CreateAreaInput | UpdateAreaInput>({
-    resolver: zodResolver(isEditing ? updateAreaSchema : createAreaSchema),
-    defaultValues: area ? {
-      parentId: area.parentId || undefined,
-      name: area.name,
-      level: area.level,
-      code: area.code,
-      isDisabled: area.isDisabled,
-    } : {
-      parentId: smartParentId || undefined,
-      name: '',
-      level: smartLevel ?? AreaLevel.PROVINCE,
-      code: '',
-      isDisabled: DisabledStatus.ENABLED,
-    },
-  });
-
-  const handleSubmit = async (data: CreateAreaInput | UpdateAreaInput) => {
-    await onSubmit(data);
-  };
-
-
-  return (
-    <Form {...form}>
-      <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
-        <div className="grid grid-cols-1 gap-6">
-          {/* 层级显示(只读) */}
-          <FormField
-            control={form.control}
-            name="level"
-            render={({ field }) => (
-              <FormItem>
-                <FormLabel>层级</FormLabel>
-                <FormControl>
-                  <Input
-                    value={getLevelDisplayName(field.value)}
-                    disabled
-                    className="bg-muted"
-                  />
-                </FormControl>
-                <FormDescription>
-                  根据操作上下文自动设置的层级
-                </FormDescription>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
-
-          {/* 父级区域显示(只读) */}
-          <FormField
-            control={form.control}
-            name="parentId"
-            render={({ field }) => (
-              <FormItem>
-                <FormLabel>父级区域</FormLabel>
-                <FormControl>
-                  <Input
-                    type="number"
-                    value={field.value || ''}
-                    disabled
-                    className="bg-muted"
-                    placeholder="顶级区域(无父级)"
-                  />
-                </FormControl>
-                <FormDescription>
-                  根据操作上下文自动设置的父级区域ID
-                </FormDescription>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
-
-          {/* 区域名称 */}
-          <FormField
-            control={form.control}
-            name="name"
-            render={({ field }) => (
-              <FormItem>
-                <FormLabel>区域名称</FormLabel>
-                <FormControl>
-                  <Input
-                    placeholder="输入区域名称"
-                    {...field}
-                  />
-                </FormControl>
-                <FormDescription>
-                  输入省市区名称,如:北京市、上海市、朝阳区等
-                </FormDescription>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
-
-          {/* 行政区划代码 */}
-          <FormField
-            control={form.control}
-            name="code"
-            render={({ field }) => (
-              <FormItem>
-                <FormLabel>行政区划代码</FormLabel>
-                <FormControl>
-                  <Input
-                    placeholder="输入行政区划代码"
-                    {...field}
-                  />
-                </FormControl>
-                <FormDescription>
-                  输入标准的行政区划代码
-                </FormDescription>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
-
-          {/* 状态选择 */}
-          <FormField
-            control={form.control}
-            name="isDisabled"
-            render={({ field }) => (
-              <FormItem>
-                <FormLabel>状态</FormLabel>
-                <Select onValueChange={(value) => field.onChange(Number(value))} defaultValue={field.value?.toString()}>
-                  <FormControl>
-                    <SelectTrigger>
-                      <SelectValue placeholder="选择状态" />
-                    </SelectTrigger>
-                  </FormControl>
-                  <SelectContent>
-                    <SelectItem value={DisabledStatus.ENABLED.toString()}>
-                      启用
-                    </SelectItem>
-                    <SelectItem value={DisabledStatus.DISABLED.toString()}>
-                      禁用
-                    </SelectItem>
-                  </SelectContent>
-                </Select>
-                <FormDescription>
-                  选择省市区状态
-                </FormDescription>
-                <FormMessage />
-              </FormItem>
-            )}
-          />
-        </div>
-
-        {/* 表单操作按钮 */}
-        <div className="flex justify-end gap-4">
-          <Button
-            type="button"
-            variant="outline"
-            onClick={onCancel}
-            disabled={isLoading}
-          >
-            取消
-          </Button>
-          <Button
-            type="submit"
-            disabled={isLoading}
-          >
-            {isLoading ? '提交中...' : isEditing ? '更新' : '创建'}
-          </Button>
-        </div>
-      </form>
-    </Form>
-  );
-};

+ 0 - 258
web/src/client/admin/components/AreaSelect.tsx

@@ -1,258 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
-import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
-import { areaClient } from '@/client/api';
-import type { InferResponseType } from 'hono/client';
-
-// 类型定义
-type AreaResponse = InferResponseType<typeof areaClient.$get, 200>['data'][0];
-
-interface AreaSelectProps {
-  value?: {
-    provinceId?: number;
-    cityId?: number;
-    districtId?: number;
-  };
-  onChange?: (value: {
-    provinceId?: number;
-    cityId?: number;
-    districtId?: number;
-  }) => void;
-  disabled?: boolean;
-  required?: boolean;
-  className?: string;
-}
-
-export const AreaSelect: React.FC<AreaSelectProps> = ({
-  value = {},
-  onChange,
-  disabled = false,
-  required = false,
-  className
-}) => {
-  const [selectedProvince, setSelectedProvince] = useState<number | undefined>(value.provinceId);
-  const [selectedCity, setSelectedCity] = useState<number | undefined>(value.cityId);
-  const [selectedDistrict, setSelectedDistrict] = useState<number | undefined>(value.districtId);
-
-  // 查询省份列表
-  const { data: provinces, isLoading: isLoadingProvinces } = useQuery({
-    queryKey: ['areas', 'provinces'],
-    queryFn: async () => {
-      const res = await areaClient.$get({
-        query: {
-          page: 1,
-          pageSize: 100,
-          filters: JSON.stringify({
-            level: 1,
-            isDisabled: 0
-          }),
-          sortBy: 'id',
-          sortOrder: 'ASC'
-        }
-      });
-      if (res.status !== 200) throw new Error('获取省份列表失败');
-      return await res.json();
-    },
-    staleTime: 10 * 60 * 1000,
-    gcTime: 30 * 60 * 1000,
-  });
-
-  // 查询城市列表
-  const { data: cities, isLoading: isLoadingCities } = useQuery({
-    queryKey: ['areas', 'cities', selectedProvince],
-    queryFn: async () => {
-      if (!selectedProvince) return { data: [] };
-      const res = await areaClient.$get({
-        query: {
-          page: 1,
-          pageSize: 100,
-          filters: JSON.stringify({
-            level: 2,
-            parentId: selectedProvince,
-            isDisabled: 0
-          }),
-          sortBy: 'id',
-          sortOrder: 'ASC'
-        }
-      });
-      if (res.status !== 200) throw new Error('获取城市列表失败');
-      return await res.json();
-    },
-    staleTime: 10 * 60 * 1000,
-    gcTime: 30 * 60 * 1000,
-    enabled: !!selectedProvince,
-  });
-
-  // 查询区县列表
-  const { data: districts, isLoading: isLoadingDistricts } = useQuery({
-    queryKey: ['areas', 'districts', selectedCity],
-    queryFn: async () => {
-      if (!selectedCity) return { data: [] };
-      const res = await areaClient.$get({
-        query: {
-          page: 1,
-          pageSize: 100,
-          filters: JSON.stringify({
-            level: 3,
-            parentId: selectedCity,
-            isDisabled: 0
-          }),
-          sortBy: 'id',
-          sortOrder: 'ASC'
-        }
-      });
-      if (res.status !== 200) throw new Error('获取区县列表失败');
-      return await res.json();
-    },
-    staleTime: 10 * 60 * 1000,
-    gcTime: 30 * 60 * 1000,
-    enabled: !!selectedCity,
-  });
-
-  // 处理省份选择
-  const handleProvinceChange = (provinceId: string) => {
-    const id = provinceId && provinceId !== 'none' ? Number(provinceId) : undefined;
-    setSelectedProvince(id);
-    setSelectedCity(undefined);
-    setSelectedDistrict(undefined);
-
-    onChange?.({
-      provinceId: id,
-      cityId: undefined,
-      districtId: undefined
-    });
-  };
-
-  // 处理城市选择
-  const handleCityChange = (cityId: string) => {
-    const id = cityId && cityId !== 'none' ? Number(cityId) : undefined;
-    setSelectedCity(id);
-    setSelectedDistrict(undefined);
-
-    onChange?.({
-      provinceId: selectedProvince,
-      cityId: id,
-      districtId: undefined
-    });
-  };
-
-  // 处理区县选择
-  const handleDistrictChange = (districtId: string) => {
-    const id = districtId && districtId !== 'none' ? Number(districtId) : undefined;
-    setSelectedDistrict(id);
-
-    onChange?.({
-      provinceId: selectedProvince,
-      cityId: selectedCity,
-      districtId: id
-    });
-  };
-
-  // 同步外部值变化
-  useEffect(() => {
-    setSelectedProvince(value.provinceId);
-    setSelectedCity(value.cityId);
-    setSelectedDistrict(value.districtId);
-  }, [value.provinceId, value.cityId, value.districtId]);
-
-  return (
-    <div className={`grid grid-cols-1 md:grid-cols-3 gap-4 ${className}`}>
-      {/* 省份选择 */}
-      <div>
-        <FormItem>
-          <FormLabel>
-            省份{required && <span className="text-destructive">*</span>}
-          </FormLabel>
-          <Select
-            value={selectedProvince?.toString() || ''}
-            onValueChange={handleProvinceChange}
-            disabled={disabled || isLoadingProvinces}
-          >
-            <FormControl>
-              <SelectTrigger>
-                <SelectValue placeholder="选择省份" />
-              </SelectTrigger>
-            </FormControl>
-            <SelectContent>
-              <SelectItem value="none">请选择省份</SelectItem>
-              {provinces?.data.map((province: AreaResponse) => (
-                <SelectItem key={province.id} value={province.id.toString()}>
-                  {province.name}
-                </SelectItem>
-              ))}
-            </SelectContent>
-          </Select>
-          <FormDescription>
-            选择所在省份
-          </FormDescription>
-          <FormMessage />
-        </FormItem>
-      </div>
-
-      {/* 城市选择 */}
-      <div>
-        <FormItem>
-          <FormLabel>
-            城市{required && selectedProvince && <span className="text-destructive">*</span>}
-          </FormLabel>
-          <Select
-            value={selectedCity?.toString() || ''}
-            onValueChange={handleCityChange}
-            disabled={disabled || !selectedProvince || isLoadingCities}
-          >
-            <FormControl>
-              <SelectTrigger>
-                <SelectValue placeholder="选择城市" />
-              </SelectTrigger>
-            </FormControl>
-            <SelectContent>
-              <SelectItem value="none">请选择城市</SelectItem>
-              {cities?.data.map((city: AreaResponse) => (
-                <SelectItem key={city.id} value={city.id.toString()}>
-                  {city.name}
-                </SelectItem>
-              ))}
-            </SelectContent>
-          </Select>
-          <FormDescription>
-            选择所在城市
-          </FormDescription>
-          <FormMessage />
-        </FormItem>
-      </div>
-
-      {/* 区县选择 */}
-      <div>
-        <FormItem>
-          <FormLabel>
-            区县{required && selectedCity && <span className="text-destructive">*</span>}
-          </FormLabel>
-          <Select
-            value={selectedDistrict?.toString() || ''}
-            onValueChange={handleDistrictChange}
-            disabled={disabled || !selectedCity || isLoadingDistricts}
-          >
-            <FormControl>
-              <SelectTrigger>
-                <SelectValue placeholder="选择区县" />
-              </SelectTrigger>
-            </FormControl>
-            <SelectContent>
-              <SelectItem value="none">请选区县</SelectItem>
-              {districts?.data.map((district: AreaResponse) => (
-                <SelectItem key={district.id} value={district.id.toString()}>
-                  {district.name}
-                </SelectItem>
-              ))}
-            </SelectContent>
-          </Select>
-          <FormDescription>
-            选择所在区县
-          </FormDescription>
-          <FormMessage />
-        </FormItem>
-      </div>
-    </div>
-  );
-};

+ 0 - 350
web/src/client/admin/components/AreaSelect4Level.tsx

@@ -1,350 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
-import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
-import { areaClient } from '@/client/api';
-import type { InferResponseType } from 'hono/client';
-
-// 类型定义
-type AreaResponse = InferResponseType<typeof areaClient.$get, 200>['data'][0];
-
-interface AreaSelect4LevelProps {
-  provinceValue?: number;
-  cityValue?: number;
-  districtValue?: number;
-  townValue?: number;
-  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<number>(provinceValue);
-  const [selectedCity, setSelectedCity] = useState<number>(cityValue);
-  const [selectedDistrict, setSelectedDistrict] = useState<number>(districtValue);
-  const [selectedTown, setSelectedTown] = useState<number>(townValue);
-
-  // 查询省份列表
-  const { data: provinces, isLoading: isLoadingProvinces } = useQuery({
-    queryKey: ['areas', 'provinces'],
-    queryFn: async () => {
-      const res = await areaClient.$get({
-        query: {
-          page: 1,
-          pageSize: 100,
-          filters: JSON.stringify({
-            level: 1,
-            isDisabled: 0
-          }),
-          sortBy: 'id',
-          sortOrder: 'ASC'
-        }
-      });
-      if (res.status !== 200) throw new Error('获取省份列表失败');
-      return await res.json();
-    },
-    staleTime: 10 * 60 * 1000,
-    gcTime: 30 * 60 * 1000,
-  });
-
-  // 查询城市列表
-  const { data: cities, isLoading: isLoadingCities } = useQuery({
-    queryKey: ['areas', 'cities', selectedProvince],
-    queryFn: async () => {
-      if (!selectedProvince) return { data: [] };
-      const res = await areaClient.$get({
-        query: {
-          page: 1,
-          pageSize: 100,
-          filters: JSON.stringify({
-            level: 2,
-            parentId: selectedProvince,
-            isDisabled: 0
-          }),
-          sortBy: 'id',
-          sortOrder: 'ASC'
-        }
-      });
-      if (res.status !== 200) throw new Error('获取城市列表失败');
-      return await res.json();
-    },
-    staleTime: 10 * 60 * 1000,
-    gcTime: 30 * 60 * 1000,
-    enabled: !!selectedProvince,
-  });
-
-  // 查询区县列表
-  const { data: districts, isLoading: isLoadingDistricts } = useQuery({
-    queryKey: ['areas', 'districts', selectedCity],
-    queryFn: async () => {
-      if (!selectedCity) return { data: [] };
-      const res = await areaClient.$get({
-        query: {
-          page: 1,
-          pageSize: 100,
-          filters: JSON.stringify({
-            level: 3,
-            parentId: selectedCity,
-            isDisabled: 0
-          }),
-          sortBy: 'id',
-          sortOrder: 'ASC'
-        }
-      });
-      if (res.status !== 200) throw new Error('获取区县列表失败');
-      return await res.json();
-    },
-    staleTime: 10 * 60 * 1000,
-    gcTime: 30 * 60 * 1000,
-    enabled: !!selectedCity,
-  });
-
-  // 查询乡镇列表
-  const { data: towns, isLoading: isLoadingTowns } = useQuery({
-    queryKey: ['areas', 'towns', selectedDistrict],
-    queryFn: async () => {
-      if (!selectedDistrict) return { data: [] };
-      const res = await areaClient.$get({
-        query: {
-          page: 1,
-          pageSize: 100,
-          filters: JSON.stringify({
-            level: 4,
-            parentId: selectedDistrict,
-            isDisabled: 0
-          }),
-          sortBy: 'id',
-          sortOrder: 'ASC'
-        }
-      });
-      if (res.status !== 200) throw new Error('获取乡镇列表失败');
-      return await res.json();
-    },
-    staleTime: 10 * 60 * 1000,
-    gcTime: 30 * 60 * 1000,
-    enabled: !!selectedDistrict,
-  });
-
-  // 处理省份选择
-  const handleProvinceChange = (provinceId: string) => {
-    const id = provinceId && provinceId !== 'none' ? Number(provinceId) : 0;
-    setSelectedProvince(id);
-    setSelectedCity(0);
-    setSelectedDistrict(0);
-    setSelectedTown(0);
-    onProvinceChange?.(id);
-    onCityChange?.(0);
-    onDistrictChange?.(0);
-    onTownChange?.(0);
-  };
-
-  // 处理城市选择
-  const handleCityChange = (cityId: string) => {
-    const id = cityId && cityId !== 'none' ? Number(cityId) : 0;
-    setSelectedCity(id);
-    setSelectedDistrict(0);
-    setSelectedTown(0);
-    onCityChange?.(id);
-    onDistrictChange?.(0);
-    onTownChange?.(0);
-  };
-
-  // 处理区县选择
-  const handleDistrictChange = (districtId: string) => {
-    const id = districtId && districtId !== 'none' ? Number(districtId) : 0;
-    setSelectedDistrict(id);
-    setSelectedTown(0);
-    onDistrictChange?.(id);
-    onTownChange?.(0);
-  };
-
-  // 处理乡镇选择
-  const handleTownChange = (townId: string) => {
-    const id = townId && townId !== 'none' ? Number(townId) : 0;
-    setSelectedTown(id);
-    onTownChange?.(id);
-  };
-
-  // 同步外部值变化
-  useEffect(() => {
-    setSelectedProvince(provinceValue);
-  }, [provinceValue]);
-
-  useEffect(() => {
-    setSelectedCity(cityValue);
-  }, [cityValue]);
-
-  useEffect(() => {
-    setSelectedDistrict(districtValue);
-  }, [districtValue]);
-
-  useEffect(() => {
-    setSelectedTown(townValue);
-  }, [townValue]);
-
-  return (
-    <div className={`grid grid-cols-1 md:grid-cols-4 gap-4 ${className}`}>
-      {/* 省份选择 */}
-      <div>
-        <FormItem>
-          {showLabels && (
-            <FormLabel>
-              省份{required && <span className="text-destructive">*</span>}
-            </FormLabel>
-          )}
-          <Select
-            value={selectedProvince?.toString() || '0'}
-            onValueChange={handleProvinceChange}
-            disabled={disabled || isLoadingProvinces}
-          >
-            <FormControl>
-              <SelectTrigger>
-                <SelectValue placeholder="选择省份" />
-              </SelectTrigger>
-            </FormControl>
-            <SelectContent>
-              <SelectItem value="0">请选择省份</SelectItem>
-              {provinces?.data.map((province: AreaResponse) => (
-                <SelectItem key={province.id} value={province.id.toString()}>
-                  {province.name}
-                </SelectItem>
-              ))}
-            </SelectContent>
-          </Select>
-          {showLabels && (
-            <FormDescription>
-              选择所在省份
-            </FormDescription>
-          )}
-          <FormMessage />
-        </FormItem>
-      </div>
-
-      {/* 城市选择 */}
-      <div>
-        <FormItem>
-          {showLabels && (
-            <FormLabel>
-              城市{required && selectedProvince && <span className="text-destructive">*</span>}
-            </FormLabel>
-          )}
-          <Select
-            value={selectedCity?.toString() || '0'}
-            onValueChange={handleCityChange}
-            disabled={disabled || !selectedProvince || isLoadingCities}
-          >
-            <FormControl>
-              <SelectTrigger>
-                <SelectValue placeholder="选择城市" />
-              </SelectTrigger>
-            </FormControl>
-            <SelectContent>
-              <SelectItem value="0">请选择城市</SelectItem>
-              {cities?.data.map((city: AreaResponse) => (
-                <SelectItem key={city.id} value={city.id.toString()}>
-                  {city.name}
-                </SelectItem>
-              ))}
-            </SelectContent>
-          </Select>
-          {showLabels && (
-            <FormDescription>
-              选择所在城市
-            </FormDescription>
-          )}
-          <FormMessage />
-        </FormItem>
-      </div>
-
-      {/* 区县选择 */}
-      <div>
-        <FormItem>
-          {showLabels && (
-            <FormLabel>
-              区县{required && selectedCity && <span className="text-destructive">*</span>}
-            </FormLabel>
-          )}
-          <Select
-            value={selectedDistrict?.toString() || '0'}
-            onValueChange={handleDistrictChange}
-            disabled={disabled || !selectedCity || isLoadingDistricts}
-          >
-            <FormControl>
-              <SelectTrigger>
-                <SelectValue placeholder="选择区县" />
-              </SelectTrigger>
-            </FormControl>
-            <SelectContent>
-              <SelectItem value="0">请选区县</SelectItem>
-              {districts?.data.map((district: AreaResponse) => (
-                <SelectItem key={district.id} value={district.id.toString()}>
-                  {district.name}
-                </SelectItem>
-              ))}
-            </SelectContent>
-          </Select>
-          {showLabels && (
-            <FormDescription>
-              选择所在区县
-            </FormDescription>
-          )}
-          <FormMessage />
-        </FormItem>
-      </div>
-
-      {/* 乡镇选择 */}
-      <div>
-        <FormItem>
-          {showLabels && (
-            <FormLabel>
-              乡镇{required && selectedDistrict && <span className="text-destructive">*</span>}
-            </FormLabel>
-          )}
-          <Select
-            value={selectedTown?.toString() || '0'}
-            onValueChange={handleTownChange}
-            disabled={disabled || !selectedDistrict || isLoadingTowns}
-          >
-            <FormControl>
-              <SelectTrigger>
-                <SelectValue placeholder="选择乡镇" />
-              </SelectTrigger>
-            </FormControl>
-            <SelectContent>
-              <SelectItem value="0">请选择乡镇</SelectItem>
-              {towns?.data.map((town: AreaResponse) => (
-                <SelectItem key={town.id} value={town.id.toString()}>
-                  {town.name}
-                </SelectItem>
-              ))}
-            </SelectContent>
-          </Select>
-          {showLabels && (
-            <FormDescription>
-              选择所在乡镇
-            </FormDescription>
-          )}
-          <FormMessage />
-        </FormItem>
-      </div>
-    </div>
-  );
-};

+ 7 - 0
web/src/client/admin/menu.tsx

@@ -191,6 +191,13 @@ export const useMenu = () => {
     //   path: '/admin/agents',
     //   permission: 'agent:manage'
     // },
+    {
+      key: 'areas',
+      label: '区域管理',
+      icon: <MapPin className="h-4 w-4" />,
+      path: '/admin/areas',
+      permission: 'area:manage'
+    },
     {
       key: 'delivery-addresses',
       label: '收货地址',

+ 0 - 463
web/src/client/admin/pages/AreasTreePage.tsx

@@ -1,463 +0,0 @@
-import React from 'react';
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { Button } from '@/client/components/ui/button';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
-import { Plus } from 'lucide-react';
-import { useState } from 'react';
-import { areaClient } from '@/client/api';
-import type { InferResponseType, InferRequestType } from 'hono/client';
-import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
-import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/client/components/ui/alert-dialog';
-import { AreaForm } from '../components/AreaForm';
-import { AreaTreeAsync } from '../components/AreaTreeAsync';
-import type { CreateAreaInput, UpdateAreaInput } from '@d8d/geo-areas/schemas';
-import { toast } from 'sonner';
-
-// 类型提取规范
-type AreaResponse = InferResponseType<typeof areaClient.$get, 200>['data'][0];
-type CreateAreaRequest = InferRequestType<typeof areaClient.$post>['json'];
-type UpdateAreaRequest = InferRequestType<typeof areaClient[':id']['$put']>['json'];
-
-// 树形节点类型
-interface AreaNode {
-  id: number;
-  name: string;
-  code: string;
-  level: number;
-  parentId: number | null;
-  isDisabled: number;
-  children?: AreaNode[];
-}
-
-// 统一操作处理函数
-const handleOperation = async (operation: () => Promise<any>) => {
-  try {
-    await operation();
-    // toast.success('操作成功');
-    console.log('操作成功');
-  } catch (error) {
-    console.error('操作失败:', error);
-    // toast.error('操作失败,请重试');
-    throw error;
-  }
-};
-
-
-export const AreasTreePage: React.FC = () => {
-  const queryClient = useQueryClient();
-  const [expandedNodes, setExpandedNodes] = useState<Set<number>>(new Set());
-  const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
-  const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
-  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
-  const [isStatusDialogOpen, setIsStatusDialogOpen] = useState(false);
-  const [selectedArea, setSelectedArea] = useState<AreaResponse | null>(null);
-  const [isAddChildDialogOpen, setIsAddChildDialogOpen] = useState(false);
-  const [parentAreaForChild, setParentAreaForChild] = useState<AreaNode | null>(null);
-
-  // 查询省级数据(异步加载)
-  const { data: provinceData, isLoading: isProvinceLoading } = useQuery({
-    queryKey: ['areas-tree-province'],
-    queryFn: async () => {
-      const res = await areaClient.$get({
-        query: { 
-          page: 1, 
-          pageSize: 100 , 
-          filters: JSON.stringify({ level: 1}),
-          sortBy: 'id',
-          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,
-  });
-
-  // 创建省市区
-  const createMutation = useMutation({
-    mutationFn: async (data: CreateAreaRequest) => {
-      await handleOperation(async () => {
-        const res = await areaClient.$post({ json: data });
-        if (res.status !== 201) throw new Error('创建省市区失败');
-      });
-    },
-    onSuccess: (_, variables) => {
-      // 更新根级缓存
-      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
-
-      // 如果创建的是子节点,更新父节点的子树缓存
-      if (variables.parentId) {
-        queryClient.invalidateQueries({ queryKey: ['areas-subtree', variables.parentId] });
-      }
-
-      // 显示成功提示
-      toast.success('省市区创建成功');
-
-      // 关闭对话框
-      setIsCreateDialogOpen(false);
-
-      // 如果是创建子节点,还需要关闭子节点对话框
-      if (variables.parentId) {
-        setIsAddChildDialogOpen(false);
-        setParentAreaForChild(null);
-      }
-    },
-    onError: () => {
-      toast.error('创建失败,请重试');
-    }
-  });
-
-  // 更新省市区
-  const updateMutation = useMutation({
-    mutationFn: async ({ id, data }: { id: number; data: UpdateAreaRequest }) => {
-      await handleOperation(async () => {
-        const res = await areaClient[':id'].$put({
-          param: { id },
-          json: data
-        });
-        if (res.status !== 200) throw new Error('更新省市区失败');
-      });
-    },
-    onSuccess: () => {
-      // 更新根级缓存
-      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
-
-      // 如果更新的节点有父节点,更新父节点的子树缓存
-      if (selectedArea?.parentId) {
-        queryClient.invalidateQueries({ queryKey: ['areas-subtree', selectedArea.parentId] });
-      }
-
-      // 显示成功提示
-      toast.success('省市区更新成功');
-
-      setIsEditDialogOpen(false);
-      setSelectedArea(null);
-    },
-    onError: () => {
-      toast.error('更新失败,请重试');
-    }
-  });
-
-  // 删除省市区
-  const deleteMutation = useMutation({
-    mutationFn: async (id: number) => {
-      await handleOperation(async () => {
-        const res = await areaClient[':id'].$delete({
-          param: { id }
-        });
-        if (res.status !== 204) throw new Error('删除省市区失败');
-      });
-    },
-    onSuccess: () => {
-      // 更新根级缓存
-      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
-
-      // 如果删除的节点有父节点,更新父节点的子树缓存
-      if (selectedArea?.parentId) {
-        queryClient.invalidateQueries({ queryKey: ['areas-subtree', selectedArea.parentId] });
-      }
-
-      // 显示成功提示
-      toast.success('省市区删除成功');
-
-      setIsDeleteDialogOpen(false);
-      setSelectedArea(null);
-    },
-    onError: () => {
-      toast.error('删除失败,请重试');
-    }
-  });
-
-  // 启用/禁用省市区
-  const toggleStatusMutation = useMutation({
-    mutationFn: async ({ id, isDisabled }: { id: number; isDisabled: number }) => {
-      await handleOperation(async () => {
-        const res = await areaClient[':id'].$put({
-          param: { id },
-          json: { isDisabled }
-        });
-        if (res.status !== 200) throw new Error('更新省市区状态失败');
-      });
-    },
-    onSuccess: () => {
-      // 更新根级缓存
-      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
-
-      // 如果状态切换的节点有父节点,更新父节点的子树缓存
-      if (selectedArea?.parentId) {
-        queryClient.invalidateQueries({ queryKey: ['areas-subtree', selectedArea.parentId] });
-      }
-
-      // 显示成功提示
-      toast.success(`省市区${selectedArea?.isDisabled === 0 ? '禁用' : '启用'}成功`);
-
-      setIsStatusDialogOpen(false);
-      setSelectedArea(null);
-    },
-    onError: () => {
-      toast.error('状态切换失败,请重试');
-    }
-  });
-
-
-  // 处理创建省市区
-  const handleCreateArea = async (data: CreateAreaInput | UpdateAreaInput) => {
-    await createMutation.mutateAsync(data as CreateAreaInput);
-  };
-
-  // 处理更新省市区
-  const handleUpdateArea = async (data: UpdateAreaInput) => {
-    if (!selectedArea) return;
-    await updateMutation.mutateAsync({ id: selectedArea.id, data });
-  };
-
-  // 处理删除省市区
-  const handleDeleteArea = async () => {
-    if (!selectedArea) return;
-    await deleteMutation.mutateAsync(selectedArea.id);
-  };
-
-  // 处理启用/禁用省市区
-  const handleToggleStatus = async (isDisabled: number) => {
-    if (!selectedArea) return;
-    await toggleStatusMutation.mutateAsync({ id: selectedArea.id, isDisabled });
-  };
-
-  // 处理新增子节点
-  const handleAddChild = (area: AreaNode) => {
-    setParentAreaForChild(area);
-    setIsAddChildDialogOpen(true);
-  };
-
-  // 处理创建子节点
-  const handleCreateChildArea = async (data: CreateAreaInput | UpdateAreaInput) => {
-    await createMutation.mutateAsync(data as CreateAreaInput);
-  };
-
-  // 打开编辑对话框
-  const handleEdit = (area: AreaNode) => {
-    // 将 AreaNode 转换为 AreaResponse
-    const areaResponse: AreaResponse = {
-      ...area,
-      isDeleted: 0,
-      createdAt: new Date().toISOString(),
-      updatedAt: new Date().toISOString(),
-      createdBy: null,
-      updatedBy: null
-    };
-    setSelectedArea(areaResponse);
-    setIsEditDialogOpen(true);
-  };
-
-  // 打开删除对话框
-  const handleDelete = (area: AreaNode) => {
-    // 将 AreaNode 转换为 AreaResponse
-    const areaResponse: AreaResponse = {
-      ...area,
-      isDeleted: 0,
-      createdAt: new Date().toISOString(),
-      updatedAt: new Date().toISOString(),
-      createdBy: null,
-      updatedBy: null
-    };
-    setSelectedArea(areaResponse);
-    setIsDeleteDialogOpen(true);
-  };
-
-  // 打开状态切换对话框
-  const handleToggleStatusDialog = (area: AreaNode) => {
-    // 将 AreaNode 转换为 AreaResponse
-    const areaResponse: AreaResponse = {
-      ...area,
-      isDeleted: 0,
-      createdAt: new Date().toISOString(),
-      updatedAt: new Date().toISOString(),
-      createdBy: null,
-      updatedBy: null
-    };
-    setSelectedArea(areaResponse);
-    setIsStatusDialogOpen(true);
-  };
-
-  // 切换节点展开状态
-  const handleToggleNode = (nodeId: number) => {
-    setExpandedNodes(prev => {
-      const newSet = new Set(prev);
-      if (newSet.has(nodeId)) {
-        newSet.delete(nodeId);
-      } else {
-        newSet.add(nodeId);
-      }
-      return newSet;
-    });
-  };
-
-
-  return (
-    <div className="space-y-6">
-      <div className="flex items-center justify-between">
-        <div>
-          <h1 className="text-3xl font-bold tracking-tight">省市区树形管理</h1>
-          <p className="text-muted-foreground">
-            异步加载树形结构,高效管理省市区数据
-          </p>
-        </div>
-        <Button onClick={() => setIsCreateDialogOpen(true)}>
-          <Plus className="mr-2 h-4 w-4" />
-          新增省
-        </Button>
-      </div>
-
-      <Card>
-        <CardHeader>
-          <CardTitle>省市区树形结构</CardTitle>
-          <CardDescription>
-            以树形结构查看和管理省市区层级关系,默认只加载省级数据
-          </CardDescription>
-        </CardHeader>
-        <CardContent>
-          {/* 树形视图 */}
-          {isProvinceLoading ? (
-            <div className="text-center py-8">
-              加载中...
-            </div>
-          ) : !provinceData || provinceData.length === 0 ? (
-            <div className="text-center py-8">
-              暂无数据
-            </div>
-          ) : (
-            <AreaTreeAsync
-              areas={provinceData}
-              expandedNodes={expandedNodes}
-              onToggleNode={handleToggleNode}
-              onEdit={handleEdit}
-              onDelete={handleDelete}
-              onToggleStatus={handleToggleStatusDialog}
-              onAddChild={handleAddChild}
-            />
-          )}
-        </CardContent>
-      </Card>
-
-      {/* 创建省市区对话框 */}
-      <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
-        <DialogContent className="max-w-2xl">
-          <DialogHeader>
-            <DialogTitle>新增省</DialogTitle>
-            <DialogDescription>
-              填写省信息
-            </DialogDescription>
-          </DialogHeader>
-          <AreaForm
-            onSubmit={handleCreateArea}
-            isLoading={createMutation.isPending}
-            onCancel={() => setIsCreateDialogOpen(false)}
-            smartLevel={1} // 默认设置为省级
-          />
-        </DialogContent>
-      </Dialog>
-
-      {/* 编辑省市区对话框 */}
-      <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
-        <DialogContent className="max-w-2xl">
-          <DialogHeader>
-            <DialogTitle>编辑省市区</DialogTitle>
-            <DialogDescription>
-              修改省市区信息
-            </DialogDescription>
-          </DialogHeader>
-          {selectedArea && (
-            <AreaForm
-              area={{
-                id: selectedArea.id,
-                parentId: selectedArea.parentId || 0,
-                name: selectedArea.name,
-                level: selectedArea.level,
-                code: selectedArea.code,
-                isDisabled: selectedArea.isDisabled
-              }}
-              onSubmit={handleUpdateArea}
-              isLoading={updateMutation.isPending}
-              onCancel={() => {
-                setIsEditDialogOpen(false);
-                setSelectedArea(null);
-              }}
-            />
-          )}
-        </DialogContent>
-      </Dialog>
-
-      {/* 新增子节点对话框 */}
-      <Dialog open={isAddChildDialogOpen} onOpenChange={setIsAddChildDialogOpen}>
-        <DialogContent className="max-w-2xl">
-          <DialogHeader>
-            <DialogTitle>
-              {parentAreaForChild?.level === 1 ? '新增市' : '新增区'}
-            </DialogTitle>
-            <DialogDescription>
-              {parentAreaForChild?.level === 1
-                ? `在省份 "${parentAreaForChild?.name}" 下新增市`
-                : `在城市 "${parentAreaForChild?.name}" 下新增区/县`}
-            </DialogDescription>
-          </DialogHeader>
-          <AreaForm
-            onSubmit={handleCreateChildArea}
-            isLoading={createMutation.isPending}
-            onCancel={() => {
-              setIsAddChildDialogOpen(false);
-              setParentAreaForChild(null);
-            }}
-            smartLevel={(parentAreaForChild?.level ?? 0) + 1}
-            smartParentId={parentAreaForChild?.id}
-          />
-        </DialogContent>
-      </Dialog>
-
-      {/* 删除确认对话框 */}
-      <AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
-        <AlertDialogContent>
-          <AlertDialogHeader>
-            <AlertDialogTitle>确认删除</AlertDialogTitle>
-            <AlertDialogDescription>
-              确定要删除省市区 "{selectedArea?.name}" 吗?此操作不可恢复。
-            </AlertDialogDescription>
-          </AlertDialogHeader>
-          <AlertDialogFooter>
-            <AlertDialogCancel>取消</AlertDialogCancel>
-            <AlertDialogAction
-              onClick={handleDeleteArea}
-              disabled={deleteMutation.isPending}
-            >
-              {deleteMutation.isPending ? '删除中...' : '确认删除'}
-            </AlertDialogAction>
-          </AlertDialogFooter>
-        </AlertDialogContent>
-      </AlertDialog>
-
-      {/* 状态切换确认对话框 */}
-      <AlertDialog open={isStatusDialogOpen} onOpenChange={setIsStatusDialogOpen}>
-        <AlertDialogContent>
-          <AlertDialogHeader>
-            <AlertDialogTitle>
-              {selectedArea?.isDisabled === 0 ? '禁用' : '启用'}确认
-            </AlertDialogTitle>
-            <AlertDialogDescription>
-              确定要{selectedArea?.isDisabled === 0 ? '禁用' : '启用'}省市区 "{selectedArea?.name}" 吗?
-            </AlertDialogDescription>
-          </AlertDialogHeader>
-          <AlertDialogFooter>
-            <AlertDialogCancel>取消</AlertDialogCancel>
-            <AlertDialogAction
-              onClick={() => handleToggleStatus(selectedArea?.isDisabled === 0 ? 1 : 0)}
-              disabled={toggleStatusMutation.isPending}
-            >
-              {toggleStatusMutation.isPending ? '处理中...' : '确认'}
-            </AlertDialogAction>
-          </AlertDialogFooter>
-        </AlertDialogContent>
-      </AlertDialog>
-    </div>
-  );
-};