forked from gxwebsoft/mp-10550
feat(address): 添加微信地址导入功能和一键导航呼叫功能
- 新增微信地址导入流程,支持从微信原生地址选择后跳转到编辑页面完善定位 - 添加WxAddressDraft缓存机制用于存储微信返回的地址草稿数据 - 实现一键导航功能,支持通过订单地址ID或地址信息进行地图导航 - 添加一键呼叫功能,支持直接拨打电话联系骑手或门店 - 优化地址编辑页面支持微信导入模式和默认地址检查
This commit is contained in:
@@ -48,10 +48,15 @@ const AddUserAddress = () => {
|
|||||||
const [inputText, setInputText] = useState<string>('')
|
const [inputText, setInputText] = useState<string>('')
|
||||||
const [selectedLocation, setSelectedLocation] = useState<SelectedLocation | null>(null)
|
const [selectedLocation, setSelectedLocation] = useState<SelectedLocation | null>(null)
|
||||||
const formRef = useRef<any>(null)
|
const formRef = useRef<any>(null)
|
||||||
|
const wxDraftRef = useRef<Partial<ShopUserAddress> | null>(null)
|
||||||
|
const wxDraftPatchedRef = useRef(false)
|
||||||
|
|
||||||
// 判断是编辑还是新增模式
|
// 判断是编辑还是新增模式
|
||||||
const isEditMode = !!params.id
|
const isEditMode = !!params.id
|
||||||
const addressId = params.id ? Number(params.id) : undefined
|
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 () => {
|
const reload = async () => {
|
||||||
// 整理地区数据
|
// 整理地区数据
|
||||||
@@ -59,7 +64,7 @@ const AddUserAddress = () => {
|
|||||||
|
|
||||||
// 新增模式:若存在“默认地址”但缺少经纬度,则强制引导到编辑页完善定位
|
// 新增模式:若存在“默认地址”但缺少经纬度,则强制引导到编辑页完善定位
|
||||||
// 目的:确保业务依赖默认地址坐标(选店/配送范围)时不会因为缺失坐标而失败
|
// 目的:确保业务依赖默认地址坐标(选店/配送范围)时不会因为缺失坐标而失败
|
||||||
if (!isEditMode) {
|
if (!isEditMode && !skipDefaultCheck) {
|
||||||
try {
|
try {
|
||||||
const defaultList = await listShopUserAddress({ isDefault: true })
|
const defaultList = await listShopUserAddress({ isDefault: true })
|
||||||
const defaultAddr = defaultList?.[0]
|
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) {
|
if (isEditMode && addressId) {
|
||||||
try {
|
try {
|
||||||
@@ -391,6 +421,7 @@ const AddUserAddress = () => {
|
|||||||
// 准备提交的数据
|
// 准备提交的数据
|
||||||
const submitData = {
|
const submitData = {
|
||||||
...values,
|
...values,
|
||||||
|
country: FormData.country,
|
||||||
province: FormData.province,
|
province: FormData.province,
|
||||||
city: FormData.city,
|
city: FormData.city,
|
||||||
region: FormData.region,
|
region: FormData.region,
|
||||||
@@ -448,13 +479,34 @@ const AddUserAddress = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 动态设置页面标题
|
// 动态设置页面标题
|
||||||
Taro.setNavigationBarTitle({
|
Taro.setNavigationBarTitle({
|
||||||
title: isEditMode ? '编辑收货地址' : '新增收货地址'
|
title: isEditMode ? '编辑收货地址' : (fromWx ? '完善收货地址' : '新增收货地址')
|
||||||
});
|
});
|
||||||
|
|
||||||
reload().then(() => {
|
reload().then(() => {
|
||||||
setLoading(false)
|
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) {
|
if (loading) {
|
||||||
return <Loading className={'px-2'}>加载中</Loading>
|
return <Loading className={'px-2'}>加载中</Loading>
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import {addShopUserAddress} from "@/api/shop/shopUserAddress";
|
|
||||||
import { getCurrentLngLat } from "@/utils/location";
|
|
||||||
|
|
||||||
const WxAddress = () => {
|
const WxAddress = () => {
|
||||||
/**
|
/**
|
||||||
* 从微信API获取用户收货地址
|
* 从微信API获取用户收货地址
|
||||||
* 调用微信原生地址选择界面,获取成功后保存到服务器并刷新列表
|
* 调用微信原生地址选择界面,获取成功后跳转到“新增收货地址”页面,让用户选择定位后再保存
|
||||||
*/
|
*/
|
||||||
const getWeChatAddress = () => {
|
const getWeChatAddress = () => {
|
||||||
Taro.chooseAddress()
|
Taro.chooseAddress()
|
||||||
.then(async res => {
|
.then(async res => {
|
||||||
const loc = await getCurrentLngLat()
|
// 仅填充微信地址信息,不要用“当前定位”覆盖经纬度(会造成经纬度与地址不匹配)。
|
||||||
if (!loc) {
|
// 选择后跳转到“新增/编辑收货地址”页面,让用户手动选择地图定位后再保存。
|
||||||
// Avoid leaving the user on an empty page.
|
const addressDraft = {
|
||||||
setTimeout(() => Taro.navigateBack(), 300)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化微信返回的地址数据为后端所需格式
|
|
||||||
const addressData = {
|
|
||||||
name: res.userName,
|
name: res.userName,
|
||||||
phone: res.telNumber,
|
phone: res.telNumber,
|
||||||
country: res.nationalCode || '中国',
|
country: res.nationalCode || '中国',
|
||||||
@@ -27,40 +19,32 @@ const WxAddress = () => {
|
|||||||
city: res.cityName,
|
city: res.cityName,
|
||||||
region: res.countyName,
|
region: res.countyName,
|
||||||
address: res.detailInfo,
|
address: res.detailInfo,
|
||||||
postalCode: res.postalCode,
|
isDefault: false,
|
||||||
lng: loc.lng,
|
|
||||||
lat: loc.lat,
|
|
||||||
isDefault: false
|
|
||||||
}
|
}
|
||||||
console.log(res, 'addrs..')
|
Taro.setStorageSync('WxAddressDraft', addressDraft)
|
||||||
// 调用保存地址的API(假设存在该接口)
|
// 用 redirectTo 替换当前页面,避免保存后 navigateBack 回到空白的 wxAddress 页面。
|
||||||
addShopUserAddress(addressData)
|
await Taro.redirectTo({ url: '/user/address/add?fromWx=1&skipDefaultCheck=1' })
|
||||||
.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'})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('获取微信地址失败:', 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({
|
Taro.showModal({
|
||||||
title: '授权失败',
|
title: '授权失败',
|
||||||
content: '请在设置中允许获取地址权限',
|
content: '请在设置中允许获取地址权限',
|
||||||
showCancel: false
|
showCancel: false
|
||||||
})
|
})
|
||||||
|
setTimeout(() => Taro.navigateBack(), 300)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Taro.showToast({ title: '获取微信地址失败', icon: 'none' })
|
||||||
|
setTimeout(() => Taro.navigateBack(), 300)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import Taro, { useDidShow } from '@tarojs/taro';
|
import Taro, { useDidShow } from '@tarojs/taro';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -18,6 +18,7 @@ import { pageGltUserTicket } from '@/api/glt/gltUserTicket';
|
|||||||
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model';
|
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model';
|
||||||
import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder';
|
import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder';
|
||||||
import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model';
|
import type { GltTicketOrder } from '@/api/glt/gltTicketOrder/model';
|
||||||
|
import { getShopUserAddress } from '@/api/shop/shopUserAddress';
|
||||||
import { BaseUrl } from '@/config/app';
|
import { BaseUrl } from '@/config/app';
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
@@ -46,6 +47,8 @@ const UserTicketList = () => {
|
|||||||
const [qrTicket, setQrTicket] = useState<GltUserTicket | null>(null);
|
const [qrTicket, setQrTicket] = useState<GltUserTicket | null>(null);
|
||||||
const [qrImageUrl, setQrImageUrl] = useState('');
|
const [qrImageUrl, setQrImageUrl] = useState('');
|
||||||
|
|
||||||
|
const addressCacheRef = useRef<Record<number, { lng: number; lat: number; fullAddress?: string } | null>>({});
|
||||||
|
|
||||||
const getUserId = () => {
|
const getUserId = () => {
|
||||||
const raw = Taro.getStorageSync('UserId');
|
const raw = Taro.getStorageSync('UserId');
|
||||||
const id = Number(raw);
|
const id = Number(raw);
|
||||||
@@ -262,6 +265,81 @@ const UserTicketList = () => {
|
|||||||
return d.isValid() ? d.format('YYYY年MM月DD日') : v;
|
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) => {
|
const getTicketOrderStatusMeta = (order: GltTicketOrder) => {
|
||||||
if (order.status === 1) return { text: '已冻结', type: 'warning' as const };
|
if (order.status === 1) return { text: '已冻结', type: 'warning' as const };
|
||||||
|
|
||||||
@@ -498,6 +576,32 @@ const UserTicketList = () => {
|
|||||||
<View className="mt-1">
|
<View className="mt-1">
|
||||||
<Text className="text-xs text-gray-500">下单时间:{formatDateTime(item.createTime)}</Text>
|
<Text className="text-xs text-gray-500">下单时间:{formatDateTime(item.createTime)}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
{(!!item.addressId || !!item.address || !!item.riderPhone || !!item.storePhone) ? (
|
||||||
|
<View className="mt-3 flex justify-end gap-2">
|
||||||
|
{(!!item.addressId || !!item.address) ? (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
void handleNavigateToAddress(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
一键导航
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{(!!item.riderPhone || !!item.storePhone) ? (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
void handleOneClickCall(item);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
一键呼叫
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
{/*{item.storeName ? (*/}
|
{/*{item.storeName ? (*/}
|
||||||
{/* <View className="mt-1 text-xs text-gray-500">*/}
|
{/* <View className="mt-1 text-xs text-gray-500">*/}
|
||||||
{/* <Text>门店:{item.storeName}</Text>*/}
|
{/* <Text>门店:{item.storeName}</Text>*/}
|
||||||
|
|||||||
Reference in New Issue
Block a user