forked from gxwebsoft/mp-10550
feat(rider): 添加配送员模块和订单图片保存功能
- 新增配送员首页界面,包含订单管理、工资明细、配送小区、仓库地址等功能入口 - 实现小程序码保存到相册功能,支持权限检查和错误处理 - 添加相册写入权限配置和图片下载临时路径处理 - 修复订单列表商品信息显示问题,优化支付流程 - 更新首页轮播图广告代码,调整用户中心网格布局 - 增加订单页面返回时的数据刷新机制,提升用户体验
This commit is contained in:
@@ -90,6 +90,12 @@ export default {
|
||||
'comments/index',
|
||||
'search/index']
|
||||
},
|
||||
{
|
||||
"root": "rider",
|
||||
"pages": [
|
||||
"index"
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "admin",
|
||||
"pages": [
|
||||
@@ -138,6 +144,9 @@ export default {
|
||||
permission: {
|
||||
"scope.userLocation": {
|
||||
"desc": "你的位置信息将用于小程序位置接口的效果展示"
|
||||
},
|
||||
"scope.writePhotosAlbum": {
|
||||
"desc": "用于保存小程序码到相册,方便分享给好友"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {businessGradients} from '@/styles/gradients'
|
||||
const DealerQrcode: React.FC = () => {
|
||||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [saving, setSaving] = useState<boolean>(false)
|
||||
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
||||
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
||||
const {dealerUser} = useDealerUser()
|
||||
@@ -67,6 +68,66 @@ const DealerQrcode: React.FC = () => {
|
||||
}
|
||||
}, [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 () => {
|
||||
if (!miniProgramCodeUrl) {
|
||||
@@ -78,39 +139,64 @@ const DealerQrcode: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 先下载图片到本地
|
||||
const res = await Taro.downloadFile({
|
||||
url: miniProgramCodeUrl
|
||||
})
|
||||
if (saving) return
|
||||
setSaving(true)
|
||||
Taro.showLoading({title: '保存中...'})
|
||||
|
||||
if (res.statusCode === 200) {
|
||||
// 保存到相册
|
||||
await Taro.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath
|
||||
})
|
||||
const hasPermission = await ensureWriteAlbumPermission()
|
||||
if (!hasPermission) return
|
||||
|
||||
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({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errMsg?.includes('auth deny')) {
|
||||
Taro.showModal({
|
||||
const errMsg = error?.errMsg || error?.message
|
||||
if (errMsg?.includes('cancel')) {
|
||||
Taro.showToast({title: '已取消', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
if (isAlbumAuthError(errMsg)) {
|
||||
const modal = await Taro.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存图片到相册',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
Taro.openSetting()
|
||||
}
|
||||
}
|
||||
confirmText: '去设置'
|
||||
})
|
||||
if (modal.confirm) {
|
||||
await Taro.openSetting()
|
||||
}
|
||||
} else {
|
||||
Taro.showToast({
|
||||
// Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
|
||||
await Taro.showModal({
|
||||
title: '保存失败',
|
||||
icon: 'error'
|
||||
content: errMsg || '保存失败,请稍后重试',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
Taro.hideLoading()
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +344,7 @@ const DealerQrcode: React.FC = () => {
|
||||
block
|
||||
icon={<Download/>}
|
||||
onClick={saveMiniProgramCode}
|
||||
disabled={!miniProgramCodeUrl || loading}
|
||||
disabled={!miniProgramCodeUrl || loading || saving}
|
||||
>
|
||||
保存小程序码到相册
|
||||
</Button>
|
||||
|
||||
@@ -181,11 +181,11 @@ const DealerWithdraw: React.FC = () => {
|
||||
}
|
||||
|
||||
if (amount < 100) {
|
||||
Taro.showToast({
|
||||
title: '最低提现金额为100元',
|
||||
icon: 'error'
|
||||
})
|
||||
return
|
||||
// Taro.showToast({
|
||||
// title: '最低提现金额为100元',
|
||||
// icon: 'error'
|
||||
// })
|
||||
// return
|
||||
}
|
||||
|
||||
if (amount > available) {
|
||||
|
||||
@@ -49,7 +49,7 @@ const MyPage = () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [flashRes] = await Promise.allSettled([
|
||||
getCmsAdByCode('flash'),
|
||||
getCmsAdByCode('mp-ad'),
|
||||
getCmsAdByCode('hot_today'),
|
||||
pageCmsArticle({ limit: 1, recommend: 1 }),
|
||||
])
|
||||
|
||||
@@ -49,7 +49,7 @@ const UserCell = () => {
|
||||
border: 'none'
|
||||
} 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="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<ShoppingAdd color="#3b82f6" size="20"/>
|
||||
|
||||
3
src/rider/index.config.ts
Normal file
3
src/rider/index.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '配送中心'
|
||||
})
|
||||
0
src/rider/index.scss
Normal file
0
src/rider/index.scss
Normal file
295
src/rider/index.tsx
Normal file
295
src/rider/index.tsx
Normal 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
|
||||
@@ -1,5 +1,5 @@
|
||||
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 Taro from '@tarojs/taro';
|
||||
import {InfiniteLoading} from '@nutui/nutui-react-taro'
|
||||
@@ -80,7 +80,8 @@ const tabs = [
|
||||
|
||||
// 扩展订单接口,包含商品信息
|
||||
interface OrderWithGoods extends ShopOrder {
|
||||
orderGoods?: ShopOrderGoods[];
|
||||
// 避免与 ShopOrder.orderGoods (OrderGoods[]) 冲突:这里使用单独字段保存接口返回的商品明细
|
||||
orderGoodsList?: ShopOrderGoods[];
|
||||
}
|
||||
|
||||
interface OrderListProps {
|
||||
@@ -92,8 +93,9 @@ interface OrderListProps {
|
||||
|
||||
function OrderList(props: OrderListProps) {
|
||||
const [list, setList] = useState<OrderWithGoods[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const pageRef = useRef(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [payingOrderId, setPayingOrderId] = useState<number | null>(null)
|
||||
// 根据传入的statusFilter设置初始tab索引
|
||||
const getInitialTabIndex = () => {
|
||||
if (props.searchParams?.statusFilter !== undefined) {
|
||||
@@ -183,7 +185,7 @@ function OrderList(props: OrderListProps) {
|
||||
const reload = useCallback(async (resetPage = false, targetPage?: number) => {
|
||||
setLoading(true);
|
||||
setError(null); // 清除之前的错误
|
||||
const currentPage = resetPage ? 1 : (targetPage || page);
|
||||
const currentPage = resetPage ? 1 : (targetPage || pageRef.current);
|
||||
const statusParams = getOrderStatusParams(tapIndex);
|
||||
// 合并搜索条件,tab的statusFilter优先级更高
|
||||
const searchConditions: any = {
|
||||
@@ -205,7 +207,6 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
try {
|
||||
const res = await pageShopOrder(searchConditions);
|
||||
let newList: OrderWithGoods[];
|
||||
|
||||
if (res?.list && res?.list.length > 0) {
|
||||
// 批量获取订单商品信息,限制并发数量
|
||||
@@ -220,13 +221,13 @@ function OrderList(props: OrderListProps) {
|
||||
const orderGoods = await listShopOrderGoods({orderId: order.orderId});
|
||||
return {
|
||||
...order,
|
||||
orderGoods: orderGoods || []
|
||||
orderGoodsList: orderGoods || []
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取订单商品失败:', error);
|
||||
return {
|
||||
...order,
|
||||
orderGoods: []
|
||||
orderGoodsList: []
|
||||
};
|
||||
}
|
||||
})
|
||||
@@ -248,7 +249,7 @@ function OrderList(props: OrderListProps) {
|
||||
setHasMore(false);
|
||||
}
|
||||
|
||||
setPage(currentPage);
|
||||
pageRef.current = currentPage;
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('加载订单失败:', error);
|
||||
@@ -260,14 +261,14 @@ function OrderList(props: OrderListProps) {
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}, [tapIndex, page, props.searchParams]); // 移除 list 依赖
|
||||
}, [tapIndex, props.searchParams]); // 移除 list/page 依赖,避免useEffect触发循环
|
||||
|
||||
const reloadMore = useCallback(async () => {
|
||||
if (loading || !hasMore) return; // 防止重复加载
|
||||
const nextPage = page + 1;
|
||||
setPage(nextPage);
|
||||
const nextPage = pageRef.current + 1;
|
||||
pageRef.current = nextPage;
|
||||
await reload(false, nextPage);
|
||||
}, [loading, hasMore, page, reload]);
|
||||
}, [loading, hasMore, reload]);
|
||||
|
||||
// 确认收货 - 显示确认对话框
|
||||
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
|
||||
));
|
||||
|
||||
@@ -351,49 +352,23 @@ function OrderList(props: OrderListProps) {
|
||||
};
|
||||
|
||||
// 再次购买 (已完成状态)
|
||||
const buyAgain = (order: ShopOrder) => {
|
||||
const buyAgain = (order: OrderWithGoods) => {
|
||||
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({
|
||||
// title: '再次购买功能开发中',
|
||||
// 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) => {
|
||||
setOrderToCancel(order);
|
||||
@@ -437,7 +412,7 @@ function OrderList(props: OrderListProps) {
|
||||
};
|
||||
|
||||
// 立即支付
|
||||
const payOrder = async (order: ShopOrder) => {
|
||||
const payOrder = async (order: OrderWithGoods) => {
|
||||
try {
|
||||
if (!order.orderId || !order.orderNo) {
|
||||
Taro.showToast({
|
||||
@@ -447,6 +422,11 @@ function OrderList(props: OrderListProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payingOrderId === order.orderId) {
|
||||
return;
|
||||
}
|
||||
setPayingOrderId(order.orderId);
|
||||
|
||||
// 检查订单是否已过期
|
||||
if (order.createTime && isPaymentExpired(order.createTime)) {
|
||||
Taro.showToast({
|
||||
@@ -475,11 +455,34 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
Taro.showLoading({title: '发起支付...'});
|
||||
|
||||
// 构建商品数据
|
||||
const goodsItems = order.orderGoods?.map(goods => ({
|
||||
goodsId: goods.goodsId,
|
||||
quantity: goods.totalNum || 1
|
||||
})) || [];
|
||||
// 构建商品数据:优先使用列表已加载的商品信息;缺失时再补拉一次,避免goodsItems为空导致后端拒绝/再次支付失败
|
||||
let orderGoods = order.orderGoodsList || [];
|
||||
if (!orderGoods.length) {
|
||||
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,
|
||||
goodsItems: goodsItems,
|
||||
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);
|
||||
@@ -506,6 +515,7 @@ function OrderList(props: OrderListProps) {
|
||||
}
|
||||
|
||||
// 调用微信支付
|
||||
try {
|
||||
await Taro.requestPayment({
|
||||
timeStamp: result.timeStamp,
|
||||
nonceStr: result.nonceStr,
|
||||
@@ -513,6 +523,18 @@ function OrderList(props: OrderListProps) {
|
||||
signType: (result.signType || 'MD5') as 'MD5' | 'HMAC-SHA256',
|
||||
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({
|
||||
@@ -533,13 +555,14 @@ function OrderList(props: OrderListProps) {
|
||||
console.error('支付失败:', error);
|
||||
|
||||
let errorMessage = '支付失败,请重试';
|
||||
if (error.message) {
|
||||
if (error.message.includes('cancel')) {
|
||||
const rawMsg: string = error?.errMsg || error?.message || '';
|
||||
if (rawMsg) {
|
||||
if (rawMsg.includes('cancel')) {
|
||||
errorMessage = '用户取消支付';
|
||||
} else if (error.message.includes('余额不足')) {
|
||||
} else if (rawMsg.includes('余额不足')) {
|
||||
errorMessage = '账户余额不足';
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
errorMessage = rawMsg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,13 +572,13 @@ function OrderList(props: OrderListProps) {
|
||||
});
|
||||
} finally {
|
||||
Taro.hideLoading();
|
||||
setPayingOrderId(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
void reload(true); // 首次加载或tab切换时重置页码
|
||||
}, [tapIndex]); // 只监听tapIndex变化,避免reload依赖循环
|
||||
void reload(true); // 首次加载、tab切换或搜索条件变化时重置页码
|
||||
}, [reload]);
|
||||
|
||||
// 监听外部statusFilter变化,同步更新tab索引
|
||||
useEffect(() => {
|
||||
@@ -705,8 +728,8 @@ function OrderList(props: OrderListProps) {
|
||||
|
||||
{/* 商品信息 */}
|
||||
<View className={'goods-info'}>
|
||||
{item.orderGoods && item.orderGoods.length > 0 ? (
|
||||
item.orderGoods.map((goods, goodsIndex) => (
|
||||
{item.orderGoodsList && item.orderGoodsList.length > 0 ? (
|
||||
item.orderGoodsList.map((goods, goodsIndex) => (
|
||||
<View key={goodsIndex} className={'flex items-center mb-2'}>
|
||||
<Image
|
||||
src={goods.image || '/default-goods.png'}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Space, NavBar, Button, Input} from '@nutui/nutui-react-taro'
|
||||
import {Search, Filter, ArrowLeft} from '@nutui/icons-react-taro'
|
||||
import {View} from '@tarojs/components';
|
||||
import OrderList from "./components/OrderList";
|
||||
import {useRouter} from '@tarojs/taro'
|
||||
import {useDidShow, useRouter} from '@tarojs/taro'
|
||||
import {ShopOrderParam} from "@/api/shop/shopOrder/model";
|
||||
import './order.scss'
|
||||
|
||||
@@ -72,6 +72,17 @@ function Order() {
|
||||
reload().then()
|
||||
}, []);
|
||||
|
||||
// 页面从其它页面返回/重新展示时,刷新一次列表数据
|
||||
// 典型场景:微信支付取消后返回到待支付列表,需要重新拉取订单/商品信息,避免使用旧数据再次支付失败
|
||||
useDidShow(() => {
|
||||
const statusFilter =
|
||||
params.statusFilter != undefined && params.statusFilter !== ''
|
||||
? parseInt(params.statusFilter)
|
||||
: -1;
|
||||
// 同步路由上的statusFilter,并触发子组件重新拉取列表
|
||||
setSearchParams(prev => ({ ...prev, statusFilter }));
|
||||
});
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
<View style={{height: `${statusBarHeight || 0}px`, backgroundColor: '#ffffff'}}></View>
|
||||
|
||||
Reference in New Issue
Block a user