|
@@ -0,0 +1,280 @@
|
|
|
|
|
+import { getArea } from '@/services/address';
|
|
|
|
|
+import React, { useEffect, useState } from 'react';
|
|
|
|
|
+import {
|
|
|
|
|
+ ActivityIndicator,
|
|
|
|
|
+ Dimensions,
|
|
|
|
|
+ Modal,
|
|
|
|
|
+ ScrollView,
|
|
|
|
|
+ StyleSheet,
|
|
|
|
|
+ Text,
|
|
|
|
|
+ TouchableOpacity,
|
|
|
|
|
+ View,
|
|
|
|
|
+} from 'react-native';
|
|
|
|
|
+
|
|
|
|
|
+interface AreaItem {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ pid?: number;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface RegionPickerProps {
|
|
|
|
|
+ visible: boolean;
|
|
|
|
|
+ onClose: () => void;
|
|
|
|
|
+ onSelect: (province: string, city: string, district: string) => void;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const { width, height } = Dimensions.get('window');
|
|
|
|
|
+
|
|
|
|
|
+export const RegionPicker: React.FC<RegionPickerProps> = ({
|
|
|
|
|
+ visible,
|
|
|
|
|
+ onClose,
|
|
|
|
|
+ onSelect,
|
|
|
|
|
+}) => {
|
|
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
|
|
+ const [tabs, setTabs] = useState(['请选择', '', '']);
|
|
|
|
|
+ const [activeTab, setActiveTab] = useState(0);
|
|
|
|
|
+
|
|
|
|
|
+ const [provinces, setProvinces] = useState<AreaItem[]>([]);
|
|
|
|
|
+ const [cities, setCities] = useState<AreaItem[]>([]);
|
|
|
|
|
+ const [districts, setDistricts] = useState<AreaItem[]>([]);
|
|
|
|
|
+
|
|
|
|
|
+ const [selectedProvince, setSelectedProvince] = useState<AreaItem | null>(null);
|
|
|
|
|
+ const [selectedCity, setSelectedCity] = useState<AreaItem | null>(null);
|
|
|
|
|
+ // District selection ends the flow
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (visible && provinces.length === 0) {
|
|
|
|
|
+ loadProvinces();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [visible]);
|
|
|
|
|
+
|
|
|
|
|
+ const loadProvinces = async () => {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Assuming pid=0 or 1 for top level. service says pid=1 is default
|
|
|
|
|
+ const list = await getArea(0); // Try 0 first, typically 0 is root
|
|
|
|
|
+ if (list && list.length > 0) {
|
|
|
|
|
+ setProvinces(list);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Fallback or retry with 1 if 0 returns empty/null?
|
|
|
|
|
+ // Service code: pid ? { pid } : { pid: 1 }. So if I pass 0, it passes {pid:0}.
|
|
|
|
|
+ // Let's assume typical tree structure. If fail, maybe try different PID or log error.
|
|
|
|
|
+ // Actually, let's try 0.
|
|
|
|
|
+ setProvinces(list || []);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error(e);
|
|
|
|
|
+ }
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleSelect = async (item: AreaItem) => {
|
|
|
|
|
+ if (activeTab === 0) {
|
|
|
|
|
+ // Province selected
|
|
|
|
|
+ setSelectedProvince(item);
|
|
|
|
|
+ setTabs([item.name, '请选择', '']);
|
|
|
|
|
+ setActiveTab(1);
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ const list = await getArea(Number(item.id));
|
|
|
|
|
+ setCities(list || []);
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ } else if (activeTab === 1) {
|
|
|
|
|
+ // City selected
|
|
|
|
|
+ setSelectedCity(item);
|
|
|
|
|
+ setTabs([selectedProvince!.name, item.name, '请选择']);
|
|
|
|
|
+ setActiveTab(2);
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ const list = await getArea(Number(item.id));
|
|
|
|
|
+ setDistricts(list || []);
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // District selected
|
|
|
|
|
+ onSelect(selectedProvince!.name, selectedCity!.name, item.name);
|
|
|
|
|
+ onClose();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleTabPress = (index: number) => {
|
|
|
|
|
+ // Only allow going back to previous tabs if data exists
|
|
|
|
|
+ if (index < activeTab) {
|
|
|
|
|
+ setActiveTab(index);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const renderList = () => {
|
|
|
|
|
+ let data : AreaItem[] = [];
|
|
|
|
|
+ if (activeTab === 0) data = provinces;
|
|
|
|
|
+ else if (activeTab === 1) data = cities;
|
|
|
|
|
+ else data = districts;
|
|
|
|
|
+
|
|
|
|
|
+ // Filter out invalid items if strictly needed, but let's trust API
|
|
|
|
|
+ return (
|
|
|
|
|
+ <ScrollView contentContainerStyle={styles.listContent}>
|
|
|
|
|
+ {data.map((item) => {
|
|
|
|
|
+ const isSelected =
|
|
|
|
|
+ (activeTab === 0 && item.id === selectedProvince?.id) ||
|
|
|
|
|
+ (activeTab === 1 && item.id === selectedCity?.id);
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <TouchableOpacity
|
|
|
|
|
+ key={item.id}
|
|
|
|
|
+ style={styles.item}
|
|
|
|
|
+ onPress={() => handleSelect(item)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Text style={[styles.itemText, isSelected && styles.itemTextActive]}>
|
|
|
|
|
+ {item.name}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ {isSelected && <Text style={styles.checkIcon}>✓</Text>}
|
|
|
|
|
+ </TouchableOpacity>
|
|
|
|
|
+ )})}
|
|
|
|
|
+ </ScrollView>
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Modal
|
|
|
|
|
+ visible={visible}
|
|
|
|
|
+ transparent
|
|
|
|
|
+ animationType="slide"
|
|
|
|
|
+ onRequestClose={onClose}
|
|
|
|
|
+ >
|
|
|
|
|
+ <View style={styles.mask}>
|
|
|
|
|
+ <TouchableOpacity style={styles.maskClickable} onPress={onClose} />
|
|
|
|
|
+ <View style={styles.container}>
|
|
|
|
|
+ <View style={styles.header}>
|
|
|
|
|
+ <Text style={styles.title}>配送至</Text>
|
|
|
|
|
+ <TouchableOpacity onPress={onClose} style={styles.closeBtn}>
|
|
|
|
|
+ <Text style={styles.closeText}>✕</Text>
|
|
|
|
|
+ </TouchableOpacity>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ <View style={styles.tabs}>
|
|
|
|
|
+ {tabs.map((tab, index) => (
|
|
|
|
|
+ tab ? (
|
|
|
|
|
+ <TouchableOpacity
|
|
|
|
|
+ key={index}
|
|
|
|
|
+ onPress={() => handleTabPress(index)}
|
|
|
|
|
+ style={[styles.tabItem, activeTab === index && styles.tabItemActive]}
|
|
|
|
|
+ disabled={!tab || (index > activeTab)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Text style={[styles.tabText, activeTab === index && styles.tabTextActive]}>
|
|
|
|
|
+ {tab}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ {activeTab === index && <View style={styles.tabLine} />}
|
|
|
|
|
+ </TouchableOpacity>
|
|
|
|
|
+ ) : null
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {loading ? (
|
|
|
|
|
+ <View style={styles.loadingBox}>
|
|
|
|
|
+ <ActivityIndicator size="small" color="#e79018" />
|
|
|
|
|
+ </View>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ renderList()
|
|
|
|
|
+ )}
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </Modal>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const styles = StyleSheet.create({
|
|
|
|
|
+ mask: {
|
|
|
|
|
+ flex: 1,
|
|
|
|
|
+ backgroundColor: 'rgba(0,0,0,0.5)',
|
|
|
|
|
+ justifyContent: 'flex-end',
|
|
|
|
|
+ },
|
|
|
|
|
+ maskClickable: {
|
|
|
|
|
+ flex: 1,
|
|
|
|
|
+ },
|
|
|
|
|
+ container: {
|
|
|
|
|
+ height: height * 0.7, // 70% height
|
|
|
|
|
+ backgroundColor: '#fff',
|
|
|
|
|
+ borderTopLeftRadius: 16,
|
|
|
|
|
+ borderTopRightRadius: 16,
|
|
|
|
|
+ },
|
|
|
|
|
+ header: {
|
|
|
|
|
+ flexDirection: 'row',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ justifyContent: 'center', // Title centered
|
|
|
|
|
+ height: 50,
|
|
|
|
|
+ borderBottomWidth: 1,
|
|
|
|
|
+ borderBottomColor: '#eee',
|
|
|
|
|
+ position: 'relative',
|
|
|
|
|
+ },
|
|
|
|
|
+ title: {
|
|
|
|
|
+ fontSize: 16,
|
|
|
|
|
+ fontWeight: 'bold',
|
|
|
|
|
+ color: '#333',
|
|
|
|
|
+ },
|
|
|
|
|
+ closeBtn: {
|
|
|
|
|
+ position: 'absolute',
|
|
|
|
|
+ right: 15,
|
|
|
|
|
+ top: 0,
|
|
|
|
|
+ bottom: 0,
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ paddingHorizontal: 10,
|
|
|
|
|
+ },
|
|
|
|
|
+ closeText: {
|
|
|
|
|
+ fontSize: 18,
|
|
|
|
|
+ color: '#999',
|
|
|
|
|
+ },
|
|
|
|
|
+ tabs: {
|
|
|
|
|
+ flexDirection: 'row',
|
|
|
|
|
+ borderBottomWidth: 1,
|
|
|
|
|
+ borderBottomColor: '#eee',
|
|
|
|
|
+ paddingHorizontal: 15,
|
|
|
|
|
+ },
|
|
|
|
|
+ tabItem: {
|
|
|
|
|
+ marginRight: 25,
|
|
|
|
|
+ height: 44,
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ position: 'relative',
|
|
|
|
|
+ },
|
|
|
|
|
+ tabItemActive: {},
|
|
|
|
|
+ tabText: {
|
|
|
|
|
+ fontSize: 14,
|
|
|
|
|
+ color: '#333',
|
|
|
|
|
+ },
|
|
|
|
|
+ tabTextActive: {
|
|
|
|
|
+ color: '#e79018',
|
|
|
|
|
+ fontWeight: 'bold',
|
|
|
|
|
+ },
|
|
|
|
|
+ tabLine: {
|
|
|
|
|
+ position: 'absolute',
|
|
|
|
|
+ bottom: 0,
|
|
|
|
|
+ left: '10%',
|
|
|
|
|
+ width: '80%',
|
|
|
|
|
+ height: 2,
|
|
|
|
|
+ backgroundColor: '#e79018',
|
|
|
|
|
+ },
|
|
|
|
|
+ loadingBox: {
|
|
|
|
|
+ flex: 1,
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ },
|
|
|
|
|
+ listContent: {
|
|
|
|
|
+ paddingBottom: 40,
|
|
|
|
|
+ },
|
|
|
|
|
+ item: {
|
|
|
|
|
+ flexDirection: 'row',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ justifyContent: 'space-between',
|
|
|
|
|
+ paddingVertical: 14,
|
|
|
|
|
+ paddingHorizontal: 15,
|
|
|
|
|
+ borderBottomWidth: StyleSheet.hairlineWidth,
|
|
|
|
|
+ borderBottomColor: '#f5f5f5',
|
|
|
|
|
+ },
|
|
|
|
|
+ itemText: {
|
|
|
|
|
+ fontSize: 14,
|
|
|
|
|
+ color: '#333',
|
|
|
|
|
+ },
|
|
|
|
|
+ itemTextActive: {
|
|
|
|
|
+ color: '#e79018',
|
|
|
|
|
+ },
|
|
|
|
|
+ checkIcon: {
|
|
|
|
|
+ color: '#e79018',
|
|
|
|
|
+ fontSize: 16,
|
|
|
|
|
+ },
|
|
|
|
|
+});
|