RegionPicker.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import { getArea } from '@/services/address';
  2. import React, { useEffect, useState } from 'react';
  3. import {
  4. ActivityIndicator,
  5. Dimensions,
  6. Modal,
  7. ScrollView,
  8. StyleSheet,
  9. Text,
  10. TouchableOpacity,
  11. View,
  12. } from 'react-native';
  13. interface AreaItem {
  14. id: string;
  15. name: string;
  16. pid?: number;
  17. }
  18. interface RegionPickerProps {
  19. visible: boolean;
  20. onClose: () => void;
  21. onSelect: (province: string, city: string, district: string) => void;
  22. }
  23. const { width, height } = Dimensions.get('window');
  24. export const RegionPicker: React.FC<RegionPickerProps> = ({
  25. visible,
  26. onClose,
  27. onSelect,
  28. }) => {
  29. const [loading, setLoading] = useState(false);
  30. const [tabs, setTabs] = useState(['请选择', '', '']);
  31. const [activeTab, setActiveTab] = useState(0);
  32. const [provinces, setProvinces] = useState<AreaItem[]>([]);
  33. const [cities, setCities] = useState<AreaItem[]>([]);
  34. const [districts, setDistricts] = useState<AreaItem[]>([]);
  35. const [selectedProvince, setSelectedProvince] = useState<AreaItem | null>(null);
  36. const [selectedCity, setSelectedCity] = useState<AreaItem | null>(null);
  37. // District selection ends the flow
  38. useEffect(() => {
  39. if (visible && provinces.length === 0) {
  40. loadProvinces();
  41. }
  42. }, [visible]);
  43. const loadProvinces = async () => {
  44. setLoading(true);
  45. try {
  46. // Assuming pid=0 or 1 for top level. service says pid=1 is default
  47. const list = await getArea(0); // Try 0 first, typically 0 is root
  48. if (list && list.length > 0) {
  49. setProvinces(list);
  50. } else {
  51. // Fallback or retry with 1 if 0 returns empty/null?
  52. // Service code: pid ? { pid } : { pid: 1 }. So if I pass 0, it passes {pid:0}.
  53. // Let's assume typical tree structure. If fail, maybe try different PID or log error.
  54. // Actually, let's try 0.
  55. setProvinces(list || []);
  56. }
  57. } catch (e) {
  58. console.error(e);
  59. }
  60. setLoading(false);
  61. };
  62. const handleSelect = async (item: AreaItem) => {
  63. if (activeTab === 0) {
  64. // Province selected
  65. setSelectedProvince(item);
  66. setTabs([item.name, '请选择', '']);
  67. setActiveTab(1);
  68. setLoading(true);
  69. const list = await getArea(Number(item.id));
  70. setCities(list || []);
  71. setLoading(false);
  72. } else if (activeTab === 1) {
  73. // City selected
  74. setSelectedCity(item);
  75. setTabs([selectedProvince!.name, item.name, '请选择']);
  76. setActiveTab(2);
  77. setLoading(true);
  78. const list = await getArea(Number(item.id));
  79. setDistricts(list || []);
  80. setLoading(false);
  81. } else {
  82. // District selected
  83. onSelect(selectedProvince!.name, selectedCity!.name, item.name);
  84. onClose();
  85. }
  86. };
  87. const handleTabPress = (index: number) => {
  88. // Only allow going back to previous tabs if data exists
  89. if (index < activeTab) {
  90. setActiveTab(index);
  91. }
  92. };
  93. const renderList = () => {
  94. let data : AreaItem[] = [];
  95. if (activeTab === 0) data = provinces;
  96. else if (activeTab === 1) data = cities;
  97. else data = districts;
  98. // Filter out invalid items if strictly needed, but let's trust API
  99. return (
  100. <ScrollView contentContainerStyle={styles.listContent}>
  101. {data.map((item) => {
  102. const isSelected =
  103. (activeTab === 0 && item.id === selectedProvince?.id) ||
  104. (activeTab === 1 && item.id === selectedCity?.id);
  105. return (
  106. <TouchableOpacity
  107. key={item.id}
  108. style={styles.item}
  109. onPress={() => handleSelect(item)}
  110. >
  111. <Text style={[styles.itemText, isSelected && styles.itemTextActive]}>
  112. {item.name}
  113. </Text>
  114. {isSelected && <Text style={styles.checkIcon}>✓</Text>}
  115. </TouchableOpacity>
  116. )})}
  117. </ScrollView>
  118. );
  119. };
  120. return (
  121. <Modal
  122. visible={visible}
  123. transparent
  124. animationType="slide"
  125. onRequestClose={onClose}
  126. >
  127. <View style={styles.mask}>
  128. <TouchableOpacity style={styles.maskClickable} onPress={onClose} />
  129. <View style={styles.container}>
  130. <View style={styles.header}>
  131. <Text style={styles.title}>配送至</Text>
  132. <TouchableOpacity onPress={onClose} style={styles.closeBtn}>
  133. <Text style={styles.closeText}>✕</Text>
  134. </TouchableOpacity>
  135. </View>
  136. <View style={styles.tabs}>
  137. {tabs.map((tab, index) => (
  138. tab ? (
  139. <TouchableOpacity
  140. key={index}
  141. onPress={() => handleTabPress(index)}
  142. style={[styles.tabItem, activeTab === index && styles.tabItemActive]}
  143. disabled={!tab || (index > activeTab)}
  144. >
  145. <Text style={[styles.tabText, activeTab === index && styles.tabTextActive]}>
  146. {tab}
  147. </Text>
  148. {activeTab === index && <View style={styles.tabLine} />}
  149. </TouchableOpacity>
  150. ) : null
  151. ))}
  152. </View>
  153. {loading ? (
  154. <View style={styles.loadingBox}>
  155. <ActivityIndicator size="small" color="#e79018" />
  156. </View>
  157. ) : (
  158. renderList()
  159. )}
  160. </View>
  161. </View>
  162. </Modal>
  163. );
  164. };
  165. const styles = StyleSheet.create({
  166. mask: {
  167. flex: 1,
  168. backgroundColor: 'rgba(0,0,0,0.5)',
  169. justifyContent: 'flex-end',
  170. },
  171. maskClickable: {
  172. flex: 1,
  173. },
  174. container: {
  175. height: height * 0.7, // 70% height
  176. backgroundColor: '#fff',
  177. borderTopLeftRadius: 16,
  178. borderTopRightRadius: 16,
  179. },
  180. header: {
  181. flexDirection: 'row',
  182. alignItems: 'center',
  183. justifyContent: 'center', // Title centered
  184. height: 50,
  185. borderBottomWidth: 1,
  186. borderBottomColor: '#eee',
  187. position: 'relative',
  188. },
  189. title: {
  190. fontSize: 16,
  191. fontWeight: 'bold',
  192. color: '#333',
  193. },
  194. closeBtn: {
  195. position: 'absolute',
  196. right: 15,
  197. top: 0,
  198. bottom: 0,
  199. justifyContent: 'center',
  200. paddingHorizontal: 10,
  201. },
  202. closeText: {
  203. fontSize: 18,
  204. color: '#999',
  205. },
  206. tabs: {
  207. flexDirection: 'row',
  208. borderBottomWidth: 1,
  209. borderBottomColor: '#eee',
  210. paddingHorizontal: 15,
  211. },
  212. tabItem: {
  213. marginRight: 25,
  214. height: 44,
  215. justifyContent: 'center',
  216. position: 'relative',
  217. },
  218. tabItemActive: {},
  219. tabText: {
  220. fontSize: 14,
  221. color: '#333',
  222. },
  223. tabTextActive: {
  224. color: '#e79018',
  225. fontWeight: 'bold',
  226. },
  227. tabLine: {
  228. position: 'absolute',
  229. bottom: 0,
  230. left: '10%',
  231. width: '80%',
  232. height: 2,
  233. backgroundColor: '#e79018',
  234. },
  235. loadingBox: {
  236. flex: 1,
  237. justifyContent: 'center',
  238. alignItems: 'center',
  239. },
  240. listContent: {
  241. paddingBottom: 40,
  242. },
  243. item: {
  244. flexDirection: 'row',
  245. alignItems: 'center',
  246. justifyContent: 'space-between',
  247. paddingVertical: 14,
  248. paddingHorizontal: 15,
  249. borderBottomWidth: StyleSheet.hairlineWidth,
  250. borderBottomColor: '#f5f5f5',
  251. },
  252. itemText: {
  253. fontSize: 14,
  254. color: '#333',
  255. },
  256. itemTextActive: {
  257. color: '#e79018',
  258. },
  259. checkIcon: {
  260. color: '#e79018',
  261. fontSize: 16,
  262. },
  263. });