- 在“我的钱包”上方新增“门店中心”按钮,仅管理员可见 - 新建门店中心页面,实现订单列表按状态筛选功能 - 支持订单操作:标记已付款、标记已发货、标记已完成 - 操作时必须上传操作凭证图片,支持拍照和相册选择,最多3张 - 凭证图片上传至指定接口,操作成功后刷新订单列表 - 更新 app.config 注册门店中心新页面及对应配置 - 说明订单状态字段及对应数值含义,便于状态判断与展示
383 lines
14 KiB
TypeScript
383 lines
14 KiB
TypeScript
import React, { useState, useEffect } 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'
|
||
|
||
type TabKey = 'unpaid' | 'unshipped' | 'shipped' | 'completed'
|
||
|
||
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
|
||
|
||
type OpType = 'pay' | 'ship' | 'complete'
|
||
|
||
const OP_LABEL: Record<OpType, string> = {
|
||
pay: '已付款',
|
||
ship: '已发货',
|
||
complete: '已完成',
|
||
}
|
||
|
||
/** 上传图片到后端,返回图片 URL */
|
||
const uploadImage = (filePath: string): Promise<string> => {
|
||
return new Promise((resolve, reject) => {
|
||
Taro.uploadFile({
|
||
url: 'https://shop-api.websoft.top/api/oss/upload',
|
||
filePath,
|
||
name: 'file',
|
||
header: { 'content-type': 'application/json', TenantId },
|
||
success: (res) => {
|
||
try {
|
||
const data = JSON.parse(res.data)
|
||
if (data.code === 0 && data.data?.url) {
|
||
resolve(data.data.url)
|
||
} else {
|
||
reject(new Error(data.message || '上传失败'))
|
||
}
|
||
} catch {
|
||
reject(new Error('解析上传响应失败'))
|
||
}
|
||
},
|
||
fail: () => reject(new Error('上传请求失败')),
|
||
})
|
||
})
|
||
}
|
||
|
||
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 [showModal, setShowModal] = useState(false)
|
||
const [currentOrder, setCurrentOrder] = useState<ShopOrder | null>(null)
|
||
const [opType, setOpType] = useState<OpType>('pay')
|
||
const [proofImages, setProofImages] = useState<string[]>([])
|
||
const [submitting, setSubmitting] = useState(false)
|
||
|
||
/** 加载订单列表 */
|
||
const loadOrders = async (tab: TabKey = activeTab, page: number = 1, append = false) => {
|
||
const tabConfig = TABS.find(t => t.key === tab)
|
||
if (!tabConfig) return
|
||
setLoading(true)
|
||
try {
|
||
const res: any = await pageShopOrder({
|
||
...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)
|
||
} catch (e: any) {
|
||
Taro.showToast({ title: e.message || '加载失败', icon: 'none' })
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => { loadOrders(activeTab, 1) }, [activeTab])
|
||
|
||
/** 打开操作弹窗 */
|
||
const openModal = (order: ShopOrder, type: OpType) => {
|
||
setCurrentOrder(order)
|
||
setOpType(type)
|
||
setProofImages([])
|
||
setShowModal(true)
|
||
}
|
||
|
||
/** 关闭弹窗 */
|
||
const closeModal = () => {
|
||
setShowModal(false)
|
||
setCurrentOrder(null)
|
||
setProofImages([])
|
||
}
|
||
|
||
/** 选择凭证图片 */
|
||
const chooseProofImage = () => {
|
||
Taro.chooseImage({
|
||
count: 1,
|
||
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' })
|
||
}
|
||
},
|
||
})
|
||
}
|
||
|
||
/** 移除凭证图片 */
|
||
const removeProofImage = (idx: number) => {
|
||
setProofImages(prev => prev.filter((_, i) => i !== idx))
|
||
}
|
||
|
||
/** 提交操作 */
|
||
const submitOperation = async () => {
|
||
if (!currentOrder) return
|
||
if (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()
|
||
} else if (opType === 'complete') {
|
||
updateData.orderStatus = 1
|
||
}
|
||
|
||
// 将凭证追加到备注
|
||
updateData.comments = (currentOrder.comments || '') + `\n${proofText}`
|
||
|
||
await updateShopOrder(updateData)
|
||
Taro.showToast({ title: '操作成功', icon: 'success' })
|
||
closeModal()
|
||
loadOrders(activeTab, 1)
|
||
} catch (e: any) {
|
||
Taro.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
/** 获取订单可进行的操作 */
|
||
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 renderOrderCard = (order: ShopOrder) => {
|
||
const actions = getOrderActions(order)
|
||
return (
|
||
<View key={order.orderId} className='bg-white rounded-xl mx-3 mt-3 p-4'>
|
||
{/* 订单头部 */}
|
||
<View className='flex justify-between items-center mb-3'>
|
||
<Text className='text-xs text-gray-400'>订单号:{order.orderNo}</Text>
|
||
<Text className='text-xs' style={{ color: getStatusColor(order) }}>
|
||
{getStatusText(order)}
|
||
</Text>
|
||
</View>
|
||
|
||
{/* 商品列表 */}
|
||
{(order as any).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' />
|
||
)}
|
||
<View className='flex-1'>
|
||
<Text className='text-sm text-gray-800 block'>{goods.goodsName}</Text>
|
||
{goods.specInfo && (
|
||
<Text className='text-xs text-gray-400 mt-1 block'>{goods.specInfo}</Text>
|
||
)}
|
||
<View className='flex justify-between items-center mt-1'>
|
||
<Text className='text-sm text-red-500 font-medium'>¥{goods.price}</Text>
|
||
<Text className='text-xs text-gray-400'>x{goods.quantity}</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
))}
|
||
|
||
{/* 收货信息 */}
|
||
{(order.realName || order.phone) && (
|
||
<View className='bg-gray-50 rounded-lg p-3 mb-3'>
|
||
<Text className='text-sm text-gray-700 block'>{order.realName} {order.phone}</Text>
|
||
{order.address && (
|
||
<Text className='text-xs text-gray-400 mt-1 block'>{order.address}</Text>
|
||
)}
|
||
</View>
|
||
)}
|
||
|
||
{/* 金额 */}
|
||
<View className='flex justify-between items-center mb-3'>
|
||
<Text className='text-xs text-gray-400'>
|
||
共{(order as any).orderGoods?.length || 0}件商品
|
||
</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'>
|
||
{actions.map(act => (
|
||
<View
|
||
key={act.type}
|
||
className='px-4 py-2 rounded-lg border border-blue-500'
|
||
onClick={() => openModal(order, act.type)}
|
||
>
|
||
<Text className='text-sm text-blue-500'>{act.label}</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
)}
|
||
</View>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<View className='min-h-full bg-gray-50'>
|
||
{/* Tab 栏 */}
|
||
<View className='bg-white flex'>
|
||
{TABS.map(tab => (
|
||
<View
|
||
key={tab.key}
|
||
className={`flex-1 text-center py-3 border-b-2 ${
|
||
activeTab === tab.key ? 'border-blue-500' : 'border-transparent'
|
||
}`}
|
||
onClick={() => setActiveTab(tab.key as TabKey)}
|
||
>
|
||
<Text className={`text-sm ${activeTab === tab.key ? 'text-blue-500 font-medium' : 'text-gray-500'}`}>
|
||
{tab.label}
|
||
</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
|
||
{/* 订单列表 */}
|
||
<ScrollView
|
||
scrollY
|
||
style={{ height: 'calc(100vh - 100px)' }}
|
||
onScrollToLower={() => {
|
||
if (orderList.length < total && !loading) loadOrders(activeTab, pageNo + 1, true)
|
||
}}
|
||
>
|
||
{loading && orderList.length === 0 ? (
|
||
<View className='flex justify-center items-center py-20'>
|
||
<Text className='text-gray-400'>加载中...</Text>
|
||
</View>
|
||
) : orderList.length === 0 ? (
|
||
<View className='flex justify-center items-center py-20'>
|
||
<Text className='text-gray-400'>暂无订单</Text>
|
||
</View>
|
||
) : (
|
||
orderList.map(renderOrderCard)
|
||
)}
|
||
{loading && orderList.length > 0 && (
|
||
<View className='flex justify-center items-center py-4'>
|
||
<Text className='text-gray-400 text-sm'>加载中...</Text>
|
||
</View>
|
||
)}
|
||
<View className='h-6' />
|
||
</ScrollView>
|
||
|
||
{/* 操作弹窗 */}
|
||
{showModal && (
|
||
<View className='fixed inset-0 z-50 flex items-end justify-center'>
|
||
{/* 遮罩 */}
|
||
<View className='absolute inset-0 bg-black/50' onClick={closeModal} />
|
||
{/* 弹窗内容 */}
|
||
<View className='relative bg-white rounded-t-2xl w-full px-5 pt-6 pb-10'>
|
||
<Text className='text-lg font-medium text-gray-800 text-center mb-5 block'>
|
||
确认{OP_LABEL[opType]}
|
||
</Text>
|
||
|
||
{/* 订单信息 */}
|
||
{currentOrder && (
|
||
<View className='bg-gray-50 rounded-xl p-4 mb-5'>
|
||
<Text className='text-sm text-gray-700 block'>订单号:{currentOrder.orderNo}</Text>
|
||
<Text className='text-sm text-gray-700 mt-1 block'>
|
||
实付金额:<Text className='text-red-500 font-medium'>¥{currentOrder.payPrice || currentOrder.totalPrice}</Text>
|
||
</Text>
|
||
</View>
|
||
)}
|
||
|
||
{/* 凭证上传 */}
|
||
<Text className='text-sm text-gray-600 mb-3 block'>上传操作凭证(必填,最多3张)</Text>
|
||
<View className='flex flex-wrap gap-3 mb-6'>
|
||
{proofImages.map((url, idx) => (
|
||
<View key={idx} className='relative'>
|
||
<Image className='w-20 h-20 rounded-lg bg-gray-50' src={url} mode='aspectFill' />
|
||
<View
|
||
className='absolute -top-2 -right-2 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center'
|
||
onClick={() => removeProofImage(idx)}
|
||
>
|
||
<Text className='text-white text-xs'>×</Text>
|
||
</View>
|
||
</View>
|
||
))}
|
||
{proofImages.length < 3 && (
|
||
<View
|
||
className='w-20 h-20 rounded-lg bg-gray-50 border-2 border-dashed border-gray-200 flex items-center justify-center'
|
||
onClick={chooseProofImage}
|
||
>
|
||
<Text className='text-2xl text-gray-300'>+</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* 操作按钮 */}
|
||
<View className='flex gap-3'>
|
||
<View className='flex-1 py-3 rounded-xl bg-gray-100 text-center' onClick={closeModal}>
|
||
<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}
|
||
>
|
||
{submitting ? (
|
||
<Text className='text-sm text-white'>提交中...</Text>
|
||
) : (
|
||
<Text className='text-sm text-white'>确认{OP_LABEL[opType]}</Text>
|
||
)}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
</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'
|
||
}
|