|
|
@@ -0,0 +1,350 @@
|
|
|
+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>
|
|
|
+ );
|
|
|
+};
|