```
feat(registration): 优化经销商注册流程并增加地址定位功能 - 修改导航栏标题从“邀请注册”为“注册成为会员” - 修复重复提交问题并移除不必要的submitting状态 - 增加昵称和头像的必填验证提示 - 添加用户角色缺失时的默认角色写入机制 - 集成地图选点功能,支持经纬度获取和地址解析 - 实现微信地址导入功能,自动填充基本信息 - 增加定位权限检查和错误处理机制 - 添加.gitignore规则忽略备份文件夹src__bak - 移除已废弃的银行卡和客户管理页面代码 - 优化表单验证规则和错误提示信息 - 实现经销商注册成功后自动跳转到“我的”页面 - 添加用户信息缓存刷新机制确保角色信息同步 ```
This commit is contained in:
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
304
src/rider/index.tsx
Normal file
304
src/rider/index.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
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,
|
||||
Scan
|
||||
} 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.Item text={'水票核销'} onClick={() => navigateToPage('/rider/ticket/verification/index?auto=1')}>
|
||||
<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>
|
||||
|
||||
{/* 第二行功能 */}
|
||||
{/*<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
|
||||
4
src/rider/orders/index.config.ts
Normal file
4
src/rider/orders/index.config.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
navigationBarTitleText: '送水订单',
|
||||
navigationBarTextStyle: 'black'
|
||||
}
|
||||
610
src/rider/orders/index.tsx
Normal file
610
src/rider/orders/index.tsx
Normal file
@@ -0,0 +1,610 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import {
|
||||
Tabs,
|
||||
TabPane,
|
||||
Cell,
|
||||
Space,
|
||||
Button,
|
||||
Dialog,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Image,
|
||||
Empty,
|
||||
InfiniteLoading,
|
||||
PullToRefresh,
|
||||
Loading
|
||||
} from '@nutui/nutui-react-taro'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import dayjs from 'dayjs'
|
||||
import { pageGltTicketOrder, updateGltTicketOrder } from '@/api/glt/gltTicketOrder'
|
||||
import type { GltTicketOrder, GltTicketOrderParam } from '@/api/glt/gltTicketOrder/model'
|
||||
import { uploadFile } from '@/api/system/file'
|
||||
|
||||
export default function RiderOrders() {
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
const riderId = useMemo(() => {
|
||||
const v = Number(Taro.getStorageSync('UserId'))
|
||||
return Number.isFinite(v) && v > 0 ? v : undefined
|
||||
}, [])
|
||||
|
||||
const pageRef = useRef(1)
|
||||
const listRef = useRef<GltTicketOrder[]>([])
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
const [list, setList] = useState<GltTicketOrder[]>([])
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [deliverDialogVisible, setDeliverDialogVisible] = useState(false)
|
||||
const [deliverSubmitting, setDeliverSubmitting] = useState(false)
|
||||
const [deliverOrder, setDeliverOrder] = useState<GltTicketOrder | null>(null)
|
||||
const [deliverImg, setDeliverImg] = useState<string | undefined>(undefined)
|
||||
|
||||
type DeliverConfirmMode = 'photoComplete' | 'waitCustomerConfirm'
|
||||
const [deliverConfirmMode, setDeliverConfirmMode] = useState<DeliverConfirmMode>('photoComplete')
|
||||
|
||||
const riderTabs = useMemo(
|
||||
() => [
|
||||
{ index: 0, title: '全部' },
|
||||
{ index: 1, title: '待配送', deliveryStatus: 10 },
|
||||
{ index: 2, title: '配送中', deliveryStatus: 20 },
|
||||
{ index: 3, title: '待确认', deliveryStatus: 30 },
|
||||
{ index: 4, title: '已完成', deliveryStatus: 40 }
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const getOrderStatusText = (order: GltTicketOrder) => {
|
||||
if (order.status === 1) return '已冻结'
|
||||
|
||||
const deliveryStatus = order.deliveryStatus
|
||||
if (deliveryStatus === 40) return '已完成'
|
||||
if (deliveryStatus === 30) return '待客户确认'
|
||||
if (deliveryStatus === 20) return '配送中'
|
||||
if (deliveryStatus === 10) return '待配送'
|
||||
|
||||
// 兼容:如果后端暂未下发 deliveryStatus,就用时间字段推断
|
||||
if (order.receiveConfirmTime) return '已完成'
|
||||
if (order.sendEndTime) return '待客户确认'
|
||||
if (order.sendStartTime) return '配送中'
|
||||
if (order.riderId) return '待配送'
|
||||
return '待派单'
|
||||
}
|
||||
|
||||
const getOrderStatusColor = (order: GltTicketOrder) => {
|
||||
const text = getOrderStatusText(order)
|
||||
if (text === '已完成') return 'text-green-600'
|
||||
if (text === '待客户确认') return 'text-purple-600'
|
||||
if (text === '配送中') return 'text-blue-600'
|
||||
if (text === '待配送') return 'text-amber-600'
|
||||
if (text === '已冻结') return 'text-orange-600'
|
||||
return 'text-gray-500'
|
||||
}
|
||||
|
||||
const canStartDeliver = (order: GltTicketOrder) => {
|
||||
if (!order.id) return false
|
||||
if (order.status === 1) return false
|
||||
if (!riderId || order.riderId !== riderId) return false
|
||||
if (order.deliveryStatus && order.deliveryStatus !== 10) return false
|
||||
return !order.sendStartTime && !order.sendEndTime
|
||||
}
|
||||
|
||||
const canConfirmDelivered = (order: GltTicketOrder) => {
|
||||
if (!order.id) return false
|
||||
if (order.status === 1) return false
|
||||
if (!riderId || order.riderId !== riderId) return false
|
||||
if (order.receiveConfirmTime) return false
|
||||
if (order.deliveryStatus === 40) return false
|
||||
if (order.sendEndTime) return false
|
||||
|
||||
// 只允许在“配送中”阶段确认送达
|
||||
if (typeof order.deliveryStatus === 'number') return order.deliveryStatus === 20
|
||||
return !!order.sendStartTime
|
||||
}
|
||||
|
||||
const canCompleteByPhoto = (order: GltTicketOrder) => {
|
||||
if (!order.id) return false
|
||||
if (order.status === 1) return false
|
||||
if (!riderId || order.riderId !== riderId) return false
|
||||
if (order.receiveConfirmTime) return false
|
||||
if (order.deliveryStatus === 40) return false
|
||||
// 已送达但未完成:允许补传照片并直接完成
|
||||
return !!order.sendEndTime
|
||||
}
|
||||
|
||||
const filterByTab = useCallback(
|
||||
(orders: GltTicketOrder[]) => {
|
||||
if (tabIndex === 0) return orders
|
||||
|
||||
const current = riderTabs.find(t => t.index === tabIndex)
|
||||
const status = current?.deliveryStatus
|
||||
if (!status) return orders
|
||||
|
||||
// 如果后端已实现 deliveryStatus 筛选,这里基本不会再过滤;否则用兼容逻辑兜底。
|
||||
return orders.filter(o => {
|
||||
const ds = o.deliveryStatus
|
||||
if (typeof ds === 'number') return ds === status
|
||||
if (status === 10) return !!o.riderId && !o.sendStartTime && !o.sendEndTime
|
||||
if (status === 20) return !!o.sendStartTime && !o.sendEndTime
|
||||
if (status === 30) return !!o.sendEndTime && !o.receiveConfirmTime
|
||||
if (status === 40) return !!o.receiveConfirmTime
|
||||
return true
|
||||
})
|
||||
},
|
||||
[riderTabs, tabIndex]
|
||||
)
|
||||
|
||||
const reload = useCallback(
|
||||
async (resetPage = false) => {
|
||||
if (!riderId) return
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const currentPage = resetPage ? 1 : pageRef.current
|
||||
const currentTab = riderTabs.find(t => t.index === tabIndex)
|
||||
const params: GltTicketOrderParam = {
|
||||
page: currentPage,
|
||||
limit: PAGE_SIZE,
|
||||
riderId,
|
||||
deliveryStatus: currentTab?.deliveryStatus
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await pageGltTicketOrder(params as any)
|
||||
const incomingAll = (res?.list || []) as GltTicketOrder[]
|
||||
|
||||
// 兼容:后端若暂未实现 riderId 过滤,前端兜底过滤掉非本人的订单
|
||||
const incoming = incomingAll.filter(o => o?.deleted !== 1 && o?.riderId === riderId)
|
||||
|
||||
const prev = resetPage ? [] : listRef.current
|
||||
const next = resetPage ? incoming : prev.concat(incoming)
|
||||
listRef.current = next
|
||||
setList(next)
|
||||
|
||||
const total = typeof res?.count === 'number' ? res.count : undefined
|
||||
const filteredOut = incomingAll.length - incoming.length
|
||||
if (typeof total === 'number' && filteredOut === 0) {
|
||||
setHasMore(next.length < total)
|
||||
} else {
|
||||
setHasMore(incomingAll.length >= PAGE_SIZE)
|
||||
}
|
||||
|
||||
pageRef.current = currentPage + 1
|
||||
} catch (e) {
|
||||
console.error('加载配送订单失败:', e)
|
||||
setError('加载失败,请重试')
|
||||
setHasMore(false)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[PAGE_SIZE, loading, riderId, riderTabs, tabIndex]
|
||||
)
|
||||
|
||||
const reloadMore = useCallback(async () => {
|
||||
if (loading || !hasMore) return
|
||||
await reload(false)
|
||||
}, [hasMore, loading, reload])
|
||||
|
||||
const openDeliverDialog = (order: GltTicketOrder, opts?: { mode?: DeliverConfirmMode }) => {
|
||||
setDeliverOrder(order)
|
||||
setDeliverImg(order.sendEndImg)
|
||||
setDeliverConfirmMode(opts?.mode || (order.sendEndImg ? 'photoComplete' : 'waitCustomerConfirm'))
|
||||
setDeliverDialogVisible(true)
|
||||
}
|
||||
|
||||
const handleChooseDeliverImg = async () => {
|
||||
try {
|
||||
const file = await uploadFile()
|
||||
setDeliverImg(file?.url)
|
||||
} catch (e) {
|
||||
console.error('上传送达照片失败:', e)
|
||||
Taro.showToast({ title: '上传失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartDeliver = async (order: GltTicketOrder) => {
|
||||
if (!order?.id) return
|
||||
if (!canStartDeliver(order)) return
|
||||
try {
|
||||
await updateGltTicketOrder({
|
||||
id: order.id,
|
||||
deliveryStatus: 20,
|
||||
sendStartTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
})
|
||||
Taro.showToast({ title: '已开始配送', icon: 'success' })
|
||||
pageRef.current = 1
|
||||
listRef.current = []
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
await reload(true)
|
||||
} catch (e) {
|
||||
console.error('开始配送失败:', e)
|
||||
Taro.showToast({ title: '开始配送失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirmDelivered = async () => {
|
||||
if (!deliverOrder?.id) return
|
||||
if (deliverSubmitting) return
|
||||
if (deliverConfirmMode === 'photoComplete' && !deliverImg) {
|
||||
Taro.showToast({ title: '请先拍照/上传送达照片', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setDeliverSubmitting(true)
|
||||
try {
|
||||
const now = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
||||
// 送达时间:首次“确认送达”写入;补传照片时不要覆盖原送达时间
|
||||
const deliveredAt = deliverOrder.sendEndTime || now
|
||||
// - waitCustomerConfirm:只标记“已送达”,进入待客户确认
|
||||
// - photoComplete:拍照留档后可直接完成(是否允许由后端策略决定)
|
||||
const payload: GltTicketOrder =
|
||||
deliverConfirmMode === 'photoComplete'
|
||||
? {
|
||||
id: deliverOrder.id,
|
||||
deliveryStatus: 40,
|
||||
sendEndTime: deliveredAt,
|
||||
sendEndImg: deliverImg,
|
||||
receiveConfirmTime: now,
|
||||
receiveConfirmType: 20
|
||||
}
|
||||
: {
|
||||
id: deliverOrder.id,
|
||||
deliveryStatus: 30,
|
||||
sendEndTime: deliveredAt,
|
||||
sendEndImg: deliverImg
|
||||
}
|
||||
|
||||
await updateGltTicketOrder(payload)
|
||||
|
||||
Taro.showToast({ title: '已确认送达', icon: 'success' })
|
||||
setDeliverDialogVisible(false)
|
||||
setDeliverOrder(null)
|
||||
setDeliverImg(undefined)
|
||||
setDeliverConfirmMode('photoComplete')
|
||||
pageRef.current = 1
|
||||
listRef.current = []
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
await reload(true)
|
||||
} catch (e) {
|
||||
console.error('确认送达失败:', e)
|
||||
Taro.showToast({ title: '确认送达失败', icon: 'none' })
|
||||
} finally {
|
||||
setDeliverSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
listRef.current = list
|
||||
}, [list])
|
||||
|
||||
useDidShow(() => {
|
||||
pageRef.current = 1
|
||||
listRef.current = []
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
void reload(true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
pageRef.current = 1
|
||||
listRef.current = []
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
void reload(true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tabIndex, riderId])
|
||||
|
||||
if (!riderId) {
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen p-4">
|
||||
<Text>请先登录</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const displayList = filterByTab(list)
|
||||
|
||||
return (
|
||||
<View className="bg-gray-50 min-h-screen">
|
||||
|
||||
<View>
|
||||
<Tabs
|
||||
align="left"
|
||||
className="fixed left-0"
|
||||
style={{zIndex: 998, borderBottom: '1px solid #e5e5e5'}}
|
||||
tabStyle={{backgroundColor: '#ffffff'}}
|
||||
value={tabIndex}
|
||||
onChange={(paneKey) => setTabIndex(Number(paneKey))}
|
||||
>
|
||||
{riderTabs.map(t => (
|
||||
<TabPane key={t.index} title={loading && tabIndex === t.index ? `${t.title}...` : t.title}></TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
<PullToRefresh
|
||||
onRefresh={async () => {
|
||||
pageRef.current = 1
|
||||
listRef.current = []
|
||||
setList([])
|
||||
setHasMore(true)
|
||||
await reload(true)
|
||||
}}
|
||||
headHeight={60}
|
||||
>
|
||||
<View
|
||||
style={{ height: '84vh', width: '100%', padding: '0', overflowY: 'auto', overflowX: 'hidden' }}
|
||||
id="rider-order-scroll"
|
||||
>
|
||||
{error ? (
|
||||
<View className="flex flex-col items-center justify-center h-64">
|
||||
<Text className="text-gray-500 mb-4">{error}</Text>
|
||||
<Button size="small" type="primary" onClick={() => reload(true)}>
|
||||
重新加载
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<InfiniteLoading
|
||||
target="rider-order-scroll"
|
||||
hasMore={hasMore}
|
||||
onLoadMore={reloadMore}
|
||||
loadingText={
|
||||
<View className="flex justify-center items-center py-4">
|
||||
<Loading />
|
||||
<View className="ml-2">加载中...</View>
|
||||
</View>
|
||||
}
|
||||
loadMoreText={
|
||||
displayList.length === 0 ? (
|
||||
<Empty style={{ backgroundColor: 'transparent' }} description="暂无配送订单" />
|
||||
) : (
|
||||
<View className="h-24 text-center text-gray-500">没有更多了</View>
|
||||
)
|
||||
}
|
||||
>
|
||||
{displayList.map(o => {
|
||||
const timeText = o.createTime ? dayjs(o.createTime).format('YYYY-MM-DD HH:mm') : '-'
|
||||
const addr = o.address || (o.addressId ? `地址ID:${o.addressId}` : '-')
|
||||
const remark = o.buyerRemarks || o.comments || ''
|
||||
const qty = Number(o.totalNum || 0)
|
||||
|
||||
const flow1Done = !!o.riderId
|
||||
const flow2Done = !!o.sendStartTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 20)
|
||||
const flow3Done = !!o.sendEndTime || (typeof o.deliveryStatus === 'number' && o.deliveryStatus >= 30)
|
||||
const flow4Done = !!o.receiveConfirmTime || o.deliveryStatus === 40
|
||||
|
||||
const phoneToCall = o.phone
|
||||
const storePhone = o.storePhone
|
||||
const pickupName = o.warehouseName || o.storeName
|
||||
const pickupAddr = o.warehouseAddress || o.storeAddress
|
||||
|
||||
return (
|
||||
<Cell key={String(o.id)} style={{ padding: '16px' }}>
|
||||
<View className="w-full">
|
||||
<View className="flex justify-between items-center">
|
||||
<Text className="text-gray-800 font-bold text-sm">
|
||||
{o.userTicketId ? `票号#${o.userTicketId}` : '送水订单'}
|
||||
</Text>
|
||||
<Text className={`${getOrderStatusColor(o)} text-sm font-medium`}>{getOrderStatusText(o)}</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-gray-400 text-xs mt-1">下单时间:{timeText}</View>
|
||||
|
||||
<View className="mt-3 bg-white rounded-lg">
|
||||
<View className="text-sm text-gray-700">
|
||||
<Text className="text-gray-500">客户:</Text>
|
||||
<Text>{o.nickname || '-'} {o.phone ? `(${o.phone})` : ''}</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">收货地址:</Text>
|
||||
<Text>{addr}</Text>
|
||||
</View>
|
||||
{!!remark && (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">买家留言:</Text>
|
||||
<Text>{remark}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">数量:</Text>
|
||||
<Text>{Number.isFinite(qty) ? qty : '-'}</Text>
|
||||
<Text className="text-gray-500 ml-3">金额:</Text>
|
||||
<Text>¥{o.price || '-'}</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">配送时间:</Text>
|
||||
<Text>{o.sendTime ? dayjs(o.sendTime).format('YYYY-MM-DD HH:mm') : '-'}</Text>
|
||||
</View>
|
||||
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">取水点:</Text>
|
||||
<Text>{pickupName || '-'}</Text>
|
||||
</View>
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">取水地址:</Text>
|
||||
<Text>{pickupAddr || '-'}</Text>
|
||||
</View>
|
||||
|
||||
{!!o.sendStartTime && (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">开始配送:</Text>
|
||||
<Text>{dayjs(o.sendStartTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||
</View>
|
||||
)}
|
||||
{!!o.sendEndTime && (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">送达时间:</Text>
|
||||
<Text>{dayjs(o.sendEndTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||
</View>
|
||||
)}
|
||||
{!!o.receiveConfirmTime && (
|
||||
<View className="text-sm text-gray-700 mt-1">
|
||||
<Text className="text-gray-500">完成时间:</Text>
|
||||
<Text>{dayjs(o.receiveConfirmTime).format('YYYY-MM-DD HH:mm')}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{o.sendEndImg ? (
|
||||
<View className="text-sm text-gray-700 mt-2">
|
||||
<Text className="text-gray-500">送达照片:</Text>
|
||||
<View className="mt-2">
|
||||
<Image src={o.sendEndImg} width="100%" height="120" />
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* 配送流程 */}
|
||||
<View className="mt-3 bg-gray-50 rounded-lg p-2 text-xs">
|
||||
<Text className="text-gray-600">流程:</Text>
|
||||
<Text className={flow1Done ? 'text-green-600 font-medium' : 'text-gray-400'}>1 派单</Text>
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow2Done ? 'text-blue-600 font-medium' : 'text-gray-400'}>2 配送中</Text>
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow3Done ? 'text-purple-600 font-medium' : 'text-gray-400'}>3 送达留档</Text>
|
||||
<Text className="mx-1 text-gray-400">{'>'}</Text>
|
||||
<Text className={flow4Done ? 'text-green-600 font-medium' : 'text-gray-400'}>4 完成</Text>
|
||||
</View>
|
||||
|
||||
<View className="mt-3 flex justify-end">
|
||||
<Space>
|
||||
{!!phoneToCall && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
Taro.makePhoneCall({ phoneNumber: phoneToCall })
|
||||
}}
|
||||
>
|
||||
联系客户
|
||||
</Button>
|
||||
)}
|
||||
{!!addr && addr !== '-' && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
void Taro.setClipboardData({ data: addr })
|
||||
Taro.showToast({ title: '地址已复制', icon: 'none' })
|
||||
}}
|
||||
>
|
||||
复制地址
|
||||
</Button>
|
||||
)}
|
||||
{!!storePhone && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
Taro.makePhoneCall({ phoneNumber: storePhone })
|
||||
}}
|
||||
>
|
||||
联系门店
|
||||
</Button>
|
||||
)}
|
||||
{canStartDeliver(o) && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
void handleStartDeliver(o)
|
||||
}}
|
||||
>
|
||||
开始配送
|
||||
</Button>
|
||||
)}
|
||||
{canConfirmDelivered(o) && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
openDeliverDialog(o, { mode: 'waitCustomerConfirm' })
|
||||
}}
|
||||
>
|
||||
确认送达
|
||||
</Button>
|
||||
)}
|
||||
{canCompleteByPhoto(o) && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
openDeliverDialog(o, { mode: 'photoComplete' })
|
||||
}}
|
||||
>
|
||||
补传照片完成
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</View>
|
||||
</View>
|
||||
</Cell>
|
||||
)
|
||||
})}
|
||||
</InfiniteLoading>
|
||||
)}
|
||||
</View>
|
||||
</PullToRefresh>
|
||||
</View>
|
||||
|
||||
<Dialog
|
||||
title="确认送达"
|
||||
visible={deliverDialogVisible}
|
||||
confirmText={
|
||||
deliverSubmitting
|
||||
? '提交中...'
|
||||
: deliverConfirmMode === 'photoComplete'
|
||||
? '拍照完成'
|
||||
: '确认送达'
|
||||
}
|
||||
cancelText="取消"
|
||||
onConfirm={handleConfirmDelivered}
|
||||
onCancel={() => {
|
||||
if (deliverSubmitting) return
|
||||
setDeliverDialogVisible(false)
|
||||
setDeliverOrder(null)
|
||||
setDeliverImg(undefined)
|
||||
setDeliverConfirmMode('photoComplete')
|
||||
}}
|
||||
>
|
||||
<View className="text-sm text-gray-700">
|
||||
<View>到达收货点后,可选择“拍照留档直接完成”或“等待客户确认收货”。</View>
|
||||
|
||||
<View className="mt-3">
|
||||
<RadioGroup value={deliverConfirmMode} onChange={v => setDeliverConfirmMode(v as DeliverConfirmMode)}>
|
||||
<Radio value="photoComplete">拍照留档(直接完成)</Radio>
|
||||
<Radio value="waitCustomerConfirm">客户确认收货(可不拍照)</Radio>
|
||||
</RadioGroup>
|
||||
</View>
|
||||
<View className="mt-3">
|
||||
<Button size="small" onClick={handleChooseDeliverImg}>
|
||||
{deliverImg ? '重新拍照/上传' : '拍照/上传'}
|
||||
</Button>
|
||||
</View>
|
||||
{deliverImg && (
|
||||
<View className="mt-3">
|
||||
<Image src={deliverImg} width="100%" height="120" />
|
||||
<View className="mt-2 flex justify-end">
|
||||
<Button size="small" onClick={() => setDeliverImg(undefined)}>
|
||||
移除照片
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View className="mt-3 text-xs text-gray-500">
|
||||
说明:如选择“客户确认收货”,订单进入“待客户确认”;客户在用户端确认收货或超时自动确认(需后端支持)。
|
||||
</View>
|
||||
</View>
|
||||
</Dialog>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
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: '水票核销'
|
||||
})
|
||||
|
||||
280
src/rider/ticket/verification/index.tsx
Normal file
280
src/rider/ticket/verification/index.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro, { useDidShow, useRouter } 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 router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [lastTicket, setLastTicket] = useState<GltUserTicket | null>(null)
|
||||
const [lastQty, setLastQty] = useState<number>(1)
|
||||
const [records, setRecords] = useState<VerifyRecord[]>([])
|
||||
|
||||
const autoScanOnceRef = useRef(false)
|
||||
|
||||
const canVerify = useMemo(() => {
|
||||
return (
|
||||
hasRole('rider') ||
|
||||
hasRole('store') ||
|
||||
hasRole('staff') ||
|
||||
hasRole('admin') ||
|
||||
isAdmin()
|
||||
)
|
||||
}, [hasRole, isAdmin])
|
||||
|
||||
const autoScanEnabled = useMemo(() => {
|
||||
const p: any = router?.params || {}
|
||||
return p.auto === '1' || p.auto === 'true'
|
||||
}, [router])
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// If navigated in "auto" mode, open scan on first show when user has permission.
|
||||
useDidShow(() => {
|
||||
// Reset the flag when user manually re-enters the page via navigation again.
|
||||
// (This runs on every show; only the first show with auto enabled will trigger scan.)
|
||||
if (!autoScanEnabled) autoScanOnceRef.current = false
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoScanEnabled) return
|
||||
if (autoScanOnceRef.current) return
|
||||
if (!canVerify) return
|
||||
autoScanOnceRef.current = true
|
||||
// Defer to ensure page is fully mounted before opening camera.
|
||||
setTimeout(() => {
|
||||
handleScan().catch(() => {})
|
||||
}, 80)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoScanEnabled, canVerify])
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user