forked from gxwebsoft/mp-10550
feat(rider): 新增水票核销功能
- 添加水票核销扫码页面,支持扫描加密和明文二维码 - 实现水票验证逻辑,包括余额检查和核销确认 - 添加核销记录展示,最多保留最近10条记录 - 在骑手端界面增加水票核销入口 - 新增获取用户水票总数的API接口 - 优化首页轮播图加载,增加缓存和懒加载机制 - 添加门店选择功能,支持订单确认页切换门店 - 修复物流信息类型安全问题 - 更新用户中心门店相关文案显示
This commit is contained in:
@@ -103,3 +103,19 @@ export async function getGltUserTicket(id: number) {
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的水票总数
|
||||
*/
|
||||
export async function getMyGltUserTicketTotal() {
|
||||
const res = await request.get<ApiResult<number | { total?: number }>>(
|
||||
'/glt/glt-user-ticket/my-total'
|
||||
);
|
||||
if (res.code === 0) {
|
||||
const data: any = res.data;
|
||||
if (typeof data === 'number') return data;
|
||||
if (data && typeof data.total === 'number') return data.total;
|
||||
return 0;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@ export default {
|
||||
"root": "rider",
|
||||
"pages": [
|
||||
"index",
|
||||
"orders/index"
|
||||
"orders/index",
|
||||
"ticket/verification/index"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ import {Image} from '@nutui/nutui-react-taro'
|
||||
import {getCmsAdByCode} from "@/api/cms/cmsAd";
|
||||
import navTo from "@/utils/common";
|
||||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||||
import Taro from '@tarojs/taro'
|
||||
|
||||
type AdImage = {
|
||||
url?: string
|
||||
@@ -41,41 +42,45 @@ const MyPage = () => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
// const [disableSwiper, setDisableSwiper] = useState(false)
|
||||
|
||||
const CACHE_KEY = 'home_banner_mp-ad'
|
||||
|
||||
// 用于记录触摸开始位置
|
||||
// const touchStartRef = useRef({x: 0, y: 0})
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
const loadData = async (opts?: {silent?: boolean}) => {
|
||||
if (!opts?.silent) setLoading(true)
|
||||
try {
|
||||
const [flashRes] = await Promise.allSettled([
|
||||
getCmsAdByCode('mp-ad'),
|
||||
getCmsAdByCode('hot_today'),
|
||||
pageCmsArticle({ limit: 1, recommend: 1 }),
|
||||
])
|
||||
|
||||
if (flashRes.status === 'fulfilled') {
|
||||
console.log('flashflashflash', flashRes.value)
|
||||
setCarouselData(flashRes.value)
|
||||
} else {
|
||||
console.error('Failed to fetch flash:', flashRes.reason)
|
||||
}
|
||||
|
||||
// 只阻塞 banner 自己的数据;其他数据预热不应影响首屏展示速度
|
||||
const flash = await getCmsAdByCode('mp-ad')
|
||||
setCarouselData(flash)
|
||||
void Taro.setStorage({ key: CACHE_KEY, data: flash }).catch(() => {})
|
||||
} catch (error) {
|
||||
console.error('Banner数据加载失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
if (!opts?.silent) setLoading(false)
|
||||
}
|
||||
|
||||
// 后台预热(不阻塞 banner 渲染)
|
||||
void getCmsAdByCode('hot_today').catch(() => {})
|
||||
void pageCmsArticle({ limit: 1, recommend: 1 }).catch(() => {})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData().then()
|
||||
const cached = Taro.getStorageSync(CACHE_KEY) as CmsAd | undefined
|
||||
// 有缓存则先渲染缓存,避免首屏等待;再静默刷新
|
||||
if (cached && normalizeAdImages(cached).length) {
|
||||
setCarouselData(cached)
|
||||
setLoading(false)
|
||||
void loadData({ silent: true })
|
||||
return
|
||||
}
|
||||
void loadData()
|
||||
}, [])
|
||||
|
||||
// 轮播图高度,默认300px
|
||||
const carouselHeight = toNumberPx(carouselData?.height, 300)
|
||||
const carouselImages = normalizeAdImages(carouselData)
|
||||
console.log(carouselImages,'carouselImages')
|
||||
|
||||
// 骨架屏组件
|
||||
const BannerSkeleton = () => (
|
||||
@@ -149,7 +154,8 @@ const MyPage = () => {
|
||||
src={src}
|
||||
mode={'scaleToFill'}
|
||||
onClick={() => (img.path ? navTo(`${img.path}`) : undefined)}
|
||||
lazyLoad={false}
|
||||
// 首张图优先加载,其余按需懒加载,避免并发图片请求拖慢首屏可见
|
||||
lazyLoad={index !== 0}
|
||||
style={{
|
||||
height: `${carouselHeight}px`,
|
||||
borderRadius: '4px',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Header from './Header'
|
||||
import Banner from './Banner'
|
||||
import Taro, { useShareAppMessage } from '@tarojs/taro'
|
||||
import Taro, { useDidShow, useShareAppMessage } from '@tarojs/taro'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||
import { Cart, Coupon, Gift, Ticket } from '@nutui/icons-react-taro'
|
||||
@@ -8,11 +8,13 @@ import { getShopInfo } from '@/api/layout'
|
||||
import { checkAndHandleInviteRelation, hasPendingInvite } from '@/utils/invite'
|
||||
import { pageShopGoods } from '@/api/shop/shopGoods'
|
||||
import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model'
|
||||
import { getMyGltUserTicketTotal } from '@/api/glt/gltUserTicket'
|
||||
import './index.scss'
|
||||
|
||||
function Home() {
|
||||
const [activeTabKey, setActiveTabKey] = useState('recommend')
|
||||
const [goodsList, setGoodsList] = useState<ShopGoods[]>([])
|
||||
const [ticketTotal, setTicketTotal] = useState(0)
|
||||
|
||||
useShareAppMessage(() => {
|
||||
// 获取当前用户ID,用于生成邀请链接
|
||||
@@ -87,9 +89,24 @@ function Home() {
|
||||
// const handleTabsStickyChange = (isSticky: boolean) => {}
|
||||
|
||||
const reload = () => {
|
||||
|
||||
const token = Taro.getStorageSync('access_token')
|
||||
if (!token) {
|
||||
setTicketTotal(0)
|
||||
return
|
||||
}
|
||||
getMyGltUserTicketTotal()
|
||||
.then((total) => setTicketTotal(typeof total === 'number' ? total : 0))
|
||||
.catch((err) => {
|
||||
console.error('首页水票总数加载失败:', err)
|
||||
setTicketTotal(0)
|
||||
})
|
||||
};
|
||||
|
||||
// 回到首页/首次进入时都刷新一次(避免依赖 scope.userInfo 导致不触发 reload)
|
||||
useDidShow(() => {
|
||||
reload()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// 获取站点信息
|
||||
getShopInfo().then(() => {
|
||||
@@ -132,7 +149,6 @@ function Home() {
|
||||
if (res.authSetting['scope.userInfo']) {
|
||||
// 用户已经授权过,可以直接获取用户信息
|
||||
console.log('用户已经授权过,可以直接获取用户信息')
|
||||
reload();
|
||||
} else {
|
||||
// 用户未授权,需要弹出授权窗口
|
||||
console.log('用户未授权,需要弹出授权窗口')
|
||||
@@ -227,7 +243,7 @@ function Home() {
|
||||
<View className="ticket-card__head">
|
||||
<Text className="ticket-card__title">电子水票</Text>
|
||||
<Text className="ticket-card__count">
|
||||
您还有 <Text className="ticket-card__countNum">0</Text> 张水票
|
||||
您还有 <Text className="ticket-card__countNum">{ticketTotal}</Text> 张水票
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const IsDealer = () => {
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Reward className={'text-orange-100 '} size={16}/>
|
||||
<Text style={{fontSize: '16px'}}
|
||||
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '门店入驻'}</Text>
|
||||
className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '门店中心'}</Text>
|
||||
{/*<Text className={'text-white opacity-80 pl-3'}>门店核销</Text>*/}
|
||||
</View>
|
||||
}
|
||||
@@ -75,7 +75,7 @@ const IsDealer = () => {
|
||||
title={
|
||||
<View style={{display: 'inline-flex', alignItems: 'center'}}>
|
||||
<Reward className={'text-orange-100 '} size={16}/>
|
||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '门店入驻'}</Text>
|
||||
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}>{config?.vipText || '门店中心'}</Text>
|
||||
<Text className={'text-white opacity-80 pl-3'}>{config?.vipComments || ''}</Text>
|
||||
</View>
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
Dongdong,
|
||||
ArrowRight,
|
||||
Purse,
|
||||
People
|
||||
People,
|
||||
Scan
|
||||
} from '@nutui/icons-react-taro'
|
||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
||||
import { useThemeStyles } from '@/hooks/useTheme'
|
||||
@@ -240,6 +241,14 @@ const DealerIndex: React.FC = () => {
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
|
||||
<Grid.Item text={'水票核销'} onClick={() => navigateToPage('/rider/ticket/verification/index')}>
|
||||
<View className="text-center">
|
||||
<View className="w-12 h-12 bg-cyan-50 rounded-xl flex items-center justify-center mx-auto mb-2">
|
||||
<Scan color="#06b6d4" size="20" />
|
||||
</View>
|
||||
</View>
|
||||
</Grid.Item>
|
||||
</Grid>
|
||||
|
||||
{/* 第二行功能 */}
|
||||
|
||||
4
src/rider/ticket/verification/index.config.ts
Normal file
4
src/rider/ticket/verification/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '水票核销'
|
||||
})
|
||||
|
||||
253
src/rider/ticket/verification/index.tsx
Normal file
253
src/rider/ticket/verification/index.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { Button, Card, ConfigProvider } from '@nutui/nutui-react-taro'
|
||||
import { Scan, Success, Failure, Tips } from '@nutui/icons-react-taro'
|
||||
|
||||
import { decryptQrData } from '@/api/shop/shopGift'
|
||||
import { getGltUserTicket, updateGltUserTicket } from '@/api/glt/gltUserTicket'
|
||||
import type { GltUserTicket } from '@/api/glt/gltUserTicket/model'
|
||||
import { isValidJSON } from '@/utils/jsonUtils'
|
||||
import { useUser } from '@/hooks/useUser'
|
||||
|
||||
type TicketPayload = {
|
||||
userTicketId: number
|
||||
qty?: number
|
||||
userId?: number
|
||||
t?: number
|
||||
}
|
||||
|
||||
type VerifyRecord = {
|
||||
id: number
|
||||
time: string
|
||||
success: boolean
|
||||
message: string
|
||||
ticketName?: string
|
||||
userInfo?: string
|
||||
qty?: number
|
||||
}
|
||||
|
||||
const RiderTicketVerificationPage: React.FC = () => {
|
||||
const { hasRole, isAdmin } = useUser()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [lastTicket, setLastTicket] = useState<GltUserTicket | null>(null)
|
||||
const [lastQty, setLastQty] = useState<number>(1)
|
||||
const [records, setRecords] = useState<VerifyRecord[]>([])
|
||||
|
||||
const canVerify = useMemo(() => {
|
||||
return (
|
||||
hasRole('rider') ||
|
||||
hasRole('store') ||
|
||||
hasRole('staff') ||
|
||||
hasRole('admin') ||
|
||||
isAdmin()
|
||||
)
|
||||
}, [hasRole, isAdmin])
|
||||
|
||||
const addRecord = (rec: Omit<VerifyRecord, 'id' | 'time'>) => {
|
||||
const item: VerifyRecord = {
|
||||
id: Date.now(),
|
||||
time: new Date().toLocaleString(),
|
||||
...rec
|
||||
}
|
||||
setRecords(prev => [item, ...prev].slice(0, 10))
|
||||
}
|
||||
|
||||
const parsePayload = (raw: string): TicketPayload => {
|
||||
const trimmed = raw.trim()
|
||||
if (!isValidJSON(trimmed)) throw new Error('无效的水票核销信息')
|
||||
const payload = JSON.parse(trimmed) as TicketPayload
|
||||
const userTicketId = Number(payload.userTicketId)
|
||||
const qty = Math.max(1, Number(payload.qty || 1))
|
||||
if (!Number.isFinite(userTicketId) || userTicketId <= 0) {
|
||||
throw new Error('水票核销信息无效')
|
||||
}
|
||||
return { ...payload, userTicketId, qty }
|
||||
}
|
||||
|
||||
const extractPayloadFromScanResult = async (scanResult: string): Promise<TicketPayload> => {
|
||||
const trimmed = scanResult.trim()
|
||||
|
||||
// 1) 加密二维码:{ businessType, token, data }
|
||||
if (isValidJSON(trimmed)) {
|
||||
const json = JSON.parse(trimmed) as any
|
||||
if (json?.businessType && json?.token && json?.data) {
|
||||
if (json.businessType !== 'ticket') {
|
||||
throw new Error('请扫描水票核销码')
|
||||
}
|
||||
const decrypted = await decryptQrData({
|
||||
token: String(json.token),
|
||||
encryptedData: String(json.data)
|
||||
})
|
||||
return parsePayload(String(decrypted || ''))
|
||||
}
|
||||
|
||||
// 2) 明文 payload(内部调试/非加密二维码)
|
||||
if (json?.userTicketId) {
|
||||
return parsePayload(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('无效的水票核销码')
|
||||
}
|
||||
|
||||
const verifyTicket = async (payload: TicketPayload) => {
|
||||
const userTicketId = Number(payload.userTicketId)
|
||||
const qty = Math.max(1, Number(payload.qty || 1))
|
||||
|
||||
const ticket = await getGltUserTicket(userTicketId)
|
||||
if (!ticket) throw new Error('水票不存在')
|
||||
if (ticket.status === 1) throw new Error('该水票已冻结')
|
||||
const available = Number(ticket.availableQty || 0)
|
||||
const used = Number(ticket.usedQty || 0)
|
||||
if (available < qty) throw new Error('水票可用次数不足')
|
||||
|
||||
const lines: string[] = []
|
||||
lines.push(`水票:${ticket.templateName || '水票'}`)
|
||||
lines.push(`本次核销:${qty} 次`)
|
||||
lines.push(`剩余可用:${available - qty} 次`)
|
||||
if (ticket.phone) lines.push(`用户手机号:${ticket.phone}`)
|
||||
if (ticket.nickname) lines.push(`用户昵称:${ticket.nickname}`)
|
||||
|
||||
const modalRes = await Taro.showModal({
|
||||
title: '确认核销',
|
||||
content: lines.join('\n')
|
||||
})
|
||||
if (!modalRes.confirm) return
|
||||
|
||||
await updateGltUserTicket({
|
||||
...ticket,
|
||||
availableQty: available - qty,
|
||||
usedQty: used + qty
|
||||
})
|
||||
|
||||
setLastTicket({
|
||||
...ticket,
|
||||
availableQty: available - qty,
|
||||
usedQty: used + qty
|
||||
})
|
||||
setLastQty(qty)
|
||||
|
||||
addRecord({
|
||||
success: true,
|
||||
message: `核销成功(${qty}次)`,
|
||||
ticketName: ticket.templateName || '水票',
|
||||
userInfo: [ticket.nickname, ticket.phone].filter(Boolean).join(' / ') || undefined,
|
||||
qty
|
||||
})
|
||||
Taro.showToast({ title: '核销成功', icon: 'success' })
|
||||
}
|
||||
|
||||
const handleScan = async () => {
|
||||
if (loading) return
|
||||
if (!canVerify) {
|
||||
Taro.showToast({ title: '您没有核销权限', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await Taro.scanCode({})
|
||||
const scanResult = res?.result
|
||||
if (!scanResult) throw new Error('未识别到二维码内容')
|
||||
|
||||
const payload = await extractPayloadFromScanResult(scanResult)
|
||||
await verifyTicket(payload)
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || '核销失败'
|
||||
addRecord({ success: false, message: msg })
|
||||
Taro.showToast({ title: msg, icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<View className="min-h-screen bg-gray-50 p-4">
|
||||
<Card>
|
||||
<View className="flex items-center justify-between">
|
||||
<View>
|
||||
<Text className="text-base font-bold text-gray-800">水票核销</Text>
|
||||
<View className="mt-1">
|
||||
<Text className="text-xs text-gray-500">
|
||||
扫描用户出示的“水票核销码”完成核销
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Tips className="text-gray-400" size="18" />
|
||||
</View>
|
||||
|
||||
<View className="mt-4">
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
loading={loading}
|
||||
icon={<Scan />}
|
||||
onClick={handleScan}
|
||||
>
|
||||
扫码核销
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{lastTicket && (
|
||||
<View className="mt-4 bg-gray-50 rounded-lg p-3">
|
||||
<View className="flex items-center justify-between">
|
||||
<Text className="text-sm text-gray-700">最近一次核销</Text>
|
||||
<Text className="text-xs text-gray-500">使用 {lastQty} 次</Text>
|
||||
</View>
|
||||
<View className="mt-2">
|
||||
<Text className="text-sm text-gray-900">
|
||||
{lastTicket.templateName || '水票'}(剩余 {lastTicket.availableQty ?? 0} 次)
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<View className="mt-4">
|
||||
<View className="mb-2">
|
||||
<Text className="text-sm font-semibold text-gray-800">核销记录</Text>
|
||||
<Text className="text-xs text-gray-500 ml-2">仅保留最近10条</Text>
|
||||
</View>
|
||||
{records.length === 0 ? (
|
||||
<View className="bg-white rounded-lg p-4">
|
||||
<Text className="text-sm text-gray-500">暂无记录</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View className="space-y-2">
|
||||
{records.map(r => (
|
||||
<View key={r.id} className="bg-white rounded-lg p-3 flex items-start justify-between">
|
||||
<View className="flex-1 pr-3">
|
||||
<View className="flex items-center">
|
||||
{r.success ? (
|
||||
<Success className="text-green-500 mr-2" size="16" />
|
||||
) : (
|
||||
<Failure className="text-red-500 mr-2" size="16" />
|
||||
)}
|
||||
<Text className="text-sm text-gray-900">{r.message}</Text>
|
||||
</View>
|
||||
<View className="mt-1">
|
||||
<Text className="text-xs text-gray-500">
|
||||
{r.time}
|
||||
{r.ticketName ? ` · ${r.ticketName}` : ''}
|
||||
{typeof r.qty === 'number' ? ` · ${r.qty}次` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
{r.userInfo && (
|
||||
<View className="mt-1">
|
||||
<Text className="text-xs text-gray-500">{r.userInfo}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default RiderTicketVerificationPage
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
InputNumber,
|
||||
ConfigProvider
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import {Location, ArrowRight} from '@nutui/icons-react-taro'
|
||||
import {Location, ArrowRight, Shop} from '@nutui/icons-react-taro'
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
||||
import {getShopGoods} from "@/api/shop/shopGoods";
|
||||
@@ -37,6 +37,9 @@ import {
|
||||
filterUnusableCoupons
|
||||
} from "@/utils/couponUtils";
|
||||
import navTo from "@/utils/common";
|
||||
import type {ShopStore} from "@/api/shop/shopStore/model";
|
||||
import {getShopStore, listShopStore} from "@/api/shop/shopStore";
|
||||
import {getSelectedStoreFromStorage, saveSelectedStoreToStorage} from "@/utils/storeSelection";
|
||||
|
||||
|
||||
const OrderConfirm = () => {
|
||||
@@ -67,9 +70,37 @@ const OrderConfirm = () => {
|
||||
const [availableCoupons, setAvailableCoupons] = useState<CouponCardProps[]>([])
|
||||
const [couponLoading, setCouponLoading] = useState<boolean>(false)
|
||||
|
||||
// 门店选择:用于在下单页展示当前“已选门店”,并允许用户切换(写入 SelectedStore Storage)
|
||||
const [storePopupVisible, setStorePopupVisible] = useState(false)
|
||||
const [stores, setStores] = useState<ShopStore[]>([])
|
||||
const [storeLoading, setStoreLoading] = useState(false)
|
||||
const [selectedStore, setSelectedStore] = useState<ShopStore | null>(getSelectedStoreFromStorage())
|
||||
|
||||
const router = Taro.getCurrentInstance().router;
|
||||
const goodsId = router?.params?.goodsId;
|
||||
|
||||
const loadStores = async () => {
|
||||
if (storeLoading) return
|
||||
try {
|
||||
setStoreLoading(true)
|
||||
const list = await listShopStore()
|
||||
setStores((list || []).filter(s => s?.isDelete !== 1))
|
||||
} catch (e) {
|
||||
console.error('获取门店列表失败:', e)
|
||||
setStores([])
|
||||
Taro.showToast({title: '获取门店列表失败', icon: 'none'})
|
||||
} finally {
|
||||
setStoreLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const openStorePopup = async () => {
|
||||
setStorePopupVisible(true)
|
||||
if (!stores.length) {
|
||||
await loadStores()
|
||||
}
|
||||
}
|
||||
|
||||
// 计算商品总价
|
||||
const getGoodsTotal = () => {
|
||||
if (!goods) return 0
|
||||
@@ -561,6 +592,8 @@ const OrderConfirm = () => {
|
||||
}
|
||||
|
||||
useDidShow(() => {
|
||||
// 返回/切换到该页面时,刷新一下当前已选门店
|
||||
setSelectedStore(getSelectedStoreFromStorage())
|
||||
loadAllData()
|
||||
})
|
||||
|
||||
@@ -623,6 +656,26 @@ const OrderConfirm = () => {
|
||||
)}
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup>
|
||||
<Cell
|
||||
title={(
|
||||
<View className="flex items-center gap-2">
|
||||
<Shop className={'text-gray-500'}/>
|
||||
<Text>门店</Text>
|
||||
</View>
|
||||
)}
|
||||
extra={(
|
||||
<View className={'flex items-center gap-2'}>
|
||||
<View className={'text-gray-900'}>
|
||||
{selectedStore?.name || '请选择门店'}
|
||||
</View>
|
||||
<ArrowRight className={'text-gray-400'} size={14}/>
|
||||
</View>
|
||||
)}
|
||||
onClick={openStorePopup}
|
||||
/>
|
||||
</CellGroup>
|
||||
|
||||
<CellGroup>
|
||||
<Cell key={goods.goodsId}>
|
||||
<View className={'flex w-full justify-between gap-3'}>
|
||||
@@ -816,6 +869,63 @@ const OrderConfirm = () => {
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
{/* 门店选择弹窗 */}
|
||||
<Popup
|
||||
visible={storePopupVisible}
|
||||
position="bottom"
|
||||
style={{height: '70vh'}}
|
||||
onClose={() => setStorePopupVisible(false)}
|
||||
>
|
||||
<View className="p-4">
|
||||
<View className="flex justify-between items-center mb-3">
|
||||
<Text className="text-base font-medium">选择门店</Text>
|
||||
<Text
|
||||
className="text-sm text-gray-500"
|
||||
onClick={() => setStorePopupVisible(false)}
|
||||
>
|
||||
关闭
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{storeLoading ? (
|
||||
<View className="py-10 text-center text-gray-500">
|
||||
<Text>加载中...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<CellGroup>
|
||||
{stores.map((s) => {
|
||||
const isActive = !!selectedStore?.id && selectedStore.id === s.id
|
||||
return (
|
||||
<Cell
|
||||
key={s.id}
|
||||
title={<Text className={isActive ? 'text-green-600' : ''}>{s.name || `门店${s.id}`}</Text>}
|
||||
description={s.address || ''}
|
||||
onClick={async () => {
|
||||
let storeToSave: ShopStore = s
|
||||
if (s?.id) {
|
||||
try {
|
||||
const full = await getShopStore(s.id)
|
||||
if (full) storeToSave = full
|
||||
} catch (_e) {
|
||||
// keep base item
|
||||
}
|
||||
}
|
||||
setSelectedStore(storeToSave)
|
||||
saveSelectedStoreToStorage(storeToSave)
|
||||
setStorePopupVisible(false)
|
||||
Taro.showToast({title: '门店已切换', icon: 'success'})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{!stores.length && (
|
||||
<Cell title={<Text className="text-gray-500">暂无门店数据</Text>} />
|
||||
)}
|
||||
</CellGroup>
|
||||
)}
|
||||
</View>
|
||||
</Popup>
|
||||
|
||||
<Gap height={50}/>
|
||||
|
||||
<div className={'fixed z-50 bg-white w-full bottom-0 left-0 pt-4 pb-10 border-t border-gray-200'}>
|
||||
|
||||
@@ -337,12 +337,12 @@ function OrderList(props: OrderListProps) {
|
||||
};
|
||||
|
||||
// 查看物流 (待收货状态)
|
||||
const viewLogistics = (order: ShopOrder) => {
|
||||
// 跳转到物流查询页面
|
||||
Taro.navigateTo({
|
||||
url: `/user/order/logistics/index?orderId=${order.orderId}&orderNo=${order.orderNo}&expressNo=${order.transactionId || ''}&expressCompany=SF`
|
||||
});
|
||||
};
|
||||
// const viewLogistics = (order: ShopOrder) => {
|
||||
// // 跳转到物流查询页面
|
||||
// Taro.navigateTo({
|
||||
// url: `/user/order/logistics/index?orderId=${order.orderId}&orderNo=${order.orderNo}&expressNo=${order.transactionId || ''}&expressCompany=SF`
|
||||
// });
|
||||
// };
|
||||
|
||||
// 再次购买 (已完成状态)
|
||||
const buyAgain = (order: ShopOrder) => {
|
||||
@@ -822,10 +822,10 @@ function OrderList(props: OrderListProps) {
|
||||
{/* 待收货状态:显示查看物流和确认收货 */}
|
||||
{item.deliveryStatus === 20 && (!item.riderId || !!item.sendEndTime) && item.orderStatus !== 2 && item.orderStatus !== 6 && (
|
||||
<Space>
|
||||
<Button size={'small'} onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
viewLogistics(item);
|
||||
}}>查看物流</Button>
|
||||
{/*<Button size={'small'} onClick={(e) => {*/}
|
||||
{/* e.stopPropagation();*/}
|
||||
{/* viewLogistics(item);*/}
|
||||
{/*}}>查看物流</Button>*/}
|
||||
<Button size={'small'} type="primary" onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
confirmReceive(item);
|
||||
|
||||
@@ -173,7 +173,7 @@ const LogisticsPage: React.FC = () => {
|
||||
<View className="logistics-header">
|
||||
<View className="express-info">
|
||||
<Text className="company-name">
|
||||
{EXPRESS_COMPANIES[logisticsInfo.expressCompany] || logisticsInfo.expressCompany}
|
||||
{EXPRESS_COMPANIES[logisticsInfo.expressCompany as keyof typeof EXPRESS_COMPANIES] || logisticsInfo.expressCompany}
|
||||
</Text>
|
||||
<Text className="express-no">{logisticsInfo.expressNo}</Text>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user