feat(rider): 新增水票核销功能

- 添加水票核销扫码页面,支持扫描加密和明文二维码
- 实现水票验证逻辑,包括余额检查和核销确认
- 添加核销记录展示,最多保留最近10条记录
- 在骑手端界面增加水票核销入口
- 新增获取用户水票总数的API接口
- 优化首页轮播图加载,增加缓存和懒加载机制
- 添加门店选择功能,支持订单确认页切换门店
- 修复物流信息类型安全问题
- 更新用户中心门店相关文案显示
This commit is contained in:
2026-02-05 01:08:37 +08:00
parent fcbaa970d0
commit 8679b26f74
11 changed files with 454 additions and 39 deletions

View File

@@ -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'}>