Просмотр исходного кода

✨ feat(ui): add address selector component

- implement multi-level address selector with province, city, district and town
- use react-query for data fetching and caching
- add support for controlled component with value props
- implement change handlers for each level selection
- add disabled state and label visibility props
- include automatic clearing of lower-level selections when parent changes
- add proper error handling for data fetching failures
- implement responsive UI with proper styling and icons
yourname 3 месяцев назад
Родитель
Сommit
72dc575278
1 измененных файлов с 271 добавлено и 0 удалено
  1. 271 0
      mini/src/components/ui/address-selector.tsx

+ 271 - 0
mini/src/components/ui/address-selector.tsx

@@ -0,0 +1,271 @@
+import React, { useState, useEffect } from 'react'
+import { View, Picker, Text } from '@tarojs/components'
+import { useQuery } from '@tanstack/react-query'
+import { cityClient } from '@/api'
+import { InferResponseType } from 'hono'
+
+interface City {
+  id: number
+  name: string
+  level: number
+  parentId: number
+}
+
+interface AddressSelectorProps {
+  provinceValue?: number
+  cityValue?: number
+  districtValue?: number
+  townValue?: number
+  onProvinceChange?: (value: number) => void
+  onCityChange?: (value: number) => void
+  onDistrictChange?: (value: number) => void
+  onTownChange?: (value: number) => void
+  disabled?: boolean
+  showLabels?: boolean
+}
+
+export const AddressSelector: React.FC<AddressSelectorProps> = ({
+  provinceValue,
+  cityValue,
+  districtValue,
+  townValue,
+  onProvinceChange,
+  onCityChange,
+  onDistrictChange,
+  onTownChange,
+  disabled = false,
+  showLabels = true,
+}) => {
+  // 获取省份数据 (level=1)
+  const { data: provinces } = useQuery({
+    queryKey: ['cities', 'provinces'],
+    queryFn: async () => {
+      const res = await cityClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({ level: 1 }),
+          sortOrder: 'ASC',
+          sortBy: 'id'
+        },
+      })
+      if (res.status !== 200) throw new Error('获取省份数据失败')
+      const data = await res.json()
+      return data.data as City[]
+    },
+  })
+
+  // 获取城市数据 (level=2)
+  const { data: cities } = useQuery({
+    queryKey: ['cities', 'cities', provinceValue],
+    queryFn: async () => {
+      if (!provinceValue) return []
+      const res = await cityClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({ level: 2, parentId: provinceValue }),
+          sortOrder: 'ASC',
+        },
+      })
+      if (res.status !== 200) throw new Error('获取城市数据失败')
+      const data = await res.json()
+      return data.data as City[]
+    },
+    enabled: !!provinceValue,
+  })
+
+  // 获取区县数据 (level=3)
+  const { data: districts } = useQuery({
+    queryKey: ['cities', 'districts', cityValue],
+    queryFn: async () => {
+      if (!cityValue) return []
+      const res = await cityClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({ level: 3, parentId: cityValue }),
+          sortOrder: 'ASC',
+        },
+      })
+      if (res.status !== 200) throw new Error('获取区县数据失败')
+      const data = await res.json()
+      return data.data as City[]
+    },
+    enabled: !!cityValue,
+  })
+
+  // 获取街道数据 (level=4)
+  const { data: towns } = useQuery({
+    queryKey: ['cities', 'towns', districtValue],
+    queryFn: async () => {
+      if (!districtValue) return []
+      const res = await cityClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({ level: 4, parentId: districtValue }),
+          sortOrder: 'ASC',
+        },
+      })
+      if (res.status !== 200) throw new Error('获取街道数据失败')
+      const data = await res.json()
+      return data.data as City[]
+    },
+    enabled: !!districtValue,
+  })
+
+  // 清除下级选择器
+  useEffect(() => {
+    // 当省份变化时,清除城市、区县、街道
+    if (provinceValue !== undefined) {
+      if (cityValue !== undefined) {
+        onCityChange?.(0)
+      }
+      if (districtValue !== undefined) {
+        onDistrictChange?.(0)
+      }
+      if (townValue !== undefined) {
+        onTownChange?.(0)
+      }
+    }
+  }, [provinceValue])
+
+  useEffect(() => {
+    // 当城市变化时,清除区县、街道
+    if (cityValue !== undefined) {
+      if (districtValue !== undefined) {
+        onDistrictChange?.(0)
+      }
+      if (townValue !== undefined) {
+        onTownChange?.(0)
+      }
+    }
+  }, [cityValue])
+
+  useEffect(() => {
+    // 当区县变化时,清除街道
+    if (districtValue !== undefined) {
+      if (townValue !== undefined) {
+        onTownChange?.(0)
+      }
+    }
+  }, [districtValue])
+
+  const handleProvinceChange = (e: any) => {
+    const index = e.detail.value
+    const selectedProvince = provinces?.[index]
+    if (selectedProvince) {
+      onProvinceChange?.(selectedProvince.id)
+    }
+  }
+
+  const handleCityChange = (e: any) => {
+    const index = e.detail.value
+    const selectedCity = cities?.[index]
+    if (selectedCity) {
+      onCityChange?.(selectedCity.id)
+    }
+  }
+
+  const handleDistrictChange = (e: any) => {
+    const index = e.detail.value
+    const selectedDistrict = districts?.[index]
+    if (selectedDistrict) {
+      onDistrictChange?.(selectedDistrict.id)
+    }
+  }
+
+  const handleTownChange = (e: any) => {
+    const index = e.detail.value
+    const selectedTown = towns?.[index]
+    if (selectedTown) {
+      onTownChange?.(selectedTown.id)
+    }
+  }
+
+  const getProvinceIndex = () => {
+    return provinces?.findIndex(p => p.id === provinceValue) || 0
+  }
+
+  const getCityIndex = () => {
+    return cities?.findIndex(c => c.id === cityValue) || 0
+  }
+
+  const getDistrictIndex = () => {
+    return districts?.findIndex(d => d.id === districtValue) || 0
+  }
+
+  const getTownIndex = () => {
+    return towns?.findIndex(t => t.id === townValue) || 0
+  }
+
+  return (
+    <View className="space-y-4">
+      {showLabels && (
+        <View className="text-sm font-medium text-gray-700">所在地区</View>
+      )}
+      
+      <View className="space-y-3">
+        {/* 省份选择器 */}
+        <View>
+          {showLabels && (
+            <View className="text-sm text-gray-600 mb-1">省份</View>
+          )}
+          <Picker
+            mode="selector"
+            range={provinces || []}
+            rangeKey="name"
+            value={getProvinceIndex()}
+            onChange={handleProvinceChange}
+            disabled={disabled || !provinces?.length}
+          >
+            <View className="flex items-center justify-between px-3 py-2 border border-gray-300 rounded-lg bg-white">
+              <Text className="text-gray-900">
+                {provinces?.find(p => p.id === provinceValue)?.name || '请选择省份'}
+              </Text>
+              <View className="i-heroicons-chevron-down-20-solid w-5 h-5 text-gray-400" />
+            </View>
+          </Picker>
+        </View>
+
+        {/* 城市选择器 */}
+        <View>
+          {showLabels && (
+            <View className="text-sm text-gray-600 mb-1">城市</View>
+          )}
+          <Picker
+            mode="selector"
+            range={cities || []}
+            rangeKey="name"
+            value={getCityIndex()}
+            onChange={handleCityChange}
+            disabled={disabled || !cities?.length || !provinceValue}
+          >
+            <View className="flex items-center justify-between px-3 py-2 border border-gray-300 rounded-lg bg-white">
+              <Text className="text-gray-900">
+                {cities?.find(c => c.id === cityValue)?.name || 
+                 (!provinceValue ? '请先选择省份' : '请选择城市')}
+              </Text>
+              <View className="i-heroicons-chevron-down-20-solid w-5 h-5 text-gray-400" />
+            </View>
+          </Picker>
+        </View>
+
+        {/* 区县选择器 */}
+        <View>
+          {showLabels && (
+            <View className="text-sm text-gray-600 mb-1">区县</View>
+          )}
+          <Picker
+            mode="selector"
+            range={districts || []}
+            rangeKey="name"
+            value={getDistrictIndex()}
+            onChange={handleDistrictChange}
+            disabled={disabled || !districts?.length || !cityValue}
+          >
+            <View className="flex items-center justify-between px-3 py-2 border border-gray-300 rounded-lg bg-white">
+              <Text className="text-gray-900">
+                {districts?.find(d => d.id === districtValue)?.name || 
+                 (!cityValue