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