diff --git a/src/app.config.ts b/src/app.config.ts index 100a873..776676e 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -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": "用于保存小程序码到相册,方便分享给好友" } } } diff --git a/src/dealer/qrcode/index.tsx b/src/dealer/qrcode/index.tsx index 9cb8706..a676808 100644 --- a/src/dealer/qrcode/index.tsx +++ b/src/dealer/qrcode/index.tsx @@ -11,6 +11,7 @@ import {businessGradients} from '@/styles/gradients' const DealerQrcode: React.FC = () => { const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState('') const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) // const [inviteStats, setInviteStats] = useState(null) // const [statsLoading, setStatsLoading] = useState(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 => { + 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 => { + // 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 = {} + 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 - Taro.showToast({ - title: '保存成功', - icon: 'success' - }) + 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={} onClick={saveMiniProgramCode} - disabled={!miniProgramCodeUrl || loading} + disabled={!miniProgramCodeUrl || loading || saving} > 保存小程序码到相册 diff --git a/src/dealer/withdraw/index.tsx b/src/dealer/withdraw/index.tsx index bd5431e..72ebc5f 100644 --- a/src/dealer/withdraw/index.tsx +++ b/src/dealer/withdraw/index.tsx @@ -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) { diff --git a/src/pages/index/Banner.tsx b/src/pages/index/Banner.tsx index 48aa7c9..f3b04d3 100644 --- a/src/pages/index/Banner.tsx +++ b/src/pages/index/Banner.tsx @@ -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 }), ]) diff --git a/src/pages/user/components/UserGrid.tsx b/src/pages/user/components/UserGrid.tsx index faee87e..5c84262 100644 --- a/src/pages/user/components/UserGrid.tsx +++ b/src/pages/user/components/UserGrid.tsx @@ -49,7 +49,7 @@ const UserCell = () => { border: 'none' } as React.CSSProperties} > - navTo('/user/poster/poster', true)}> + navTo('/rider/index', true)}> diff --git a/src/rider/index.config.ts b/src/rider/index.config.ts new file mode 100644 index 0000000..1293fcb --- /dev/null +++ b/src/rider/index.config.ts @@ -0,0 +1,3 @@ +export default definePageConfig({ + navigationBarTitleText: '配送中心' +}) diff --git a/src/rider/index.scss b/src/rider/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/rider/index.tsx b/src/rider/index.tsx new file mode 100644 index 0000000..0a023fe --- /dev/null +++ b/src/rider/index.tsx @@ -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 ( + + + {error} + + + + ) + } + + return ( + + + {/*头部信息*/} + {dealerUser && ( + + {/* 装饰性背景元素 - 小程序兼容版本 */} + + + + + } + className="mr-4" + style={{ + border: '2px solid rgba(255, 255, 255, 0.3)' + }} + /> + + + {dealerUser?.realName || '分销商'} + + + ID: {dealerUser.userId} + + + + 加入时间 + + {formatTime(dealerUser.createTime)} + + + + + )} + + {/* 佣金统计卡片 */} + {dealerUser && ( + + + 工资统计 + + + + + {formatMoney(dealerUser.money)} + + 工资收入 + + + + {formatMoney(dealerUser.freezeMoney)} + + 桶数 + + + + {formatMoney(dealerUser.totalMoney)} + + 累计收入 + + + + )} + + {/* 团队统计 */} + {dealerUser && ( + + + 我的邀请 + navigateToPage('/dealer/team/index')} + > + 查看详情 + + + + + + + {dealerUser.firstNum || 0} + + 一级成员 + + + + {dealerUser.secondNum || 0} + + 二级成员 + + + + {dealerUser.thirdNum || 0} + + 三级成员 + + + + )} + + {/* 功能导航 */} + + 配送工具 + + + navigateToPage('/rider/orders/index')}> + + + + + + + + navigateToPage('/rider/withdraw/index')}> + + + + + + + + navigateToPage('/rider/team/index')}> + + + + + + + + navigateToPage('/rider/qrcode/index')}> + + + + + + + + + {/* 第二行功能 */} + {/**/} + {/* navigateToPage('/dealer/invite-stats/index')}>*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* /!* 预留其他功能位置 *!/*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/**/} + + + + + {/* 底部安全区域 */} + + + ) +} + +export default DealerIndex diff --git a/src/user/order/components/OrderList.tsx b/src/user/order/components/OrderList.tsx index 4ec73fe..9cab23d 100644 --- a/src/user/order/components/OrderList.tsx +++ b/src/user/order/components/OrderList.tsx @@ -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([]) - const [page, setPage] = useState(1) + const pageRef = useRef(1) const [hasMore, setHasMore] = useState(true) + const [payingOrderId, setPayingOrderId] = useState(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) { // 批量获取订单商品信息,限制并发数量 @@ -214,19 +215,19 @@ function OrderList(props: OrderListProps) { for (let i = 0; i < res.list.length; i += batchSize) { const batch = res.list.slice(i, i + batchSize); - const batchResults = await Promise.all( + const batchResults = await Promise.all( batch.map(async (order) => { try { 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,13 +515,26 @@ function OrderList(props: OrderListProps) { } // 调用微信支付 - await Taro.requestPayment({ - timeStamp: result.timeStamp, - nonceStr: result.nonceStr, - package: result.package, - signType: (result.signType || 'MD5') as 'MD5' | 'HMAC-SHA256', - paySign: result.paySign, - }); + try { + await Taro.requestPayment({ + timeStamp: result.timeStamp, + nonceStr: result.nonceStr, + package: result.package, + 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) { {/* 商品信息 */} - {item.orderGoods && item.orderGoods.length > 0 ? ( - item.orderGoods.map((goods, goodsIndex) => ( + {item.orderGoodsList && item.orderGoodsList.length > 0 ? ( + item.orderGoodsList.map((goods, goodsIndex) => ( { + const statusFilter = + params.statusFilter != undefined && params.statusFilter !== '' + ? parseInt(params.statusFilter) + : -1; + // 同步路由上的statusFilter,并触发子组件重新拉取列表 + setSearchParams(prev => ({ ...prev, statusFilter })); + }); + return (