import React, {useState, useEffect, useRef} from 'react' import {View, Text, Input, Textarea, ScrollView, Picker} from '@tarojs/components' import Taro, {useRouter} from '@tarojs/taro' import {getShopUserAddress, addShopUserAddress, updateShopUserAddress} from '@/api/shop/shopUserAddress' import type {ShopUserAddress} from '@/api/shop/shopUserAddress/model' import {parseLngLatFromText} from '@/utils/geofence' import RegionData from '@/api/json/regions-data.json' definePageConfig({ navigationBarTitleText: '新增收货地址', }) type SelectedLocation = { lng: string; lat: string; name?: string; address?: string } const isUserCancel = (e: any) => { const msg = String(e?.errMsg || e?.message || e || '') return msg.includes('cancel') } /** 检查经纬度是否有效 */ const hasValidLngLat = (addr?: Partial | null) => { if (!addr) return false const p = parseLngLatFromText(`${(addr as any)?.lng ?? ''},${(addr as any)?.lat ?? ''}`) if (!p) return false if (p.lng === 0 && p.lat === 0) return false return true } /** 确保已通过微信隐私授权(定位类接口前置要求);旧基础库无该接口时直接放行 */ const ensurePrivacyAuthorized = (): Promise => { return new Promise((resolve) => { const wxAny: any = Taro if (typeof wxAny.requirePrivacyAuthorize !== 'function') { resolve() return } if (typeof wxAny.getPrivacySetting === 'function') { wxAny.getPrivacySetting({ success: (res: any) => { if (res && res.needAuthorization) { wxAny.requirePrivacyAuthorize({success: () => resolve(), fail: () => resolve()}) } else { resolve() } }, fail: () => resolve(), }) } else { wxAny.requirePrivacyAuthorize({success: () => resolve(), fail: () => resolve()}) } }) } /** 预检并申请定位权限;已拒绝则引导去设置,返回最终是否拥有权限 */ const ensureLocationPermission = async (): Promise => { try { const setting: any = await Taro.getSetting() const auth = setting?.authSetting?.['scope.userLocation'] if (auth === true) return true if (auth === false) { const modal = await Taro.showModal({ title: '需要定位权限', content: '选择定位需要开启定位权限,请在设置中开启后重试。', confirmText: '去设置', }) if (modal.confirm) { await Taro.openSetting() const setting2: any = await Taro.getSetting() return setting2?.authSetting?.['scope.userLocation'] === true } return false } try { await Taro.authorize({scope: 'scope.userLocation'}) return true } catch { const modal = await Taro.showModal({ title: '需要定位权限', content: '选择定位需要开启定位权限,请在设置中开启后重试。', confirmText: '去设置', }) if (modal.confirm) { await Taro.openSetting() const setting2: any = await Taro.getSetting() return setting2?.authSetting?.['scope.userLocation'] === true } return false } } catch { return false } } const AddressEditPage: React.FC = () => { const {id, fromWx} = useRouter().params const isEditMode = !!id const addressId = id ? Number(id) : undefined const fromWxMode = fromWx === '1' || fromWx === 'true' const [loading, setLoading] = useState(true) const [formData, setFormData] = useState>({ name: '', phone: '', province: '', city: '', region: '', address: '', isDefault: false, }) const [regionText, setRegionText] = useState('') /** 所在地区微信原生 Picker 的 value(由 formData 派生) */ const regionValue = [formData.province, formData.city, formData.region].map(v => String(v || '')) const [inputText, setInputText] = useState('') const [selectedLocation, setSelectedLocation] = useState(null) const [regionLocked, setRegionLocked] = useState(false) const wxDraftRef = useRef | null>(null) const wxDraftPatchedRef = useRef(false) /** 解析省市区 */ const parseRegion = (text: string) => { for (const province of RegionData as any[]) { if (text.includes(province.label)) { const result: any = {province: province.label} if (province.children) { for (const city of province.children) { if (text.includes(city.label)) { result.city = city.label if (city.children) { for (const region of city.children) { if (text.includes(region.label)) { result.region = region.label return result } } } return result } } } return result } } return null } /** 解析地址文本(智能识别) */ const parseAddressText = (text: string) => { const result: any = {} // 手机号正则 const phoneRegex = /1[3-9]\d{9}/ const phoneMatch = text.match(phoneRegex) if (phoneMatch) result.phone = phoneMatch[0] // 姓名正则(2-4个中文字符,通常在开头) const nameRegex = /^[\u4e00-\u9fa5]{2,4}/ const nameMatch = text.match(nameRegex) if (nameMatch) result.name = nameMatch[0] // 省市区识别 const regionResult = parseRegion(text) if (regionResult) { result.province = regionResult.province result.city = regionResult.city result.region = regionResult.region } // 详细地址提取 let addressText = text if (result.name) addressText = addressText.replace(result.name, '') if (result.phone) addressText = addressText.replace(result.phone, '') if (result.province) addressText = addressText.replace(result.province, '') if (result.city) addressText = addressText.replace(result.city, '') if (result.region) addressText = addressText.replace(result.region, '') result.address = addressText.replace(/[,,。\s]+/g, '').trim() return result } /** 地址智能识别 */ const recognizeAddress = () => { if (!inputText.trim()) { Taro.showToast({title: '请输入要识别的文本', icon: 'none'}) return } try { const result = parseAddressText(inputText) const newFormData: any = { ...formData, name: result.name || formData.name, phone: result.phone || formData.phone, address: result.address || formData.address, } if (!regionLocked) { newFormData.province = result.province || formData.province newFormData.city = result.city || formData.city newFormData.region = result.region || formData.region } setFormData(newFormData) if (!regionLocked && result.province && result.city && result.region) { setRegionText(`${result.province} ${result.city} ${result.region}`) } Taro.showToast({title: regionLocked ? '识别成功(所在地区以定位为准)' : '识别成功', icon: 'success'}) setInputText('') } catch { Taro.showToast({title: '识别失败,请检查文本格式', icon: 'none'}) } } /** 将地图选点结果应用到表单 */ const applyChosenLocation = (res: any) => { if (!res) return if (res.latitude === undefined || res.longitude === undefined) { Taro.showToast({title: '定位信息获取失败', icon: 'none'}) return } const next: SelectedLocation = { lng: String(res.longitude), lat: String(res.latitude), name: res.name, address: res.address, } // 尝试从地图返回的地址文本解析省市区 const regionResult = res?.provinceName || res?.cityName || res?.adName ? { province: String(res.provinceName || ''), city: String(res.cityName || ''), region: String(res.adName || '') } : parseRegion(String(res.address || '')) const province = String(regionResult?.province || '').trim() const city = String(regionResult?.city || '').trim() const region = String(regionResult?.region || '').trim() if (!province || !city || !region) { Taro.showToast({title: '定位未识别到所在地区,请重新选择定位', icon: 'none'}) return } setSelectedLocation(next) setRegionLocked(true) // 将地图选点的地址同步到"收货地址"(剥离省市区,避免重复) const rawAddr = String(res.address || '').trim() const name = String(res.name || '').trim() let detail = rawAddr for (const part of [province, city, region]) { if (part) detail = detail.replace(part, '') } detail = detail.replace(/[,,]+/g, ' ').replace(/\s+/g, ' ').trim() const base = detail || rawAddr const nextDetailAddress = (() => { if (!base && !name) return '' if (!base) return name if (!name) return base return base.includes(name) ? base : `${base} ${name}` })() setFormData(prev => ({ ...prev, lng: next.lng, lat: next.lat, address: nextDetailAddress || prev.address, province, city, region, })) setRegionText(`${province} ${city} ${region}`) } /** 选择定位 */ const chooseGeoLocation = async () => { try { // 1. 微信隐私授权(新版基础库强制,未同意会直接拒绝定位接口) await ensurePrivacyAuthorized() // 2. 定位权限预检 / 申请 const granted = await ensureLocationPermission() if (!granted) { Taro.showToast({title: '未获得定位权限,请在下方手动选择所在地区', icon: 'none'}) return } // 3. 仅在有有效经纬度时传入初始化参数,避免 undefined 触发部分机型失败 const initLat = selectedLocation?.lat ? Number(selectedLocation.lat) : undefined const initLng = selectedLocation?.lng ? Number(selectedLocation.lng) : undefined const latitude = typeof initLat === 'number' && Number.isFinite(initLat) ? initLat : undefined const longitude = typeof initLng === 'number' && Number.isFinite(initLng) ? initLng : undefined const params: any = {} if (typeof latitude === 'number') params.latitude = latitude if (typeof longitude === 'number') params.longitude = longitude const res = await Taro.chooseLocation(params) applyChosenLocation(res) } catch (e: any) { if (isUserCancel(e)) return // 4. 地图失败兜底:原生 Picker 无法用代码调起,提示用户去下方手动选择所在地区; // 同时提供"重试"可再次尝试地图定位 Taro.showModal({ title: '地图打开失败', content: '无法调起地图定位,您可以在下方手动选择所在地区并填写详细地址,或稍后重试。', confirmText: '重试', cancelText: '手动填写', }).then((modal) => { if (modal.confirm) { Taro.chooseLocation({}) .then((res: any) => applyChosenLocation(res)) .catch(() => { Taro.showToast({title: '仍无法打开地图,请手动选择所在地区', icon: 'none'}) }) } else { Taro.showToast({title: '请在下方手动选择所在地区', icon: 'none'}) } }).catch(() => { Taro.showToast({title: '请在下方手动选择所在地区', icon: 'none'}) }) } } /** 地区选择器确认(微信原生 Picker mode=region) */ const handleRegionChange = (e: any) => { const val = e?.detail?.value as string[] if (!val || val.length < 3) return const [province, city, region] = val setFormData(prev => ({...prev, province, city, region})) setRegionText(`${province} ${city} ${region}`) // 用户手动修改了所在地区,解除"由地图定位锁定"状态,后续可继续手动调整 setRegionLocked(false) } /** 保存地址 */ const handleSave = async () => { // 表单校验 if (!formData.name?.trim()) { Taro.showToast({title: '请输入收货人姓名', icon: 'none'}) return } if (!formData.phone?.trim()) { Taro.showToast({title: '请输入手机号', icon: 'none'}) return } if (!/^1[3-9]\d{9}$/.test(formData.phone)) { Taro.showToast({title: '手机号格式不正确', icon: 'none'}) return } if (!formData.province || !formData.city || !formData.region) { Taro.showToast({title: '请选择所在地区', icon: 'none'}) return } if (!formData.address?.trim()) { Taro.showToast({title: '请输入详细地址', icon: 'none'}) return } // 经纬度检查(可选,不阻断保存流程) const loc = selectedLocation || (hasValidLngLat(formData) ? { lng: String(formData.lng), lat: String(formData.lat) } : null) try { const submitData: any = { name: formData.name, phone: formData.phone, country: formData.country || '中国', province: formData.province, city: formData.city, region: formData.region, address: formData.address, isDefault: formData.isDefault ?? true, ...(loc ? {lng: loc.lng, lat: loc.lat} : {}), } if (isEditMode && addressId) { submitData.id = addressId } // 执行新增或更新 if (isEditMode) { await updateShopUserAddress(submitData) } else { await addShopUserAddress(submitData) } Taro.showToast({title: `${isEditMode ? '更新' : '保存'}成功`, icon: 'success'}) setTimeout(() => Taro.navigateBack(), 1000) } catch (error) { console.error('保存失败:', error) Taro.showToast({title: `${isEditMode ? '更新' : '保存'}失败`, icon: 'none'}) } } const updateField = (field: string, value: any) => { setFormData(prev => ({...prev, [field]: value})) } /** 初始化加载 */ useEffect(() => { const init = async () => { // 动态设置页面标题 Taro.setNavigationBarTitle({ title: isEditMode ? '编辑收货地址' : (fromWxMode ? '完善收货地址' : '新增收货地址'), }) // 微信地址导入 if (!isEditMode && fromWxMode && !wxDraftPatchedRef.current) { try { const draft = Taro.getStorageSync('WxAddressDraft') if (draft) { wxDraftPatchedRef.current = true wxDraftRef.current = draft as any Taro.removeStorageSync('WxAddressDraft') setFormData(prev => ({...prev, ...(draft as any)})) const p = String((draft as any)?.province || '').trim() const c = String((draft as any)?.city || '').trim() const r = String((draft as any)?.region || '').trim() const rText = [p, c, r].filter(Boolean).join(' ') if (rText) setRegionText(rText) } } catch (_e) { /* ignore */ } } // 编辑模式:加载已有地址数据 if (isEditMode && addressId) { try { const addr = await getShopUserAddress(addressId) setFormData(addr) setRegionText(`${addr.province || ''} ${addr.city || ''} ${addr.region || ''}`) if (hasValidLngLat(addr)) { setSelectedLocation({lng: String(addr.lng), lat: String(addr.lat)}) setRegionLocked(true) } } catch (error) { console.error('加载地址失败:', error) Taro.showToast({title: '加载地址失败', icon: 'none'}) } } setLoading(false) } init() }, []) if (loading) { return ( 加载中... ) } return ( {/* 地址智能识别区 */}