AreaSelectForm.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import { Controller, useFormContext, Control, FieldValues, Path } from 'react-hook-form';
  2. import { useQuery } from '@tanstack/react-query';
  3. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
  4. import { FormDescription, FormLabel } from '@d8d/shared-ui-components/components/ui/form';
  5. import { areaClient, areaClientManager } from '../api/areaClient';
  6. import type { InferResponseType } from 'hono/client';
  7. interface AreaSelectFormProps<T extends FieldValues = FieldValues> {
  8. provinceName: Path<T>;
  9. cityName: Path<T>;
  10. districtName?: Path<T>;
  11. label?: string;
  12. description?: string;
  13. required?: boolean;
  14. disabled?: boolean;
  15. className?: string;
  16. control?: Control<T>;
  17. }
  18. // 类型定义
  19. type AreaResponse = InferResponseType<typeof areaClient.index.$get, 200>['data'][0];
  20. export const AreaSelectForm = <T extends FieldValues = FieldValues>({
  21. provinceName,
  22. cityName,
  23. districtName,
  24. label = '地区选择',
  25. description,
  26. required = false,
  27. disabled = false,
  28. className,
  29. control: propControl
  30. }: AreaSelectFormProps<T>) => {
  31. // 使用传入的 control,或者从上下文中获取
  32. const formContext = useFormContext<T>();
  33. const control = propControl || formContext?.control;
  34. if (!control) {
  35. console.error('AreaSelectForm: 缺少 react-hook-form 上下文或 control prop');
  36. return null;
  37. }
  38. // 获取表单值
  39. const provinceValue = control._getWatch(provinceName);
  40. const cityValue = control._getWatch(cityName);
  41. // districtValue 变量未使用,移除
  42. // 查询省份列表
  43. const { data: provinces, isLoading: isLoadingProvinces } = useQuery({
  44. queryKey: ['areas', 'provinces'],
  45. queryFn: async () => {
  46. const res = await areaClientManager.get().index.$get({
  47. query: {
  48. page: 1,
  49. pageSize: 100,
  50. filters: JSON.stringify({
  51. level: 1,
  52. isDisabled: 0
  53. }),
  54. sortBy: 'id',
  55. sortOrder: 'ASC'
  56. }
  57. });
  58. if (res.status !== 200) throw new Error('获取省份列表失败');
  59. return await res.json();
  60. },
  61. staleTime: 10 * 60 * 1000,
  62. gcTime: 30 * 60 * 1000,
  63. });
  64. // 查询城市列表
  65. const { data: cities, isLoading: isLoadingCities } = useQuery({
  66. queryKey: ['areas', 'cities', provinceValue],
  67. queryFn: async () => {
  68. if (!provinceValue) return { data: [] };
  69. const res = await areaClientManager.get().index.$get({
  70. query: {
  71. page: 1,
  72. pageSize: 100,
  73. filters: JSON.stringify({
  74. level: 2,
  75. parentId: Number(provinceValue),
  76. isDisabled: 0
  77. }),
  78. sortBy: 'id',
  79. sortOrder: 'ASC'
  80. }
  81. });
  82. if (res.status !== 200) throw new Error('获取城市列表失败');
  83. return await res.json();
  84. },
  85. staleTime: 10 * 60 * 1000,
  86. gcTime: 30 * 60 * 1000,
  87. enabled: !!provinceValue,
  88. });
  89. // 查询区县列表(如果提供districtName)
  90. const { data: districts, isLoading: isLoadingDistricts } = useQuery({
  91. queryKey: ['areas', 'districts', cityValue],
  92. queryFn: async () => {
  93. if (!cityValue || !districtName) return { data: [] };
  94. const res = await areaClientManager.get().index.$get({
  95. query: {
  96. page: 1,
  97. pageSize: 100,
  98. filters: JSON.stringify({
  99. level: 3,
  100. parentId: Number(cityValue),
  101. isDisabled: 0
  102. }),
  103. sortBy: 'id',
  104. sortOrder: 'ASC'
  105. }
  106. });
  107. if (res.status !== 200) throw new Error('获取区县列表失败');
  108. return await res.json();
  109. },
  110. staleTime: 10 * 60 * 1000,
  111. gcTime: 30 * 60 * 1000,
  112. enabled: !!cityValue && !!districtName,
  113. });
  114. return (
  115. <div className={className}>
  116. {label && (
  117. <FormLabel>
  118. {label}{required && <span className="text-destructive">*</span>}
  119. </FormLabel>
  120. )}
  121. <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
  122. {/* 省份选择 */}
  123. <Controller
  124. name={provinceName as any}
  125. control={control}
  126. render={({ field: provinceField, fieldState: provinceFieldState }) => (
  127. <div>
  128. <Select
  129. value={provinceField.value || ''}
  130. onValueChange={(value) => {
  131. provinceField.onChange(value && value !== 'none' ? value : '');
  132. // 清空城市和区县字段
  133. control.setValue(cityName as any, '');
  134. if (districtName) {
  135. control.setValue(districtName as any, '');
  136. }
  137. }}
  138. disabled={disabled || isLoadingProvinces}
  139. >
  140. <SelectTrigger>
  141. <SelectValue placeholder="选择省份" />
  142. </SelectTrigger>
  143. <SelectContent>
  144. <SelectItem value="none">请选择省份</SelectItem>
  145. {provinces?.data.map((province: AreaResponse) => (
  146. <SelectItem key={province.id} value={province.id.toString()}>
  147. {province.name}
  148. </SelectItem>
  149. ))}
  150. </SelectContent>
  151. </Select>
  152. {provinceFieldState.error && (
  153. <div className="text-sm font-medium text-destructive mt-1">
  154. {provinceFieldState.error.message}
  155. </div>
  156. )}
  157. </div>
  158. )}
  159. />
  160. {/* 城市选择 */}
  161. <Controller
  162. name={cityName as any}
  163. control={control}
  164. render={({ field: cityField, fieldState: cityFieldState }) => (
  165. <div>
  166. <Select
  167. value={cityField.value || ''}
  168. onValueChange={(value) => {
  169. cityField.onChange(value && value !== 'none' ? value : '');
  170. // 清空区县字段
  171. if (districtName) {
  172. control.setValue(districtName as any, '');
  173. }
  174. }}
  175. disabled={disabled || !provinceValue || isLoadingCities}
  176. >
  177. <SelectTrigger>
  178. <SelectValue placeholder="选择城市" />
  179. </SelectTrigger>
  180. <SelectContent>
  181. <SelectItem value="none">请选择城市</SelectItem>
  182. {cities?.data.map((city: AreaResponse) => (
  183. <SelectItem key={city.id} value={city.id.toString()}>
  184. {city.name}
  185. </SelectItem>
  186. ))}
  187. </SelectContent>
  188. </Select>
  189. {cityFieldState.error && (
  190. <div className="text-sm font-medium text-destructive mt-1">
  191. {cityFieldState.error.message}
  192. </div>
  193. )}
  194. </div>
  195. )}
  196. />
  197. {/* 区县选择(可选) */}
  198. {districtName && (
  199. <Controller
  200. name={districtName as any}
  201. control={control}
  202. render={({ field: districtField, fieldState: districtFieldState }) => (
  203. <div>
  204. <Select
  205. value={districtField.value || ''}
  206. onValueChange={(value) => {
  207. districtField.onChange(value && value !== 'none' ? value : '');
  208. }}
  209. disabled={disabled || !cityValue || isLoadingDistricts}
  210. >
  211. <SelectTrigger>
  212. <SelectValue placeholder="选择区县" />
  213. </SelectTrigger>
  214. <SelectContent>
  215. <SelectItem value="none">请选区县</SelectItem>
  216. {districts?.data.map((district: AreaResponse) => (
  217. <SelectItem key={district.id} value={district.id.toString()}>
  218. {district.name}
  219. </SelectItem>
  220. ))}
  221. </SelectContent>
  222. </Select>
  223. {districtFieldState.error && (
  224. <div className="text-sm font-medium text-destructive mt-1">
  225. {districtFieldState.error.message}
  226. </div>
  227. )}
  228. </div>
  229. )}
  230. />
  231. )}
  232. </div>
  233. {description && (
  234. <FormDescription className="mt-2">
  235. {description}
  236. </FormDescription>
  237. )}
  238. </div>
  239. );
  240. };