refactor(store): 重构门店中心页面及简化货到付款流程

- 修订订单状态字段,支持配送完成及送达凭证照片
- 取消注释门店中心菜单项,调整显示位置
- 重写门店中心页面,修复API分页参数及响应格式
- 新增“确认送达”操作,上传送达照片更新订单状态
- 已完成订单展示送达凭证照片
- 采用 useDidShow 生命周期刷新页面数据
- 使用 loadingRef 防止列表并发加载
- 门店中心访问权限判断由 user.isAdmin 改为通过接口检测 storeInfo
- 货到付款模式下简化门店中心页面Tab,仅保留待处理和已完成
- 待处理订单支持确认收款和确认完成两种操作,必需上传送达照片
- 已完成订单只作展示,无操作按钮
- 状态文案调整为适配货到付款流程
- Tab 颜色主题改为绿色
This commit is contained in:
2026-07-01 15:56:31 +08:00
parent 61e9879b5d
commit 87e5261a2a
605 changed files with 261 additions and 58569 deletions

View File

@@ -1,32 +1,40 @@
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useCallback, useRef } from 'react'
import { View, Text, Image, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { pageShopOrder, updateShopOrder } from '@/api/shop/shopOrder'
import type { ShopOrder } from '@/api/shop/shopOrder/model'
import { TenantId } from '../../../../config/app'
import Taro, { useDidShow } from '@tarojs/taro'
import { pageShopOrder, updateShopOrder, removeShopOrder } from '@/api/shop/shopOrder'
import type { ShopOrder, ShopOrderParam } from '@/api/shop/shopOrder/model'
import { TenantId } from '../../../config/app'
type TabKey = 'unpaid' | 'unshipped' | 'shipped' | 'completed'
definePageConfig({
navigationBarTitleText: '门店中心',
})
const TABS = [
{ key: 'unpaid', label: '待付款', params: { payStatus: 0 } },
{ key: 'unshipped', label: '待发货', params: { payStatus: 1, deliveryStatus: 10 } },
{ key: 'shipped', label: '待收货', params: { payStatus: 1, deliveryStatus: 20 } },
{ key: 'completed', label: '已完成', params: { orderStatus: 1, payStatus: 1 } },
] as const
// ─── Tab 配置(货到付款模式:待处理 + 已完成)─────────────────────
type TabKey = 'pending' | 'completed'
type OpType = 'pay' | 'ship' | 'complete'
const TABS: { key: TabKey; label: string; params: Partial<ShopOrderParam> }[] = [
{ key: 'pending', label: '待处理', params: { statusFilter: 8 } },
{ key: 'completed', label: '已完成', params: { orderStatus: 1 } },
]
// ─── 操作类型 ─────────────────────────────────────────────────────
type OpType = 'pay' | 'complete'
const OP_LABEL: Record<OpType, string> = {
pay: '已款',
ship: '已发货',
pay: '已款',
complete: '已完成',
}
/** 上传图片到后端,返回图片 URL */
const OP_DESC: Record<OpType, string> = {
pay: '变更支付状态为已付款',
complete: '同时变更支付状态为已付款、发货状态为已完成、订单状态为已完成',
}
// ─── 图片上传 ─────────────────────────────────────────────────────
const uploadImage = (filePath: string): Promise<string> => {
return new Promise((resolve, reject) => {
Taro.uploadFile({
url: 'https://shop-api.websoft.top/api/oss/upload',
url: 'https://server.websoft.top/api/oss/upload',
filePath,
name: 'file',
header: { 'content-type': 'application/json', TenantId },
@@ -47,45 +55,118 @@ const uploadImage = (filePath: string): Promise<string> => {
})
}
// ─── 状态辅助函数 ──────────────────────────────────────────────────
/** 格式化日期为 Spring Boot LocalDateTime 可接受的格式yyyy-MM-dd HH:mm:ss */
function formatDateTime(date: Date): string {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const h = String(date.getHours()).padStart(2, '0')
const mi = String(date.getMinutes()).padStart(2, '0')
const s = String(date.getSeconds()).padStart(2, '0')
return `${y}-${m}-${d} ${h}:${mi}:${s}`
}
function getStatusText(order: ShopOrder): string {
if (order.orderStatus === 2) return '待处理'
if (order.orderStatus === 1) return '已完成'
if (order.payStatus === false) return '待收款'
if (order.payStatus && order.deliveryStatus === 10) return '待发货'
if (order.payStatus && order.deliveryStatus === 20) return '待收货'
return '待处理'
}
function getStatusColor(order: ShopOrder): string {
if (order.orderStatus === 2) return '#999'
if (order.orderStatus === 1) return '#0e932e'
if (order.payStatus === false) return '#ee0a24'
if (order.payStatus) return '#ff7d00'
return '#999'
}
/** 获取待处理订单可执行的操作 */
function getOrderActions(order: ShopOrder): { label: string; type: OpType }[] {
const actions: { label: string; type: OpType }[] = []
// 未付款 → 可确认收款,也可直接确认完成
if (order.payStatus === false) {
actions.push({ label: '确认收款', type: 'pay' })
actions.push({ label: '确认完成', type: 'complete' })
}
// 已付款但未完成 → 可确认完成
if (order.payStatus && order.orderStatus !== 1 && order.orderStatus !== 2) {
actions.push({ label: '确认完成', type: 'complete' })
}
return actions
}
// ─── 页面组件 ──────────────────────────────────────────────────────
export default function StoreCenterPage() {
const [activeTab, setActiveTab] = useState<TabKey>('unshipped')
const [orderList, setOrderList] = useState<ShopOrder[]>([])
const [total, setTotal] = useState(0)
const [pageNo, setPageNo] = useState(1)
const [loading, setLoading] = useState(false)
const pageSize = 10
const [activeTab, setActiveTab] = useState<TabKey>('pending')
// 订单列表与分页
const [orderList, setOrderList] = useState<ShopOrder[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(false)
// 操作弹窗状态
const [showModal, setShowModal] = useState(false)
const [showModal, setShowModal] = useState(false)
const [currentOrder, setCurrentOrder] = useState<ShopOrder | null>(null)
const [opType, setOpType] = useState<OpType>('pay')
const [opType, setOpType] = useState<OpType>('pay')
const [proofImages, setProofImages] = useState<string[]>([])
const [submitting, setSubmitting] = useState(false)
const pageSize = 10
const loadingRef = useRef(false)
/** 加载订单列表 */
const loadOrders = async (tab: TabKey = activeTab, page: number = 1, append = false) => {
const tabConfig = TABS.find(t => t.key === tab)
if (!tabConfig) return
const loadOrders = useCallback(async (tab: TabKey, pageNo: number = 1, append = false) => {
if (loadingRef.current) return
loadingRef.current = true
setLoading(true)
try {
const res: any = await pageShopOrder({
const tabConfig = TABS.find(t => t.key === tab)
if (!tabConfig) return
const params: ShopOrderParam = {
...tabConfig.params,
pageNo,
pageSize,
} as any)
const records: ShopOrder[] = res?.records || res?.data?.records || []
const totalCount: number = res?.total || res?.data?.total || 0
setOrderList(append ? prev => [...prev, ...records] : records)
setTotal(totalCount)
setPageNo(page)
page: pageNo,
limit: pageSize,
}
const res = await pageShopOrder(params)
const list = res?.list || []
setOrderList(prev => append ? [...prev, ...list] : list)
setPage(pageNo)
setHasMore(list.length >= pageSize)
} catch (e: any) {
Taro.showToast({ title: e.message || '加载失败', icon: 'none' })
if (!append) setOrderList([])
} finally {
loadingRef.current = false
setLoading(false)
}
}
}, [])
useEffect(() => { loadOrders(activeTab, 1) }, [activeTab])
// 切换 tab 时重新加载
useEffect(() => {
loadOrders(activeTab, 1)
}, [activeTab, loadOrders])
// 页面重新显示时刷新
useDidShow(() => {
loadOrders(activeTab, 1)
})
/** 加载更多 */
const handleLoadMore = () => {
if (hasMore && !loadingRef.current) {
loadOrders(activeTab, page + 1, true)
}
}
/** 打开操作弹窗 */
const openModal = (order: ShopOrder, type: OpType) => {
@@ -102,21 +183,27 @@ export default function StoreCenterPage() {
setProofImages([])
}
/** 选择凭证图片 */
/** 选择并上传凭证图片 */
const chooseProofImage = () => {
const maxCount = 3
const remaining = maxCount - proofImages.length
if (remaining <= 0) return
Taro.chooseImage({
count: 1,
count: remaining,
sizeType: ['compressed'],
sourceType: ['camera', 'album'],
success: async (res) => {
try {
Taro.showLoading({ title: '上传中...' })
const url = await uploadImage(res.tempFilePaths[0])
Taro.hideLoading()
setProofImages(prev => [...prev, url])
} catch (e: any) {
Taro.hideLoading()
Taro.showToast({ title: e.message || '上传失败', icon: 'none' })
for (const filePath of res.tempFilePaths) {
try {
Taro.showLoading({ title: '上传中...' })
const url = await uploadImage(filePath)
Taro.hideLoading()
setProofImages(prev => [...prev, url])
} catch (e: any) {
Taro.hideLoading()
Taro.showToast({ title: e.message || '上传失败', icon: 'none' })
}
}
},
})
@@ -130,32 +217,40 @@ export default function StoreCenterPage() {
/** 提交操作 */
const submitOperation = async () => {
if (!currentOrder) return
if (proofImages.length === 0) {
Taro.showToast({ title: '请上传操作凭证', icon: 'none' })
// 确认完成必须上传凭证照片
if (opType === 'complete' && proofImages.length === 0) {
Taro.showToast({ title: '请上传配送凭证照片', icon: 'none' })
return
}
setSubmitting(true)
try {
const proofText = `【门店操作凭证】${OP_LABEL[opType]}${proofImages.join(',')}`
const updateData: any = { orderId: currentOrder.orderId }
if (opType === 'pay') {
// 确认收款:变更支付状态为已付款
updateData.payStatus = true
updateData.payTime = new Date().toISOString()
} else if (opType === 'ship') {
updateData.deliveryStatus = 20
updateData.deliveryTime = new Date().toISOString()
updateData.payTime = formatDateTime(new Date())
} else if (opType === 'complete') {
updateData.orderStatus = 1
// 确认完成:变更支付状态为已付款、发货状态为已完成、订单状态为已完成
updateData.payStatus = true
updateData.payTime = formatDateTime(new Date())
updateData.deliveryStatus = 20 // 发货状态 → 已完成
updateData.orderStatus = 1 // 订单状态 → 已完成
updateData.sendEndImg = proofImages.join(',') // 配送员送达拍照
}
// 凭证追加到备注
updateData.comments = (currentOrder.comments || '') + `\n${proofText}`
// 收款操作如有凭证追加到备注
if (opType === 'pay' && proofImages.length > 0) {
const proofText = `【门店收款凭证】:${proofImages.join(',')}`
updateData.comments = (currentOrder.comments || '') + `\n${proofText}`
}
await updateShopOrder(updateData)
Taro.showToast({ title: '操作成功', icon: 'success' })
closeModal()
loadOrders(activeTab, 1)
loadOrders(activeTab, 1) // 刷新列表
} catch (e: any) {
Taro.showToast({ title: e.message || '操作失败', icon: 'none' })
} finally {
@@ -163,23 +258,33 @@ export default function StoreCenterPage() {
}
}
/** 获取订单可进行的操作 */
const getOrderActions = (order: ShopOrder) => {
const actions: { label: string; type: OpType }[] = []
if (!order.payStatus) {
actions.push({ label: '标记已付款', type: 'pay' })
}
if (order.payStatus && order.deliveryStatus === 10) {
actions.push({ label: '标记已发货', type: 'ship' })
}
if (order.payStatus && order.deliveryStatus === 20 && order.orderStatus === 0) {
actions.push({ label: '标记已完成', type: 'complete' })
}
return actions
/** 删除订单 */
const handleDelete = (order: ShopOrder) => {
Taro.showModal({
title: '提示',
content: `确定要删除订单 ${order.orderNo} 吗?删除后无法恢复。`,
confirmColor: '#ee0a24',
success: async (res) => {
if (!res.confirm) return
try {
Taro.showLoading({ title: '删除中...' })
await removeShopOrder(order.orderId)
Taro.hideLoading()
Taro.showToast({ title: '删除成功', icon: 'success' })
loadOrders(activeTab, 1)
} catch (e: any) {
Taro.hideLoading()
Taro.showToast({ title: e.message || '删除失败', icon: 'none' })
}
},
})
}
/** 渲染单个订单卡片 */
const renderOrderCard = (order: ShopOrder) => {
const actions = getOrderActions(order)
const actions = activeTab === 'pending' ? getOrderActions(order) : []
const orderGoods = (order as any).orderGoods || []
return (
<View key={order.orderId} className='bg-white rounded-xl mx-3 mt-3 p-4'>
{/* 订单头部 */}
@@ -191,7 +296,7 @@ export default function StoreCenterPage() {
</View>
{/* 商品列表 */}
{(order as any).orderGoods?.map((goods: any, idx: number) => (
{orderGoods.map((goods: any, idx: number) => (
<View key={idx} className='flex items-center gap-3 mb-3'>
{goods.coverImage && (
<Image className='w-16 h-16 rounded-lg bg-gray-50' src={goods.coverImage} mode='aspectFill' />
@@ -219,30 +324,56 @@ export default function StoreCenterPage() {
</View>
)}
{/* 金额 */}
{/* 送达凭证(已完成订单) */}
{order.sendEndImg && (
<View className='bg-gray-50 rounded-lg p-3 mb-3'>
<Text className='text-xs text-gray-500 mb-2 block'></Text>
<View className='flex flex-wrap gap-2'>
{order.sendEndImg.split(',').map((img, i) => (
<Image key={i} className='w-16 h-16 rounded-lg' src={img} mode='aspectFill' />
))}
</View>
</View>
)}
{/* 金额 + 支付方式 */}
<View className='flex justify-between items-center mb-3'>
<Text className='text-xs text-gray-400'>
{(order as any).orderGoods?.length || 0}
{order.payType === 0 ? '余额支付' : order.payType === 4 ? '现金支付' : '货到付款'}
</Text>
<Text className='text-sm text-gray-700'>
<Text className='text-red-500 font-medium'>¥{order.payPrice || order.totalPrice}</Text>
</Text>
</View>
{/* 操作按钮 */}
{actions.length > 0 && (
<View className='flex justify-end gap-2 border-t border-gray-50 pt-3'>
{/* 操作按钮(仅待处理订单) */}
<View className='flex justify-between items-center border-t border-gray-50 pt-3'>
{/* 删除按钮 */}
<View
className='px-3 py-1.5'
onClick={() => handleDelete(order)}
>
<Text className='text-xs text-gray-400'></Text>
</View>
<View className='flex gap-2'>
{actions.map(act => (
<View
key={act.type}
className='px-4 py-2 rounded-lg border border-blue-500'
className={`px-4 py-2 rounded-lg ${
act.type === 'complete'
? 'bg-green-500'
: 'border border-blue-500'
}`}
onClick={() => openModal(order, act.type)}
>
<Text className='text-sm text-blue-500'>{act.label}</Text>
<Text className={`text-sm ${
act.type === 'complete' ? 'text-white' : 'text-blue-500'
}`}>{act.label}</Text>
</View>
))}
</View>
)}
</View>
</View>
)
}
@@ -255,11 +386,11 @@ export default function StoreCenterPage() {
<View
key={tab.key}
className={`flex-1 text-center py-3 border-b-2 ${
activeTab === tab.key ? 'border-blue-500' : 'border-transparent'
activeTab === tab.key ? 'border-green-500' : 'border-transparent'
}`}
onClick={() => setActiveTab(tab.key as TabKey)}
onClick={() => setActiveTab(tab.key)}
>
<Text className={`text-sm ${activeTab === tab.key ? 'text-blue-500 font-medium' : 'text-gray-500'}`}>
<Text className={`text-sm ${activeTab === tab.key ? 'text-green-500 font-medium' : 'text-gray-500'}`}>
{tab.label}
</Text>
</View>
@@ -269,10 +400,9 @@ export default function StoreCenterPage() {
{/* 订单列表 */}
<ScrollView
scrollY
style={{ height: 'calc(100vh - 100px)' }}
onScrollToLower={() => {
if (orderList.length < total && !loading) loadOrders(activeTab, pageNo + 1, true)
}}
style={{ height: 'calc(100vh - 50px)' }}
onScrollToLower={handleLoadMore}
lowerThreshold={100}
>
{loading && orderList.length === 0 ? (
<View className='flex justify-center items-center py-20'>
@@ -290,6 +420,11 @@ export default function StoreCenterPage() {
<Text className='text-gray-400 text-sm'>...</Text>
</View>
)}
{!hasMore && orderList.length > 0 && (
<View className='flex justify-center items-center py-4'>
<Text className='text-gray-300 text-xs'></Text>
</View>
)}
<View className='h-6' />
</ScrollView>
@@ -311,11 +446,16 @@ export default function StoreCenterPage() {
<Text className='text-sm text-gray-700 mt-1 block'>
<Text className='text-red-500 font-medium'>¥{currentOrder.payPrice || currentOrder.totalPrice}</Text>
</Text>
<Text className='text-xs text-gray-500 mt-2 block'>
{OP_DESC[opType]}
</Text>
</View>
)}
{/* 凭证上传 */}
<Text className='text-sm text-gray-600 mb-3 block'>3</Text>
<Text className='text-sm text-gray-600 mb-3 block'>
{opType === 'complete' ? '必填配送货物到达后拍照上传最多3张' : '选填最多3张'}
</Text>
<View className='flex flex-wrap gap-3 mb-6'>
{proofImages.map((url, idx) => (
<View key={idx} className='relative'>
@@ -344,8 +484,10 @@ export default function StoreCenterPage() {
<Text className='text-sm text-gray-600'></Text>
</View>
<View
className='flex-1 py-3 rounded-xl bg-blue-500 text-center flex items-center justify-center'
onClick={submitOperation}
className={`flex-1 py-3 rounded-xl text-center flex items-center justify-center ${
opType === 'complete' ? 'bg-green-500' : 'bg-blue-500'
}`}
onClick={submitting ? undefined : submitOperation}
>
{submitting ? (
<Text className='text-sm text-white'>...</Text>
@@ -360,23 +502,3 @@ export default function StoreCenterPage() {
</View>
)
}
/** 获取订单状态文字 */
function getStatusText(order: ShopOrder): string {
if (order.payStatus === false) return '待付款'
if (order.orderStatus === 2) return '已取消'
if (order.deliveryStatus === 10) return '待发货'
if (order.deliveryStatus === 20) return '待收货'
if (order.orderStatus === 1) return '已完成'
return '处理中'
}
/** 获取订单状态颜色 */
function getStatusColor(order: ShopOrder): string {
if (order.payStatus === false) return '#ee0a24'
if (order.orderStatus === 2) return '#999'
if (order.deliveryStatus === 10) return '#ff7d00'
if (order.deliveryStatus === 20) return '#4b9cf5'
if (order.orderStatus === 1) return '#0e932e'
return '#999'
}

View File

@@ -53,11 +53,8 @@ const UserPage: React.FC = () => {
})
const menuItems = [
// { icon: '🏆', label: '赛事活动', url: '/pages/event/my/index' },
// { icon: '📅', label: '预约穿线', url: '/pages/booking/list/index' },
// { icon: '🎫', label: '优惠券', url: '/pages/user/coupon-list' },
// 门店中心:仅管理员显示
...(user?.isAdmin ? [{ icon: '🏪', label: '门店中心', url: '/pages/store/center/index' }] : []),
// 门店中心:仅门店店员/店长显示(通过 /shop/shop-store-user/my 判断)
...(storeInfo ? [{ icon: '🏪', label: '门店中心', url: '/pages/store/center/index' }] : []),
{ icon: '💰', label: '我的钱包', url: '/pages/user/wallet' },
{ icon: '📍', label: '收货地址', url: '/pages/user/address-list' },
// { icon: '⭐', label: '积分明细', url: '/pages/user/points-record' },