From d86cdad470be276977f44b563418ec6c6f847abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Wed, 25 Feb 2026 18:06:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(address):=20=E6=B7=BB=E5=8A=A0=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=9C=B0=E5=9D=80=E5=AF=BC=E5=85=A5=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=92=8C=E4=B8=80=E9=94=AE=E5=AF=BC=E8=88=AA=E5=91=BC=E5=8F=AB?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增微信地址导入流程,支持从微信原生地址选择后跳转到编辑页面完善定位 - 添加WxAddressDraft缓存机制用于存储微信返回的地址草稿数据 - 实现一键导航功能,支持通过订单地址ID或地址信息进行地图导航 - 添加一键呼叫功能,支持直接拨打电话联系骑手或门店 - 优化地址编辑页面支持微信导入模式和默认地址检查 --- src/user/address/add.tsx | 58 +++++++++++++++++- src/user/address/wxAddress.tsx | 54 ++++++----------- src/user/ticket/index.tsx | 106 ++++++++++++++++++++++++++++++++- 3 files changed, 179 insertions(+), 39 deletions(-) diff --git a/src/user/address/add.tsx b/src/user/address/add.tsx index 5d1eb11..54b87de 100644 --- a/src/user/address/add.tsx +++ b/src/user/address/add.tsx @@ -48,10 +48,15 @@ const AddUserAddress = () => { 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 () => { // 整理地区数据 @@ -59,7 +64,7 @@ const AddUserAddress = () => { // 新增模式:若存在“默认地址”但缺少经纬度,则强制引导到编辑页完善定位 // 目的:确保业务依赖默认地址坐标(选店/配送范围)时不会因为缺失坐标而失败 - if (!isEditMode) { + if (!isEditMode && !skipDefaultCheck) { try { const defaultList = await listShopUserAddress({ isDefault: true }) const defaultAddr = defaultList?.[0] @@ -82,6 +87,31 @@ const AddUserAddress = () => { } } + // 微信地址导入:先用微信返回的字段预填表单,让用户手动选择定位后再保存 + 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 { @@ -391,6 +421,7 @@ const AddUserAddress = () => { // 准备提交的数据 const submitData = { ...values, + country: FormData.country, province: FormData.province, city: FormData.city, region: FormData.region, @@ -448,13 +479,34 @@ const AddUserAddress = () => { useEffect(() => { // 动态设置页面标题 Taro.setNavigationBarTitle({ - title: isEditMode ? '编辑收货地址' : '新增收货地址' + title: isEditMode ? '编辑收货地址' : (fromWx ? '完善收货地址' : '新增收货地址') }); reload().then(() => { setLoading(false) }) - }, [isEditMode]); + }, [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 加载中 diff --git a/src/user/address/wxAddress.tsx b/src/user/address/wxAddress.tsx index 8fb32a3..1eb0486 100644 --- a/src/user/address/wxAddress.tsx +++ b/src/user/address/wxAddress.tsx @@ -1,25 +1,17 @@ import {useEffect} from "react"; import Taro from '@tarojs/taro' -import {addShopUserAddress} from "@/api/shop/shopUserAddress"; -import { getCurrentLngLat } from "@/utils/location"; const WxAddress = () => { /** * 从微信API获取用户收货地址 - * 调用微信原生地址选择界面,获取成功后保存到服务器并刷新列表 + * 调用微信原生地址选择界面,获取成功后跳转到“新增收货地址”页面,让用户选择定位后再保存 */ const getWeChatAddress = () => { Taro.chooseAddress() .then(async res => { - const loc = await getCurrentLngLat() - if (!loc) { - // Avoid leaving the user on an empty page. - setTimeout(() => Taro.navigateBack(), 300) - return - } - - // 格式化微信返回的地址数据为后端所需格式 - const addressData = { + // 仅填充微信地址信息,不要用“当前定位”覆盖经纬度(会造成经纬度与地址不匹配)。 + // 选择后跳转到“新增/编辑收货地址”页面,让用户手动选择地图定位后再保存。 + const addressDraft = { name: res.userName, phone: res.telNumber, country: res.nationalCode || '中国', @@ -27,40 +19,32 @@ const WxAddress = () => { city: res.cityName, region: res.countyName, address: res.detailInfo, - postalCode: res.postalCode, - lng: loc.lng, - lat: loc.lat, - isDefault: false + isDefault: false, } - console.log(res, 'addrs..') - // 调用保存地址的API(假设存在该接口) - addShopUserAddress(addressData) - .then((msg) => { - console.log(msg) - Taro.showToast({ - title: `${msg}`, - icon: 'none' - }) - setTimeout(() => { - // 保存成功后返回 - Taro.navigateBack() - }, 1000) - }) - .catch(error => { - console.error('保存地址失败:', error) - Taro.showToast({title: '保存地址失败', icon: 'error'}) - }) + Taro.setStorageSync('WxAddressDraft', addressDraft) + // 用 redirectTo 替换当前页面,避免保存后 navigateBack 回到空白的 wxAddress 页面。 + await Taro.redirectTo({ url: '/user/address/add?fromWx=1&skipDefaultCheck=1' }) }) .catch(err => { console.error('获取微信地址失败:', err) + // 用户取消选择地址:直接返回上一页 + if (String(err?.errMsg || '').includes('cancel')) { + setTimeout(() => Taro.navigateBack(), 200) + return + } // 处理用户拒绝授权的情况 - if (err.errMsg.includes('auth deny')) { + if (String(err?.errMsg || '').includes('auth deny')) { Taro.showModal({ title: '授权失败', content: '请在设置中允许获取地址权限', showCancel: false }) + setTimeout(() => Taro.navigateBack(), 300) + return } + + Taro.showToast({ title: '获取微信地址失败', icon: 'none' }) + setTimeout(() => Taro.navigateBack(), 300) }) } diff --git a/src/user/ticket/index.tsx b/src/user/ticket/index.tsx index 011c034..5608210 100644 --- a/src/user/ticket/index.tsx +++ b/src/user/ticket/index.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useRef, useState } from 'react'; import Taro, { useDidShow } from '@tarojs/taro'; import { Button, @@ -18,6 +18,7 @@ import { pageGltUserTicket } from '@/api/glt/gltUserTicket'; import type { GltUserTicket } from '@/api/glt/gltUserTicket/model'; import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder'; import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model'; +import { getShopUserAddress } from '@/api/shop/shopUserAddress'; import { BaseUrl } from '@/config/app'; import dayjs from "dayjs"; @@ -46,6 +47,8 @@ const UserTicketList = () => { const [qrTicket, setQrTicket] = useState(null); const [qrImageUrl, setQrImageUrl] = useState(''); + const addressCacheRef = useRef>({}); + const getUserId = () => { const raw = Taro.getStorageSync('UserId'); const id = Number(raw); @@ -262,6 +265,81 @@ const UserTicketList = () => { return d.isValid() ? d.format('YYYY年MM月DD日') : v; }; + const parseLatLng = (latRaw?: unknown, lngRaw?: unknown) => { + const lat = typeof latRaw === 'number' ? latRaw : parseFloat(String(latRaw ?? '')); + const lng = typeof lngRaw === 'number' ? lngRaw : parseFloat(String(lngRaw ?? '')); + if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null; + if (Math.abs(lat) > 90 || Math.abs(lng) > 180) return null; + return { lat, lng }; + }; + + const handleNavigateToAddress = async (order: GltTicketOrder) => { + try { + // Prefer coordinates from backend if present (non-typed fields), otherwise fetch by addressId. + const anyOrder = order as any; + const direct = + parseLatLng(anyOrder?.addressLat ?? anyOrder?.lat, anyOrder?.addressLng ?? anyOrder?.lng) || + parseLatLng(anyOrder?.receiverLat, anyOrder?.receiverLng); + + let coords = direct; + let fullAddress: string | undefined = order.address || undefined; + + if (!coords && order.addressId) { + const cached = addressCacheRef.current[order.addressId]; + if (cached) { + coords = { lat: cached.lat, lng: cached.lng }; + fullAddress = fullAddress || cached.fullAddress; + } else if (cached === null) { + coords = null; + } else { + const addr = await getShopUserAddress(order.addressId); + const parsed = parseLatLng(addr?.lat, addr?.lng); + if (parsed) { + coords = parsed; + fullAddress = fullAddress || addr?.fullAddress || addr?.address || undefined; + addressCacheRef.current[order.addressId] = { ...parsed, fullAddress }; + } else { + addressCacheRef.current[order.addressId] = null; + } + } + } + + if (!coords) { + if (fullAddress) { + await Taro.setClipboardData({ data: fullAddress }); + Taro.showToast({ title: '未配置定位,地址已复制', icon: 'none' }); + } else { + Taro.showToast({ title: '暂无可导航的地址', icon: 'none' }); + } + return; + } + + Taro.openLocation({ + latitude: coords.lat, + longitude: coords.lng, + name: '收货地址', + address: fullAddress || '' + }); + } catch (e) { + console.error('一键导航失败:', e); + Taro.showToast({ title: '导航失败,请重试', icon: 'none' }); + } + }; + + const handleOneClickCall = async (order: GltTicketOrder) => { + const phone = (order.riderPhone || order.storePhone || '').trim(); + if (!phone) { + Taro.showToast({ title: '暂无可呼叫的电话', icon: 'none' }); + return; + } + try { + await Taro.makePhoneCall({ phoneNumber: phone }); + } catch (e) { + console.error('一键呼叫失败:', e); + Taro.showToast({ title: '呼叫失败,请手动拨打', icon: 'none' }); + } + }; + const getTicketOrderStatusMeta = (order: GltTicketOrder) => { if (order.status === 1) return { text: '已冻结', type: 'warning' as const }; @@ -498,6 +576,32 @@ const UserTicketList = () => { 下单时间:{formatDateTime(item.createTime)} + {(!!item.addressId || !!item.address || !!item.riderPhone || !!item.storePhone) ? ( + + {(!!item.addressId || !!item.address) ? ( + + ) : null} + {(!!item.riderPhone || !!item.storePhone) ? ( + + ) : null} + + ) : null} {/*{item.storeName ? (*/} {/* */} {/* 门店:{item.storeName}*/}