Files
xinlong-shop-taro/src/pages/store/center/index.tsx
赵忠林 61e9879b5d feat(store): 实现门店中心功能及订单管理
- 在“我的钱包”上方新增“门店中心”按钮,仅管理员可见
- 新建门店中心页面,实现订单列表按状态筛选功能
- 支持订单操作:标记已付款、标记已发货、标记已完成
- 操作时必须上传操作凭证图片,支持拍照和相册选择,最多3张
- 凭证图片上传至指定接口,操作成功后刷新订单列表
- 更新 app.config 注册门店中心新页面及对应配置
- 说明订单状态字段及对应数值含义,便于状态判断与展示
2026-07-01 13:18:46 +08:00

383 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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