feat(rider): 添加配送员模块和订单图片保存功能

- 新增配送员首页界面,包含订单管理、工资明细、配送小区、仓库地址等功能入口
- 实现小程序码保存到相册功能,支持权限检查和错误处理
- 添加相册写入权限配置和图片下载临时路径处理
- 修复订单列表商品信息显示问题,优化支付流程
- 更新首页轮播图广告代码,调整用户中心网格布局
- 增加订单页面返回时的数据刷新机制,提升用户体验
This commit is contained in:
2026-01-31 02:52:28 +08:00
parent 7227ec6d84
commit f5c6d52b78
10 changed files with 531 additions and 104 deletions

View File

@@ -90,6 +90,12 @@ export default {
'comments/index', 'comments/index',
'search/index'] 'search/index']
}, },
{
"root": "rider",
"pages": [
"index"
]
},
{ {
"root": "admin", "root": "admin",
"pages": [ "pages": [
@@ -138,6 +144,9 @@ export default {
permission: { permission: {
"scope.userLocation": { "scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示" "desc": "你的位置信息将用于小程序位置接口的效果展示"
},
"scope.writePhotosAlbum": {
"desc": "用于保存小程序码到相册,方便分享给好友"
} }
} }
} }

View File

@@ -11,6 +11,7 @@ import {businessGradients} from '@/styles/gradients'
const DealerQrcode: React.FC = () => { const DealerQrcode: React.FC = () => {
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('') const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
const [saving, setSaving] = useState<boolean>(false)
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null) // const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
// const [statsLoading, setStatsLoading] = useState<boolean>(false) // const [statsLoading, setStatsLoading] = useState<boolean>(false)
const {dealerUser} = useDealerUser() const {dealerUser} = useDealerUser()
@@ -67,6 +68,66 @@ const DealerQrcode: React.FC = () => {
} }
}, [dealerUser?.userId]) }, [dealerUser?.userId])
const isAlbumAuthError = (errMsg?: string) => {
if (!errMsg) return false
// WeChat uses variants like: "saveImageToPhotosAlbum:fail auth deny",
// "saveImageToPhotosAlbum:fail auth denied", "authorize:fail auth deny"
return (
errMsg.includes('auth deny') ||
errMsg.includes('auth denied') ||
errMsg.includes('authorize') ||
errMsg.includes('scope.writePhotosAlbum')
)
}
const ensureWriteAlbumPermission = async (): Promise<boolean> => {
try {
const setting = await Taro.getSetting()
if (setting?.authSetting?.['scope.writePhotosAlbum']) return true
await Taro.authorize({scope: 'scope.writePhotosAlbum'})
return true
} catch (error: any) {
const modal = await Taro.showModal({
title: '提示',
content: '需要您授权保存图片到相册,请在设置中开启相册权限',
confirmText: '去设置'
})
if (modal.confirm) {
await Taro.openSetting()
}
return false
}
}
const downloadImageToLocalPath = async (url: string): Promise<string> => {
// saveImageToPhotosAlbum must receive a local temp path (e.g. `http://tmp/...` or `wxfile://...`).
// Some environments may return a non-existing temp path from getImageInfo, so we verify.
if (url.startsWith('http://tmp/') || url.startsWith('wxfile://')) {
return url
}
const token = Taro.getStorageSync('access_token')
const tenantId = Taro.getStorageSync('TenantId')
const header: Record<string, string> = {}
if (token) header.Authorization = token
if (tenantId) header.TenantId = tenantId
// 先下载到本地临时文件再保存到相册
const res = await Taro.downloadFile({url, header})
if (res.statusCode !== 200 || !res.tempFilePath) {
throw new Error(`图片下载失败(${res.statusCode || 'unknown'})`)
}
// Double-check file exists to avoid: saveImageToPhotosAlbum:fail no such file or directory
try {
await Taro.getFileInfo({filePath: res.tempFilePath})
} catch (_) {
throw new Error('图片临时文件不存在,请重试')
}
return res.tempFilePath
}
// 保存小程序码到相册 // 保存小程序码到相册
const saveMiniProgramCode = async () => { const saveMiniProgramCode = async () => {
if (!miniProgramCodeUrl) { if (!miniProgramCodeUrl) {
@@ -78,39 +139,64 @@ const DealerQrcode: React.FC = () => {
} }
try { try {
// 先下载图片到本地 if (saving) return
const res = await Taro.downloadFile({ setSaving(true)
url: miniProgramCodeUrl Taro.showLoading({title: '保存中...'})
})
if (res.statusCode === 200) { const hasPermission = await ensureWriteAlbumPermission()
// 保存到相册 if (!hasPermission) return
await Taro.saveImageToPhotosAlbum({
filePath: res.tempFilePath let filePath = await downloadImageToLocalPath(miniProgramCodeUrl)
}) try {
await Taro.saveImageToPhotosAlbum({filePath})
} catch (e: any) {
const msg = e?.errMsg || e?.message || ''
// Fallback: some devices/clients may fail to save directly from a temp path.
if (
msg.includes('no such file or directory') &&
(filePath.startsWith('http://tmp/') || filePath.startsWith('wxfile://'))
) {
const saved = (await Taro.saveFile({tempFilePath: filePath})) as unknown as { savedFilePath?: string }
if (saved?.savedFilePath) {
filePath = saved.savedFilePath
}
await Taro.saveImageToPhotosAlbum({filePath})
} else {
throw e
}
}
Taro.showToast({ Taro.showToast({
title: '保存成功', title: '保存成功',
icon: 'success' icon: 'success'
}) })
}
} catch (error: any) { } catch (error: any) {
if (error.errMsg?.includes('auth deny')) { const errMsg = error?.errMsg || error?.message
Taro.showModal({ if (errMsg?.includes('cancel')) {
Taro.showToast({title: '已取消', icon: 'none'})
return
}
if (isAlbumAuthError(errMsg)) {
const modal = await Taro.showModal({
title: '提示', title: '提示',
content: '需要您授权保存图片到相册', content: '需要您授权保存图片到相册',
success: (res) => { confirmText: '去设置'
if (res.confirm) {
Taro.openSetting()
}
}
}) })
if (modal.confirm) {
await Taro.openSetting()
}
} else { } else {
Taro.showToast({ // Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
await Taro.showModal({
title: '保存失败', title: '保存失败',
icon: 'error' content: errMsg || '保存失败,请稍后重试',
showCancel: false
}) })
} }
} finally {
Taro.hideLoading()
setSaving(false)
} }
} }
@@ -258,7 +344,7 @@ const DealerQrcode: React.FC = () => {
block block
icon={<Download/>} icon={<Download/>}
onClick={saveMiniProgramCode} onClick={saveMiniProgramCode}
disabled={!miniProgramCodeUrl || loading} disabled={!miniProgramCodeUrl || loading || saving}
> >
</Button> </Button>

View File

@@ -181,11 +181,11 @@ const DealerWithdraw: React.FC = () => {
} }
if (amount < 100) { if (amount < 100) {
Taro.showToast({ // Taro.showToast({
title: '最低提现金额为100元', // title: '最低提现金额为100元',
icon: 'error' // icon: 'error'
}) // })
return // return
} }
if (amount > available) { if (amount > available) {

View File

@@ -49,7 +49,7 @@ const MyPage = () => {
setLoading(true) setLoading(true)
try { try {
const [flashRes] = await Promise.allSettled([ const [flashRes] = await Promise.allSettled([
getCmsAdByCode('flash'), getCmsAdByCode('mp-ad'),
getCmsAdByCode('hot_today'), getCmsAdByCode('hot_today'),
pageCmsArticle({ limit: 1, recommend: 1 }), pageCmsArticle({ limit: 1, recommend: 1 }),
]) ])

View File

@@ -49,7 +49,7 @@ const UserCell = () => {
border: 'none' border: 'none'
} as React.CSSProperties} } as React.CSSProperties}
> >
<Grid.Item text="企业采购" onClick={() => navTo('/user/poster/poster', true)}> <Grid.Item text="配送订单" onClick={() => navTo('/rider/index', true)}>
<View className="text-center"> <View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2"> <View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<ShoppingAdd color="#3b82f6" size="20"/> <ShoppingAdd color="#3b82f6" size="20"/>

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '配送中心'
})

0
src/rider/index.scss Normal file
View File

295
src/rider/index.tsx Normal file
View File

@@ -0,0 +1,295 @@
import React from 'react'
import {View, Text} from '@tarojs/components'
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
import {
User,
Shopping,
Dongdong,
ArrowRight,
Purse,
People
} from '@nutui/icons-react-taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import { useThemeStyles } from '@/hooks/useTheme'
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
import Taro from '@tarojs/taro'
const DealerIndex: React.FC = () => {
const {
dealerUser,
error,
refresh,
} = useDealerUser()
// 使用主题样式
const themeStyles = useThemeStyles()
// 导航到各个功能页面
const navigateToPage = (url: string) => {
Taro.navigateTo({url})
}
// 格式化金额
const formatMoney = (money?: string) => {
if (!money) return '0.00'
return parseFloat(money).toFixed(2)
}
// 格式化时间
const formatTime = (time?: string) => {
if (!time) return '-'
return new Date(time).toLocaleDateString()
}
// 获取用户主题
const userTheme = gradientUtils.getThemeByUserId(dealerUser?.userId)
// 获取渐变背景
const getGradientBackground = (themeColor?: string) => {
if (themeColor) {
const darkerColor = gradientUtils.adjustColorBrightness(themeColor, -30)
return gradientUtils.createGradient(themeColor, darkerColor)
}
return userTheme.background
}
console.log(getGradientBackground(),'getGradientBackground()')
if (error) {
return (
<View className="p-4">
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
<Text className="text-red-600">{error}</Text>
</View>
<Button type="primary" onClick={refresh}>
</Button>
</View>
)
}
return (
<View className="bg-gray-100 min-h-screen">
<View>
{/*头部信息*/}
{dealerUser && (
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
{/* 装饰性背景元素 - 小程序兼容版本 */}
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="absolute w-24 h-24 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.08)',
bottom: '-12px',
left: '-12px'
}}></View>
<View className="absolute w-16 h-16 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
top: '60px',
left: '120px'
}}></View>
<View className="flex items-center justify-between relative z-10 mb-4">
<Avatar
size="50"
src={dealerUser?.qrcode}
icon={<User/>}
className="mr-4"
style={{
border: '2px solid rgba(255, 255, 255, 0.3)'
}}
/>
<View className="flex-1 flex-col">
<View className="text-white text-lg font-bold mb-1" style={{
}}>
{dealerUser?.realName || '分销商'}
</View>
<View className="text-sm" style={{
color: 'rgba(255, 255, 255, 0.8)'
}}>
ID: {dealerUser.userId}
</View>
</View>
<View className="text-right hidden">
<Text className="text-xs" style={{
color: 'rgba(255, 255, 255, 0.9)'
}}></Text>
<Text className="text-xs" style={{
color: 'rgba(255, 255, 255, 0.7)'
}}>
{formatTime(dealerUser.createTime)}
</Text>
</View>
</View>
</View>
)}
{/* 佣金统计卡片 */}
{dealerUser && (
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
<View className="mb-4">
<Text className="font-semibold text-gray-800"></Text>
</View>
<View className="grid grid-cols-3 gap-3">
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.available
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.money)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.frozen
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.freezeMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.total
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.totalMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
</View>
</View>
)}
{/* 团队统计 */}
{dealerUser && (
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
<View className="flex items-center justify-between mb-4">
<Text className="font-semibold text-gray-800"></Text>
<View
className="text-gray-400 text-sm flex items-center"
onClick={() => navigateToPage('/dealer/team/index')}
>
<Text></Text>
<ArrowRight size="12"/>
</View>
</View>
<View className="grid grid-cols-3 gap-4">
<View className="text-center grid">
<Text className="text-xl font-bold text-purple-500 mb-1">
{dealerUser.firstNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center grid">
<Text className="text-xl font-bold text-indigo-500 mb-1">
{dealerUser.secondNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center grid">
<Text className="text-xl font-bold text-pink-500 mb-1">
{dealerUser.thirdNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
</View>
</View>
)}
{/* 功能导航 */}
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
<View className="font-semibold mb-4 text-gray-800"></View>
<ConfigProvider>
<Grid
columns={4}
className="no-border-grid"
style={{
'--nutui-grid-border-color': 'transparent',
'--nutui-grid-item-border-width': '0px',
border: 'none'
} as React.CSSProperties}
>
<Grid.Item text="配送订单" onClick={() => navigateToPage('/rider/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shopping color="#3b82f6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'工资明细'} onClick={() => navigateToPage('/rider/withdraw/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Purse color="#10b981" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'配送小区'} onClick={() => navigateToPage('/rider/team/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<People color="#8b5cf6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'仓库地址'} onClick={() => navigateToPage('/rider/qrcode/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Dongdong color="#f59e0b" size="20"/>
</View>
</View>
</Grid.Item>
</Grid>
{/* 第二行功能 */}
{/*<Grid*/}
{/* columns={4}*/}
{/* className="no-border-grid mt-4"*/}
{/* style={{*/}
{/* '--nutui-grid-border-color': 'transparent',*/}
{/* '--nutui-grid-item-border-width': '0px',*/}
{/* border: 'none'*/}
{/* } as React.CSSProperties}*/}
{/*>*/}
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Presentation color="#6366f1" size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* /!* 预留其他功能位置 *!/*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/*</Grid>*/}
</ConfigProvider>
</View>
</View>
{/* 底部安全区域 */}
<View className="h-20"></View>
</View>
)
}
export default DealerIndex

View File

@@ -1,5 +1,5 @@
import {Avatar, Cell, Space, Empty, Tabs, Button, TabPane, Image, Dialog} from '@nutui/nutui-react-taro' import {Avatar, Cell, Space, Empty, Tabs, Button, TabPane, Image, Dialog} from '@nutui/nutui-react-taro'
import {useEffect, useState, useCallback, CSSProperties} from "react"; import {useEffect, useState, useCallback, useRef, CSSProperties} from "react";
import {View, Text} from '@tarojs/components' import {View, Text} from '@tarojs/components'
import Taro from '@tarojs/taro'; import Taro from '@tarojs/taro';
import {InfiniteLoading} from '@nutui/nutui-react-taro' import {InfiniteLoading} from '@nutui/nutui-react-taro'
@@ -80,7 +80,8 @@ const tabs = [
// 扩展订单接口,包含商品信息 // 扩展订单接口,包含商品信息
interface OrderWithGoods extends ShopOrder { interface OrderWithGoods extends ShopOrder {
orderGoods?: ShopOrderGoods[]; // 避免与 ShopOrder.orderGoods (OrderGoods[]) 冲突:这里使用单独字段保存接口返回的商品明细
orderGoodsList?: ShopOrderGoods[];
} }
interface OrderListProps { interface OrderListProps {
@@ -92,8 +93,9 @@ interface OrderListProps {
function OrderList(props: OrderListProps) { function OrderList(props: OrderListProps) {
const [list, setList] = useState<OrderWithGoods[]>([]) const [list, setList] = useState<OrderWithGoods[]>([])
const [page, setPage] = useState(1) const pageRef = useRef(1)
const [hasMore, setHasMore] = useState(true) const [hasMore, setHasMore] = useState(true)
const [payingOrderId, setPayingOrderId] = useState<number | null>(null)
// 根据传入的statusFilter设置初始tab索引 // 根据传入的statusFilter设置初始tab索引
const getInitialTabIndex = () => { const getInitialTabIndex = () => {
if (props.searchParams?.statusFilter !== undefined) { if (props.searchParams?.statusFilter !== undefined) {
@@ -183,7 +185,7 @@ function OrderList(props: OrderListProps) {
const reload = useCallback(async (resetPage = false, targetPage?: number) => { const reload = useCallback(async (resetPage = false, targetPage?: number) => {
setLoading(true); setLoading(true);
setError(null); // 清除之前的错误 setError(null); // 清除之前的错误
const currentPage = resetPage ? 1 : (targetPage || page); const currentPage = resetPage ? 1 : (targetPage || pageRef.current);
const statusParams = getOrderStatusParams(tapIndex); const statusParams = getOrderStatusParams(tapIndex);
// 合并搜索条件tab的statusFilter优先级更高 // 合并搜索条件tab的statusFilter优先级更高
const searchConditions: any = { const searchConditions: any = {
@@ -205,7 +207,6 @@ function OrderList(props: OrderListProps) {
try { try {
const res = await pageShopOrder(searchConditions); const res = await pageShopOrder(searchConditions);
let newList: OrderWithGoods[];
if (res?.list && res?.list.length > 0) { if (res?.list && res?.list.length > 0) {
// 批量获取订单商品信息,限制并发数量 // 批量获取订单商品信息,限制并发数量
@@ -220,13 +221,13 @@ function OrderList(props: OrderListProps) {
const orderGoods = await listShopOrderGoods({orderId: order.orderId}); const orderGoods = await listShopOrderGoods({orderId: order.orderId});
return { return {
...order, ...order,
orderGoods: orderGoods || [] orderGoodsList: orderGoods || []
}; };
} catch (error) { } catch (error) {
console.error('获取订单商品失败:', error); console.error('获取订单商品失败:', error);
return { return {
...order, ...order,
orderGoods: [] orderGoodsList: []
}; };
} }
}) })
@@ -248,7 +249,7 @@ function OrderList(props: OrderListProps) {
setHasMore(false); setHasMore(false);
} }
setPage(currentPage); pageRef.current = currentPage;
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
console.error('加载订单失败:', error); console.error('加载订单失败:', error);
@@ -260,14 +261,14 @@ function OrderList(props: OrderListProps) {
icon: 'none' icon: 'none'
}); });
} }
}, [tapIndex, page, props.searchParams]); // 移除 list 依赖 }, [tapIndex, props.searchParams]); // 移除 list/page 依赖避免useEffect触发循环
const reloadMore = useCallback(async () => { const reloadMore = useCallback(async () => {
if (loading || !hasMore) return; // 防止重复加载 if (loading || !hasMore) return; // 防止重复加载
const nextPage = page + 1; const nextPage = pageRef.current + 1;
setPage(nextPage); pageRef.current = nextPage;
await reload(false, nextPage); await reload(false, nextPage);
}, [loading, hasMore, page, reload]); }, [loading, hasMore, reload]);
// 确认收货 - 显示确认对话框 // 确认收货 - 显示确认对话框
const confirmReceive = (order: ShopOrder) => { const confirmReceive = (order: ShopOrder) => {
@@ -325,7 +326,7 @@ function OrderList(props: OrderListProps) {
}); });
// 更新本地状态 // 更新本地状态
setDataSource(prev => prev.map(item => setList(prev => prev.map(item =>
item.orderId === order.orderId ? {...item, orderStatus: 4} : item item.orderId === order.orderId ? {...item, orderStatus: 4} : item
)); ));
@@ -351,49 +352,23 @@ function OrderList(props: OrderListProps) {
}; };
// 再次购买 (已完成状态) // 再次购买 (已完成状态)
const buyAgain = (order: ShopOrder) => { const buyAgain = (order: OrderWithGoods) => {
console.log('再次购买:', order); console.log('再次购买:', order);
goTo(`/shop/orderConfirm/index?goodsId=${order.orderGoods[0].goodsId}`) const goodsId = order.orderGoodsList?.[0]?.goodsId
if (!goodsId) {
Taro.showToast({
title: '订单商品信息缺失',
icon: 'none'
});
return;
}
goTo(`/shop/orderConfirm/index?goodsId=${goodsId}`)
// Taro.showToast({ // Taro.showToast({
// title: '再次购买功能开发中', // title: '再次购买功能开发中',
// icon: 'none' // icon: 'none'
// }); // });
}; };
// 评价商品 (已完成状态)
const evaluateGoods = (order: ShopOrder) => {
// 跳转到评价页面
Taro.navigateTo({
url: `/user/order/evaluate/index?orderId=${order.orderId}&orderNo=${order.orderNo}`
});
};
// 查看进度 (退款/售后状态)
const viewProgress = (order: ShopOrder) => {
// 根据订单状态确定售后类型
let afterSaleType = 'refund' // 默认退款
if (order.orderStatus === 4) {
afterSaleType = 'refund' // 退款申请中
} else if (order.orderStatus === 7) {
afterSaleType = 'return' // 退货申请中
}
// 跳转到售后进度页面
Taro.navigateTo({
url: `/user/order/progress/index?orderId=${order.orderId}&orderNo=${order.orderNo}&type=${afterSaleType}`
});
};
// 撤销申请 (退款/售后状态)
const cancelApplication = (order: ShopOrder) => {
console.log('撤销申请:', order);
Taro.showToast({
title: '撤销申请功能开发中',
icon: 'none'
});
};
// 取消订单 // 取消订单
const cancelOrder = (order: ShopOrder) => { const cancelOrder = (order: ShopOrder) => {
setOrderToCancel(order); setOrderToCancel(order);
@@ -437,7 +412,7 @@ function OrderList(props: OrderListProps) {
}; };
// 立即支付 // 立即支付
const payOrder = async (order: ShopOrder) => { const payOrder = async (order: OrderWithGoods) => {
try { try {
if (!order.orderId || !order.orderNo) { if (!order.orderId || !order.orderNo) {
Taro.showToast({ Taro.showToast({
@@ -447,6 +422,11 @@ function OrderList(props: OrderListProps) {
return; return;
} }
if (payingOrderId === order.orderId) {
return;
}
setPayingOrderId(order.orderId);
// 检查订单是否已过期 // 检查订单是否已过期
if (order.createTime && isPaymentExpired(order.createTime)) { if (order.createTime && isPaymentExpired(order.createTime)) {
Taro.showToast({ Taro.showToast({
@@ -475,11 +455,34 @@ function OrderList(props: OrderListProps) {
Taro.showLoading({title: '发起支付...'}); Taro.showLoading({title: '发起支付...'});
// 构建商品数据 // 构建商品数据优先使用列表已加载的商品信息缺失时再补拉一次避免goodsItems为空导致后端拒绝/再次支付失败
const goodsItems = order.orderGoods?.map(goods => ({ let orderGoods = order.orderGoodsList || [];
goodsId: goods.goodsId, if (!orderGoods.length) {
quantity: goods.totalNum || 1 try {
})) || []; orderGoods = (await listShopOrderGoods({orderId: order.orderId})) || [];
} catch (e) {
// 继续走下面的校验提示
console.error('补拉订单商品失败:', e);
}
}
const goodsItems = orderGoods
.filter(g => !!g.goodsId)
.map(goods => ({
goodsId: goods.goodsId as number,
quantity: goods.totalNum || 1,
// 若后端按SKU计算价格/库存补齐SKU/规格信息更安全
skuId: (goods as any).skuId,
specInfo: (goods as any).spec || (goods as any).specInfo
}));
if (!goodsItems.length) {
Taro.showToast({
title: '订单商品信息缺失,请稍后重试',
icon: 'none'
});
return;
}
// 对于已存在的订单,我们需要重新发起支付 // 对于已存在的订单,我们需要重新发起支付
// 构建支付请求数据,包含完整的商品信息 // 构建支付请求数据,包含完整的商品信息
@@ -488,7 +491,13 @@ function OrderList(props: OrderListProps) {
orderNo: order.orderNo, orderNo: order.orderNo,
goodsItems: goodsItems, goodsItems: goodsItems,
addressId: order.addressId, addressId: order.addressId,
payType: PaymentType.WECHAT payType: PaymentType.WECHAT,
// 尽量携带原订单信息,避免后端重新计算/校验不一致(如使用了优惠券/自提等)
couponId: order.couponId,
deliveryType: order.deliveryType,
selfTakeMerchantId: order.selfTakeMerchantId,
comments: order.comments,
title: order.title
}; };
console.log('重新支付数据:', paymentData); console.log('重新支付数据:', paymentData);
@@ -506,6 +515,7 @@ function OrderList(props: OrderListProps) {
} }
// 调用微信支付 // 调用微信支付
try {
await Taro.requestPayment({ await Taro.requestPayment({
timeStamp: result.timeStamp, timeStamp: result.timeStamp,
nonceStr: result.nonceStr, nonceStr: result.nonceStr,
@@ -513,6 +523,18 @@ function OrderList(props: OrderListProps) {
signType: (result.signType || 'MD5') as 'MD5' | 'HMAC-SHA256', signType: (result.signType || 'MD5') as 'MD5' | 'HMAC-SHA256',
paySign: result.paySign, paySign: result.paySign,
}); });
} catch (payError: any) {
const msg: string = payError?.errMsg || payError?.message || '';
if (msg.includes('cancel')) {
// 用户主动取消,不当作“失败”强提示
Taro.showToast({
title: '已取消支付',
icon: 'none'
});
return;
}
throw payError;
}
// 支付成功 // 支付成功
Taro.showToast({ Taro.showToast({
@@ -533,13 +555,14 @@ function OrderList(props: OrderListProps) {
console.error('支付失败:', error); console.error('支付失败:', error);
let errorMessage = '支付失败,请重试'; let errorMessage = '支付失败,请重试';
if (error.message) { const rawMsg: string = error?.errMsg || error?.message || '';
if (error.message.includes('cancel')) { if (rawMsg) {
if (rawMsg.includes('cancel')) {
errorMessage = '用户取消支付'; errorMessage = '用户取消支付';
} else if (error.message.includes('余额不足')) { } else if (rawMsg.includes('余额不足')) {
errorMessage = '账户余额不足'; errorMessage = '账户余额不足';
} else { } else {
errorMessage = error.message; errorMessage = rawMsg;
} }
} }
@@ -549,13 +572,13 @@ function OrderList(props: OrderListProps) {
}); });
} finally { } finally {
Taro.hideLoading(); Taro.hideLoading();
setPayingOrderId(null);
} }
}; };
useEffect(() => { useEffect(() => {
void reload(true); // 首次加载tab切换时重置页码 void reload(true); // 首次加载tab切换或搜索条件变化时重置页码
}, [tapIndex]); // 只监听tapIndex变化避免reload依赖循环 }, [reload]);
// 监听外部statusFilter变化同步更新tab索引 // 监听外部statusFilter变化同步更新tab索引
useEffect(() => { useEffect(() => {
@@ -705,8 +728,8 @@ function OrderList(props: OrderListProps) {
{/* 商品信息 */} {/* 商品信息 */}
<View className={'goods-info'}> <View className={'goods-info'}>
{item.orderGoods && item.orderGoods.length > 0 ? ( {item.orderGoodsList && item.orderGoodsList.length > 0 ? (
item.orderGoods.map((goods, goodsIndex) => ( item.orderGoodsList.map((goods, goodsIndex) => (
<View key={goodsIndex} className={'flex items-center mb-2'}> <View key={goodsIndex} className={'flex items-center mb-2'}>
<Image <Image
src={goods.image || '/default-goods.png'} src={goods.image || '/default-goods.png'}

View File

@@ -4,7 +4,7 @@ import {Space, NavBar, Button, Input} from '@nutui/nutui-react-taro'
import {Search, Filter, ArrowLeft} from '@nutui/icons-react-taro' import {Search, Filter, ArrowLeft} from '@nutui/icons-react-taro'
import {View} from '@tarojs/components'; import {View} from '@tarojs/components';
import OrderList from "./components/OrderList"; import OrderList from "./components/OrderList";
import {useRouter} from '@tarojs/taro' import {useDidShow, useRouter} from '@tarojs/taro'
import {ShopOrderParam} from "@/api/shop/shopOrder/model"; import {ShopOrderParam} from "@/api/shop/shopOrder/model";
import './order.scss' import './order.scss'
@@ -72,6 +72,17 @@ function Order() {
reload().then() reload().then()
}, []); }, []);
// 页面从其它页面返回/重新展示时,刷新一次列表数据
// 典型场景:微信支付取消后返回到待支付列表,需要重新拉取订单/商品信息,避免使用旧数据再次支付失败
useDidShow(() => {
const statusFilter =
params.statusFilter != undefined && params.statusFilter !== ''
? parseInt(params.statusFilter)
: -1;
// 同步路由上的statusFilter并触发子组件重新拉取列表
setSearchParams(prev => ({ ...prev, statusFilter }));
});
return ( return (
<View className="bg-gray-50 min-h-screen"> <View className="bg-gray-50 min-h-screen">
<View style={{height: `${statusBarHeight || 0}px`, backgroundColor: '#ffffff'}}></View> <View style={{height: `${statusBarHeight || 0}px`, backgroundColor: '#ffffff'}}></View>