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