Quellcode durchsuchen

省市区选择器

zbb vor 3 Monaten
Ursprung
Commit
6488a905b9
1 geänderte Dateien mit 280 neuen und 0 gelöschten Zeilen
  1. 280 0
      components/RegionPicker.tsx

+ 280 - 0
components/RegionPicker.tsx

@@ -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,
+  },
+});