import {useEffect, useState, useRef} from "react"; import {useRouter} from '@tarojs/taro' import {Button, Loading, CellGroup, Cell, Input, TextArea, Form} from '@nutui/nutui-react-taro' import {Scan, ArrowRight} from '@nutui/icons-react-taro' import Taro from '@tarojs/taro' import {View} from '@tarojs/components' import {Address} from '@nutui/nutui-react-taro' import {ShopUserAddress} from "@/api/shop/shopUserAddress/model"; import {getShopUserAddress, listShopUserAddress, updateShopUserAddress, addShopUserAddress} from "@/api/shop/shopUserAddress"; import RegionData from '@/api/json/regions-data.json'; import FixedButton from "@/components/FixedButton"; import { parseLngLatFromText } from "@/utils/geofence"; type SelectedLocation = { lng: string; lat: string; name?: string; address?: string } const isLocationDenied = (e: any) => { const msg = String(e?.errMsg || e?.message || e || '') return ( msg.includes('auth deny') || msg.includes('authorize') || msg.includes('permission') || msg.includes('denied') || msg.includes('scope.userLocation') ) } 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 // Treat "0,0" as missing in this app (typically used as placeholder by backends). if (p.lng === 0 && p.lat === 0) return false return true } const AddUserAddress = () => { const {params} = useRouter(); const [loading, setLoading] = useState(true) const [text, setText] = useState('') const [optionsDemo1, setOptionsDemo1] = useState([]) const [visible, setVisible] = useState(false) const [FormData, setFormData] = useState({}) const [inputText, setInputText] = useState('') const [selectedLocation, setSelectedLocation] = useState(null) const formRef = useRef(null) const wxDraftRef = useRef | null>(null) const wxDraftPatchedRef = useRef(false) // 判断是编辑还是新增模式 const isEditMode = !!params.id const addressId = params.id ? Number(params.id) : undefined const fromWx = params.fromWx === '1' || params.fromWx === 'true' const skipDefaultCheck = fromWx || params.skipDefaultCheck === '1' || params.skipDefaultCheck === 'true' const reload = async () => { // 整理地区数据 setRegionData() // 新增模式:若存在“默认地址”但缺少经纬度,则强制引导到编辑页完善定位 // 目的:确保业务依赖默认地址坐标(选店/配送范围)时不会因为缺失坐标而失败 if (!isEditMode && !skipDefaultCheck) { try { const defaultList = await listShopUserAddress({ isDefault: true }) const defaultAddr = defaultList?.[0] if (defaultAddr && !hasValidLngLat(defaultAddr)) { await Taro.showModal({ title: '需要完善定位', content: '默认收货地址缺少定位信息,请先进入编辑页面选择定位并保存后再继续。', confirmText: '去完善', showCancel: false }) if (defaultAddr.id) { Taro.navigateTo({ url: `/user/address/add?id=${defaultAddr.id}` }) } else { Taro.navigateTo({ url: '/user/address/index' }) } return } } catch (_e) { // ignore: 新增页不阻塞渲染 } } // 微信地址导入:先用微信返回的字段预填表单,让用户手动选择定位后再保存 if (!isEditMode && fromWx && !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 regionText = [p, c, r].filter(Boolean).join(' ') if (regionText) setText(regionText) } } catch (_e) { // ignore } } // 如果是编辑模式,加载地址数据 if (isEditMode && addressId) { try { const address = await getShopUserAddress(addressId) setFormData(address) // 设置所在地区 setText(`${address.province} ${address.city} ${address.region}`) // 回显已保存的经纬度(编辑模式) if (hasValidLngLat(address)) setSelectedLocation({ lng: String(address.lng), lat: String(address.lat) }) } catch (error) { console.error('加载地址失败:', error) Taro.showToast({ title: '加载地址失败', icon: 'error' }); } } } /** * 处理地区数据 */ function setRegionData() { // @ts-ignore setOptionsDemo1(RegionData?.map((a) => { return { value: a.label, text: a.label, children: a.children?.map((b) => { return { value: b.label, text: b.label, children: b.children?.map((c) => { return { value: c.label, text: c.label } }) } }) } })) } /** * 地址识别功能 */ const recognizeAddress = () => { if (!inputText.trim()) { Taro.showToast({ title: '请输入要识别的文本', icon: 'none' }); return; } try { const result = parseAddressText(inputText); // 更新表单数据 const newFormData = { ...FormData, name: result.name || FormData.name, phone: result.phone || FormData.phone, address: result.address || FormData.address, province: result.province || FormData.province, city: result.city || FormData.city, region: result.region || FormData.region }; setFormData(newFormData); // 更新地区显示文本 if (result.province && result.city && result.region) { setText(`${result.province} ${result.city} ${result.region}`); } // 更新表单字段值 if (formRef.current) { formRef.current.setFieldsValue(newFormData); } Taro.showToast({ title: '识别成功', icon: 'success' }); // 清空输入框 setInputText(''); } catch (error) { Taro.showToast({ title: '识别失败,请检查文本格式', icon: 'none' }); } }; /** * 解析地址文本 */ const parseAddressText = (text: string) => { const result: any = {}; // 手机号正则 (11位数字) 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 parseRegion = (text: string) => { // @ts-ignore for (const province of RegionData) { 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 chooseGeoLocation = async () => { 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 } setSelectedLocation(next) // 尝试从地图返回的 address 文本解析省市区(best-effort) 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 nextDetailAddress = (() => { const rawAddr = String(res.address || '').trim() const name = String(res.name || '').trim() const province = String(regionResult?.province || '').trim() const city = String(regionResult?.city || '').trim() const region = String(regionResult?.region || '').trim() // 选择定位返回的 address 往往包含省市区,这里尽量剥离掉,避免和表单的省市区字段重复 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 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: regionResult?.province || prev.province, city: regionResult?.city || prev.city, region: regionResult?.region || prev.region })) if (regionResult?.province && regionResult?.city && regionResult?.region) { setText(`${regionResult.province} ${regionResult.city} ${regionResult.region}`) } // 更新表单展示值(Form initialValues 不会跟随 FormData 变化) if (formRef.current) { const patch: any = {} if (nextDetailAddress) patch.address = nextDetailAddress if (regionResult?.region) patch.region = regionResult.region formRef.current.setFieldsValue(patch) } } try { 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 res = await Taro.chooseLocation({ latitude, longitude }) applyChosenLocation(res) } catch (e: any) { console.warn('选择定位失败:', e) if (isUserCancel(e)) return if (isLocationDenied(e)) { try { const modal = await Taro.showModal({ title: '需要定位权限', content: '选择定位需要开启定位权限,请在设置中开启后重试。', confirmText: '去设置' }) if (modal.confirm) { await Taro.openSetting() // 权限可能刚被开启:重试一次 const res = await Taro.chooseLocation({}) applyChosenLocation(res) } } catch (_e) { // ignore } return } try { await Taro.showToast({ title: '打开地图失败,请重试', icon: 'none' }) } catch (_e) { // ignore } } } // 提交表单 const submitSucceed = async (values: any) => { const loc = selectedLocation || (hasValidLngLat(FormData) ? { lng: String(FormData.lng), lat: String(FormData.lat) } : null) if (!loc) { Taro.showToast({ title: '请选择定位', icon: 'none' }) return } try { // 准备提交的数据 const submitData = { ...values, country: FormData.country, province: FormData.province, city: FormData.city, region: FormData.region, lng: loc.lng, lat: loc.lat, isDefault: true // 新增或编辑的地址都设为默认地址 }; // 如果是编辑模式,添加id if (isEditMode && addressId) { submitData.id = addressId; } // 先处理默认地址逻辑 const defaultAddress = await listShopUserAddress({isDefault: true}); if (defaultAddress && defaultAddress.length > 0) { // 如果当前编辑的不是默认地址,或者是新增地址,需要取消其他默认地址 if (!isEditMode || (isEditMode && defaultAddress[0].id !== addressId)) { await updateShopUserAddress({ ...defaultAddress[0], isDefault: false }); } } // 执行新增或更新操作 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: 'error' }); } } const submitFailed = (error: any) => { console.log(error, 'err...') } useEffect(() => { // 动态设置页面标题 Taro.setNavigationBarTitle({ title: isEditMode ? '编辑收货地址' : (fromWx ? '完善收货地址' : '新增收货地址') }); reload().then(() => { setLoading(false) }) }, [fromWx, isEditMode]); // NutUI Form 的 initialValues 在首次渲染后不再响应更新;微信导入时做一次 setFieldsValue 兜底回填。 useEffect(() => { if (loading) return if (isEditMode) return const draft = wxDraftRef.current if (!draft) return if (!formRef.current?.setFieldsValue) return try { formRef.current.setFieldsValue({ name: (draft as any)?.name, phone: (draft as any)?.phone, address: (draft as any)?.address, region: (draft as any)?.region }) } catch (_e) { // ignore } finally { wxDraftRef.current = null } }, [fromWx, isEditMode, loading]) if (loading) { return 加载中 } return ( <>
submitSucceed(values)} onFinishFailed={(errors) => submitFailed(errors)} >