AreaSelect.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import React, { useState, useEffect } from 'react';
  2. import { useQuery } from '@tanstack/react-query';
  3. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
  4. import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
  5. import { areaClient, areaClientManager } from '../api/areaClient';
  6. import type { InferResponseType } from 'hono/client';
  7. // 类型定义
  8. type AreaResponse = InferResponseType<typeof areaClient.index.$get, 200>['data'][0];
  9. interface AreaSelectProps {
  10. value?: {
  11. provinceId?: number;
  12. cityId?: number;
  13. districtId?: number;
  14. };
  15. onChange?: (value: {
  16. provinceId?: number;
  17. cityId?: number;
  18. districtId?: number;
  19. }) => void;
  20. disabled?: boolean;
  21. required?: boolean;
  22. className?: string;
  23. 'data-testid'?: string;
  24. }
  25. export const AreaSelect: React.FC<AreaSelectProps> = ({
  26. value = {},
  27. onChange,
  28. disabled = false,
  29. required = false,
  30. className,
  31. 'data-testid': testId
  32. }) => {
  33. const [selectedProvince, setSelectedProvince] = useState<number | undefined>(value.provinceId);
  34. const [selectedCity, setSelectedCity] = useState<number | undefined>(value.cityId);
  35. const [selectedDistrict, setSelectedDistrict] = useState<number | undefined>(value.districtId);
  36. // 查询省份列表
  37. const { data: provinces, isLoading: isLoadingProvinces } = useQuery({
  38. queryKey: ['areas', 'provinces'],
  39. queryFn: async () => {
  40. const res = await areaClientManager.get().index.$get({
  41. query: {
  42. page: 1,
  43. pageSize: 100,
  44. filters: JSON.stringify({
  45. level: 1,
  46. isDisabled: 0
  47. }),
  48. sortBy: 'id',
  49. sortOrder: 'ASC'
  50. }
  51. });
  52. if (res.status !== 200) throw new Error('获取省份列表失败');
  53. return await res.json();
  54. },
  55. staleTime: 10 * 60 * 1000,
  56. gcTime: 30 * 60 * 1000,
  57. });
  58. // 查询城市列表
  59. const { data: cities, isLoading: isLoadingCities } = useQuery({
  60. queryKey: ['areas', 'cities', selectedProvince],
  61. queryFn: async () => {
  62. if (!selectedProvince) return { data: [] };
  63. const res = await areaClientManager.get().index.$get({
  64. query: {
  65. page: 1,
  66. pageSize: 100,
  67. filters: JSON.stringify({
  68. level: 2,
  69. parentId: selectedProvince,
  70. isDisabled: 0
  71. }),
  72. sortBy: 'id',
  73. sortOrder: 'ASC'
  74. }
  75. });
  76. if (res.status !== 200) throw new Error('获取城市列表失败');
  77. return await res.json();
  78. },
  79. staleTime: 10 * 60 * 1000,
  80. gcTime: 30 * 60 * 1000,
  81. enabled: !!selectedProvince,
  82. });
  83. // 查询区县列表
  84. const { data: districts, isLoading: isLoadingDistricts } = useQuery({
  85. queryKey: ['areas', 'districts', selectedCity],
  86. queryFn: async () => {
  87. if (!selectedCity) return { data: [] };
  88. const res = await areaClientManager.get().index.$get({
  89. query: {
  90. page: 1,
  91. pageSize: 100,
  92. filters: JSON.stringify({
  93. level: 3,
  94. parentId: selectedCity,
  95. isDisabled: 0
  96. }),
  97. sortBy: 'id',
  98. sortOrder: 'ASC'
  99. }
  100. });
  101. if (res.status !== 200) throw new Error('获取区县列表失败');
  102. return await res.json();
  103. },
  104. staleTime: 10 * 60 * 1000,
  105. gcTime: 30 * 60 * 1000,
  106. enabled: !!selectedCity,
  107. });
  108. // 处理省份选择
  109. const handleProvinceChange = (provinceId: string) => {
  110. const id = provinceId && provinceId !== 'none' ? Number(provinceId) : undefined;
  111. setSelectedProvince(id);
  112. setSelectedCity(undefined);
  113. setSelectedDistrict(undefined);
  114. onChange?.({
  115. provinceId: id,
  116. cityId: undefined,
  117. districtId: undefined
  118. });
  119. };
  120. // 处理城市选择
  121. const handleCityChange = (cityId: string) => {
  122. const id = cityId && cityId !== 'none' ? Number(cityId) : undefined;
  123. setSelectedCity(id);
  124. setSelectedDistrict(undefined);
  125. onChange?.({
  126. provinceId: selectedProvince,
  127. cityId: id,
  128. districtId: undefined
  129. });
  130. };
  131. // 处理区县选择
  132. const handleDistrictChange = (districtId: string) => {
  133. const id = districtId && districtId !== 'none' ? Number(districtId) : undefined;
  134. setSelectedDistrict(id);
  135. onChange?.({
  136. provinceId: selectedProvince,
  137. cityId: selectedCity,
  138. districtId: id
  139. });
  140. };
  141. // 同步外部值变化
  142. useEffect(() => {
  143. setSelectedProvince(value.provinceId);
  144. setSelectedCity(value.cityId);
  145. setSelectedDistrict(value.districtId);
  146. }, [value.provinceId, value.cityId, value.districtId]);
  147. return (
  148. <div
  149. className={`grid grid-cols-1 md:grid-cols-3 gap-4 ${className}`}
  150. data-testid={testId}
  151. >
  152. {/* 省份选择 */}
  153. <div>
  154. <FormItem>
  155. <FormLabel>
  156. 省份{required && <span className="text-destructive">*</span>}
  157. </FormLabel>
  158. <Select
  159. value={selectedProvince?.toString() || ''}
  160. onValueChange={handleProvinceChange}
  161. disabled={disabled || isLoadingProvinces}
  162. >
  163. <FormControl>
  164. <SelectTrigger>
  165. <SelectValue placeholder="选择省份" />
  166. </SelectTrigger>
  167. </FormControl>
  168. <SelectContent>
  169. <SelectItem value="none">请选择省份</SelectItem>
  170. {provinces?.data.map((province: AreaResponse) => (
  171. <SelectItem key={province.id} value={province.id.toString()}>
  172. {province.name}
  173. </SelectItem>
  174. ))}
  175. </SelectContent>
  176. </Select>
  177. <FormDescription>
  178. 选择所在省份
  179. </FormDescription>
  180. <FormMessage />
  181. </FormItem>
  182. </div>
  183. {/* 城市选择 */}
  184. <div>
  185. <FormItem>
  186. <FormLabel>
  187. 城市{required && selectedProvince && <span className="text-destructive">*</span>}
  188. </FormLabel>
  189. <Select
  190. value={selectedCity?.toString() || ''}
  191. onValueChange={handleCityChange}
  192. disabled={disabled || !selectedProvince || isLoadingCities}
  193. >
  194. <FormControl>
  195. <SelectTrigger>
  196. <SelectValue placeholder="选择城市" />
  197. </SelectTrigger>
  198. </FormControl>
  199. <SelectContent>
  200. <SelectItem value="none">请选择城市</SelectItem>
  201. {cities?.data.map((city: AreaResponse) => (
  202. <SelectItem key={city.id} value={city.id.toString()}>
  203. {city.name}
  204. </SelectItem>
  205. ))}
  206. </SelectContent>
  207. </Select>
  208. <FormDescription>
  209. 选择所在城市
  210. </FormDescription>
  211. <FormMessage />
  212. </FormItem>
  213. </div>
  214. {/* 区县选择 */}
  215. <div>
  216. <FormItem>
  217. <FormLabel>
  218. 区县
  219. </FormLabel>
  220. <Select
  221. value={selectedDistrict?.toString() || ''}
  222. onValueChange={handleDistrictChange}
  223. disabled={disabled || !selectedCity || isLoadingDistricts}
  224. >
  225. <FormControl>
  226. <SelectTrigger>
  227. <SelectValue placeholder="选择区县" />
  228. </SelectTrigger>
  229. </FormControl>
  230. <SelectContent>
  231. <SelectItem value="none">请选区县</SelectItem>
  232. {districts?.data.map((district: AreaResponse) => (
  233. <SelectItem key={district.id} value={district.id.toString()}>
  234. {district.name}
  235. </SelectItem>
  236. ))}
  237. </SelectContent>
  238. </Select>
  239. <FormDescription>
  240. 选择所在区县
  241. </FormDescription>
  242. <FormMessage />
  243. </FormItem>
  244. </div>
  245. </div>
  246. );
  247. };