feat(address): 添加微信地址导入功能和一键导航呼叫功能

- 新增微信地址导入流程,支持从微信原生地址选择后跳转到编辑页面完善定位
- 添加WxAddressDraft缓存机制用于存储微信返回的地址草稿数据
- 实现一键导航功能,支持通过订单地址ID或地址信息进行地图导航
- 添加一键呼叫功能,支持直接拨打电话联系骑手或门店
- 优化地址编辑页面支持微信导入模式和默认地址检查
This commit is contained in:
2026-02-25 18:06:45 +08:00
parent 3d94125c5e
commit d86cdad470
3 changed files with 179 additions and 39 deletions

View File

@@ -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<GltUserTicket | null>(null);
const [qrImageUrl, setQrImageUrl] = useState('');
const addressCacheRef = useRef<Record<number, { lng: number; lat: number; fullAddress?: string } | null>>({});
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 = () => {
<View className="mt-1">
<Text className="text-xs text-gray-500">{formatDateTime(item.createTime)}</Text>
</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 ? (*/}
{/* <View className="mt-1 text-xs text-gray-500">*/}
{/* <Text>门店:{item.storeName}</Text>*/}