CityCascadeSelector.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import React, { useState, useEffect } from 'react';
  2. import { useQuery } from '@tanstack/react-query';
  3. import {
  4. Select,
  5. SelectContent,
  6. SelectItem,
  7. SelectTrigger,
  8. SelectValue,
  9. } from '@/client/components/ui/select';
  10. import { FormItem, FormLabel, FormControl, FormMessage } from '@/client/components/ui/form';
  11. import { cityClient } from '@/client/api';
  12. interface CityCascadeSelectorProps {
  13. provinceValue?: number;
  14. cityValue?: number;
  15. districtValue?: number;
  16. townValue?: number;
  17. onProvinceChange?: (value: number) => void;
  18. onCityChange?: (value: number) => void;
  19. onDistrictChange?: (value: number) => void;
  20. onTownChange?: (value: number) => void;
  21. disabled?: boolean;
  22. showLabels?: boolean;
  23. }
  24. interface City {
  25. id: number;
  26. name: string;
  27. level: number;
  28. parentId: number;
  29. }
  30. export const CityCascadeSelector: React.FC<CityCascadeSelectorProps> = ({
  31. provinceValue,
  32. cityValue,
  33. districtValue,
  34. townValue,
  35. onProvinceChange,
  36. onCityChange,
  37. onDistrictChange,
  38. onTownChange,
  39. disabled = false,
  40. showLabels = true,
  41. }) => {
  42. // 获取省份数据 (level=1)
  43. const { data: provinces } = useQuery({
  44. queryKey: ['cities', 'provinces'],
  45. queryFn: async () => {
  46. const res = await cityClient.$get({
  47. query: {
  48. page: 1,
  49. pageSize: 100,
  50. filters: JSON.stringify({ level: 1 }),
  51. sortOrder: 'ASC',
  52. sortBy: 'id'
  53. },
  54. });
  55. if (res.status !== 200) throw new Error('获取省份数据失败');
  56. const data = await res.json();
  57. return data.data as City[];
  58. },
  59. });
  60. // 获取城市数据 (level=2)
  61. const { data: cities } = useQuery({
  62. queryKey: ['cities', 'cities', provinceValue],
  63. queryFn: async () => {
  64. if (!provinceValue) return [];
  65. const res = await cityClient.$get({
  66. query: {
  67. page: 1,
  68. pageSize: 100,
  69. filters: JSON.stringify({ level: 2, parentId: provinceValue }),
  70. sortOrder: 'ASC',
  71. },
  72. });
  73. if (res.status !== 200) throw new Error('获取城市数据失败');
  74. const data = await res.json();
  75. return data.data as City[];
  76. },
  77. enabled: !!provinceValue,
  78. });
  79. // 获取区县数据 (level=3)
  80. const { data: districts } = useQuery({
  81. queryKey: ['cities', 'districts', cityValue],
  82. queryFn: async () => {
  83. if (!cityValue) return [];
  84. const res = await cityClient.$get({
  85. query: {
  86. page: 1,
  87. pageSize: 100,
  88. filters: JSON.stringify({ level: 3, parentId: cityValue }),
  89. sortOrder: 'ASC',
  90. },
  91. });
  92. if (res.status !== 200) throw new Error('获取区县数据失败');
  93. const data = await res.json();
  94. return data.data as City[];
  95. },
  96. enabled: !!cityValue,
  97. });
  98. // 获取街道数据 (level=4)
  99. const { data: towns } = useQuery({
  100. queryKey: ['cities', 'towns', districtValue],
  101. queryFn: async () => {
  102. if (!districtValue) return [];
  103. const res = await cityClient.$get({
  104. query: {
  105. page: 1,
  106. pageSize: 100,
  107. filters: JSON.stringify({ level: 4, parentId: districtValue }),
  108. sortOrder: 'ASC',
  109. },
  110. });
  111. if (res.status !== 200) throw new Error('获取街道数据失败');
  112. const data = await res.json();
  113. return data.data as City[];
  114. },
  115. enabled: !!districtValue,
  116. });
  117. // 清除下级选择器
  118. useEffect(() => {
  119. // 当省份变化时,清除城市、区县、街道
  120. if (provinceValue !== undefined) {
  121. if (cityValue !== undefined) {
  122. onCityChange?.(0);
  123. }
  124. if (districtValue !== undefined) {
  125. onDistrictChange?.(0);
  126. }
  127. if (townValue !== undefined) {
  128. onTownChange?.(0);
  129. }
  130. }
  131. }, [provinceValue]);
  132. useEffect(() => {
  133. // 当城市变化时,清除区县、街道
  134. if (cityValue !== undefined) {
  135. if (districtValue !== undefined) {
  136. onDistrictChange?.(0);
  137. }
  138. if (townValue !== undefined) {
  139. onTownChange?.(0);
  140. }
  141. }
  142. }, [cityValue]);
  143. useEffect(() => {
  144. // 当区县变化时,清除街道
  145. if (districtValue !== undefined) {
  146. if (townValue !== undefined) {
  147. onTownChange?.(0);
  148. }
  149. }
  150. }, [districtValue]);
  151. const renderSelect = (
  152. label: string,
  153. value: number | undefined,
  154. onChange: ((value: number) => void) | undefined,
  155. options: City[] | undefined,
  156. placeholder: string
  157. ) => (
  158. <FormItem>
  159. {showLabels && <FormLabel>{label}</FormLabel>}
  160. <FormControl>
  161. <Select
  162. value={value?.toString() || ''}
  163. onValueChange={(val) => onChange?.(parseInt(val))}
  164. disabled={disabled || !options?.length}
  165. >
  166. <SelectTrigger>
  167. <SelectValue placeholder={placeholder} />
  168. </SelectTrigger>
  169. <SelectContent>
  170. {options?.map((option) => (
  171. <SelectItem key={option.id} value={option.id.toString()}>
  172. {option.name}
  173. </SelectItem>
  174. ))}
  175. </SelectContent>
  176. </Select>
  177. </FormControl>
  178. </FormItem>
  179. );
  180. return (
  181. <div className="grid grid-cols-4 gap-4">
  182. {renderSelect(
  183. '省份',
  184. provinceValue,
  185. onProvinceChange,
  186. provinces,
  187. '选择省份'
  188. )}
  189. {renderSelect(
  190. '城市',
  191. cityValue,
  192. onCityChange,
  193. cities,
  194. provinceValue ? '选择城市' : '请先选择省份'
  195. )}
  196. {renderSelect(
  197. '区县',
  198. districtValue,
  199. onDistrictChange,
  200. districts,
  201. cityValue ? '选择区县' : '请先选择城市'
  202. )}
  203. {renderSelect(
  204. '街道',
  205. townValue,
  206. onTownChange,
  207. towns,
  208. districtValue ? '选择街道' : '请先选择区县'
  209. )}
  210. </div>
  211. );
  212. };