|
@@ -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
|