Files
xinlong-shop-taro/src/pages/store/orders/index.tsx
赵忠林 0cc0d089e9 feat(store): 在订单列表页新增下单时间展示
- 订单卡片头部左侧新增「下单时间:YYYY-MM-DD HH:mm」显示,位于订单号下方
- 状态标签右侧对齐方式由垂直居中调整为顶端对齐,适配两行文本
- 新增辅助函数 formatOrderTime 兼容 ISO 和空格分隔时间格式,仅保留到分钟
- 复用后台返回的 ShopOrder.createTime 字段,无需后端改动,保证时间数据准确同步
2026-07-23 02:03:50 +08:00

1088 lines
50 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, useCallback, useRef} from 'react'
import {View, Text, Image, ScrollView, Input, Textarea} from '@tarojs/components'
import Taro, {useDidShow} from '@tarojs/taro'
import {pageShopOrder, updateShopOrder, confirmOfflinePayment} from '@/api/shop/shopOrder'
import type {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model'
import {saveShopOrderDelivery} from '@/api/shop/shopOrderDelivery'
import {TenantId} from '@/config/app'
import {useNewOrderDetector} from '@/hooks/useNewOrderDetector'
import { getCompressedImageUrl } from '@/utils/image'
import { getMyClerk, listShopStoreUser } from '@/api/shop/shopStoreUser'
import type {ShopStoreUser} from '@/api/shop/shopStoreUser/model'
import { ensurePrivacyAuthorized } from '@/api/system/file'
definePageConfig({
navigationBarTitleText: '订单管理',
})
// ─── Tab 配置(待处理 + 已完成 + 已关闭)──────────────────────────
type TabKey = 'pending' | 'completed' | 'closed'
const TABS: { key: TabKey; label: string; params: Partial<ShopOrderParam>[] }[] = [
// 待处理:合并 statusFilter=0待付款、1待发货、2待核销、3待收货
{key: 'pending', label: '待处理', params: [{statusFilter: 0}, {statusFilter: 1}, {statusFilter: 2}, {statusFilter: 3}]},
// 已完成statusFilter=5与后台管理一致
{key: 'completed', label: '已完成', params: [{statusFilter: 5}]},
// 已关闭statusFilter=8
{key: 'closed', label: '已关闭', params: [{statusFilter: 8}]},
]
// ─── 操作类型 ─────────────────────────────────────────────────────
type OpType = 'confirmPay' | 'deliver' | 'complete' | 'ship' | 'editPrice'
const OP_LABEL: Record<OpType, string> = {
confirmPay: '收款',
deliver: '送达',
complete: '已完成',
ship: '发货',
editPrice: '改价',
}
const OP_DESC: Record<OpType, string> = {
confirmPay: '确认已收到线下付款',
deliver: '上传送达凭证照片,标记为已收货(不改变付款和订单完成状态)',
complete: '确认订单已完成,收款后点击此按钮标记订单完成',
ship: '选择发货人员并生成发货单,订单将进入已发货状态',
editPrice: '修改订单实付金额',
}
// ─── 图片上传 ─────────────────────────────────────────────────────
const uploadImage = (filePath: string): Promise<string> => {
return new Promise((resolve, reject) => {
Taro.uploadFile({
url: 'https://server.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('上传请求失败')),
})
})
}
// ─── 状态辅助函数 ──────────────────────────────────────────────────
/** 格式化日期为 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}`
}
/** 格式化下单时间为「YYYY-MM-DD HH:mm」兼容 "2026-07-23T02:00:18" 与 "2026-07-23 02:00:18" */
function formatOrderTime(raw?: string): string {
if (!raw) return ''
const s = raw.replace('T', ' ').trim()
const parts = s.split(' ')
if (parts.length < 2) return s
const timeParts = parts[1].split(':')
const hhmm = `${timeParts[0]}:${timeParts[1] || '00'}`
return `${parts[0]} ${hhmm}`
}
/** 解析 sendEndImg兼容 JSON 数组(新格式)和逗号分隔(旧格式) */
function parseSendEndImg(raw: string): string[] {
if (!raw || !raw.trim()) return []
const trimmed = raw.trim()
// 尝试 JSON 解析
if (trimmed.startsWith('[')) {
try {
const arr = JSON.parse(trimmed)
if (Array.isArray(arr)) return arr.filter(Boolean)
} catch { /* fallback to comma split */ }
}
// 旧格式:逗号分隔(兼容单张图不含逗号的 URL
return trimmed.split(',').map(s => s.trim()).filter(Boolean)
}
function getStatusText(order: ShopOrder): string {
if (order.orderStatus === 1) return '已完成'
if (order.orderStatus === 2) return '已关闭'
// 线下付款·已发货待收款(先发货后结款)
if (order.payType === 9 && order.deliveryStatus === 20) return '已发货待收款'
if (order.payStatus === false || order.payStatus === null) {
// 线下付款待确认收款
if (order.payType === 9) return '待确认收款'
return '待收款'
}
if (order.payStatus && order.deliveryStatus === 20) return '待收货'
if (order.payStatus && order.deliveryStatus === 10) return '待发货'
return '进行中'
}
function getStatusColor(order: ShopOrder): string {
if (order.orderStatus === 1) return '#0e932e'
if (order.orderStatus === 2) return '#999'
// 已发货待收款:红色(与用户端一致)
if (order.payType === 9 && order.deliveryStatus === 20) return '#ee0a24'
if (order.payStatus === false || order.payStatus === null) return '#ee0a24'
if (order.deliveryStatus === 20) return '#4b9cf5'
if (order.payStatus) return '#ff7d00'
return '#999'
}
/** 判断订单是否可操作(非已完成、非已关闭) */
function isOrderActionable(order: ShopOrder): boolean {
return order.orderStatus !== 1 && order.orderStatus !== 2
}
/** 获取订单可执行的操作 */
function getOrderActions(order: ShopOrder): { label: string; type: OpType }[] {
if (!isOrderActionable(order)) return []
const actions: { label: string; type: OpType }[] = []
// 线下付款·待确认收款:显示"确认收款"和"改价"按钮
if (!order.payStatus && order.payType === 9 && order.orderStatus === 0) {
actions.push({label: '改价', type: 'editPrice'})
actions.push({label: '确认收款', type: 'confirmPay'})
}
// 发货:
// - 已付款且未发货:正常发货
// - 线下付款未付款且未发货:支持先发货后结款
if (order.orderStatus !== 1) {
const notShipped = order.deliveryStatus == null || order.deliveryStatus < 20
if (notShipped && (order.payStatus || order.payType === 9)) {
actions.push({label: '发货', type: 'ship'})
}
}
// 送达已发货但未上传送达照片时显示deliveryStatus=20 且 sendEndImg 为空)
const shipped = order.deliveryStatus != null && order.deliveryStatus === 20
const hasDeliverProof = order.sendEndImg && order.sendEndImg !== '[]'
if (shipped && !hasDeliverProof) {
actions.push({label: '送达', type: 'deliver'})
}
// 确认完成:仅已付款才可标记订单为已完成
if (order.payStatus) {
actions.push({label: '确认完成', type: 'complete'})
}
return actions
}
// ─── 页面组件 ──────────────────────────────────────────────────────
export default function StoreOrdersPage() {
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 [currentOrder, setCurrentOrder] = useState<ShopOrder | null>(null)
const [opType, setOpType] = useState<OpType>('pay')
const [proofImages, setProofImages] = useState<string[]>([])
const [payRemarks, setPayRemarks] = useState('')
const [submitting, setSubmitting] = useState(false)
// 改价弹窗状态
const [showEditPriceModal, setShowEditPriceModal] = useState(false)
const [editPriceOrder, setEditPriceOrder] = useState<ShopOrder | null>(null)
const [editPriceValue, setEditPriceValue] = useState('')
const [editPriceRemarks, setEditPriceRemarks] = useState('')
const [editingPrice, setEditingPrice] = useState(false)
// 发货弹窗状态
const [showShipModal, setShowShipModal] = useState(false)
const [clerkList, setClerkList] = useState<ShopStoreUser[]>([])
const [selectedClerkId, setSelectedClerkId] = useState<number | null>(null)
const [loadingClerks, setLoadingClerks] = useState(false)
const [shipping, setShipping] = useState(false)
// 搜索searchInput 受控输入框searchKeyword 为已提交的搜索词
const [searchInput, setSearchInput] = useState('')
const [searchKeyword, setSearchKeyword] = useState('')
const pageSize = 10
const loadingRef = useRef(false)
// ─── 访问控制:仅门店店员可访问此页面 ──────────────────────
const [accessChecking, setAccessChecking] = useState(true)
useEffect(() => {
getMyClerk()
.then(data => {
if (!data) {
Taro.showToast({ title: '无权访问', icon: 'none', duration: 1500 })
setTimeout(() => Taro.navigateBack(), 1500)
} else {
setAccessChecking(false)
}
})
.catch(() => {
Taro.showToast({ title: '无权访问', icon: 'none', duration: 1500 })
setTimeout(() => Taro.navigateBack(), 1500)
})
}, [])
// ─── 新订单轮询检测 ──────────────────────────────────────────
const { newOrderCount, clearUnread } = useNewOrderDetector({
interval: 30000, // 30 秒轮询一次
fetchLatestOrders: useCallback(
() => pageShopOrder({ page: 1, limit: 5 }).then(r => r?.list || []),
[],
),
onNewOrders: useCallback((count) => {
// 震动提醒
Taro.vibrateShort({ type: 'medium' })
// Toast 提醒
Taro.showToast({
title: `您有 ${count} 条新订单`,
icon: 'none',
duration: 2500,
})
}, []),
})
/** 加载订单列表(待处理 Tab 会合并多个 statusFilter 的结果) */
const loadOrders = useCallback(async (tab: TabKey, pageNo: number = 1, append = false) => {
if (loadingRef.current) return
loadingRef.current = true
setLoading(true)
try {
const tabConfig = TABS.find(t => t.key === tab)
if (!tabConfig) return
// 并行请求所有 params 组合,合并去重
const allRequests = tabConfig.params.map(p =>
pageShopOrder({...p, page: pageNo, limit: pageSize, keywords: searchKeyword || undefined})
)
const allResults = await Promise.all(allRequests)
// 合并所有结果列表,按 orderId 去重
const mergedList = allResults.reduce<ShopOrder[]>((acc, res) => {
const list = res?.list || []
list.forEach(item => {
if (!acc.some(existing => existing.orderId === item.orderId)) {
acc.push(item)
}
})
return acc
}, [])
// 按 createTime 降序排列
mergedList.sort((a, b) => {
const ta = a.createTime ? new Date(a.createTime).getTime() : 0
const tb = b.createTime ? new Date(b.createTime).getTime() : 0
return tb - ta
})
setOrderList(prev => append ? [...prev, ...mergedList] : mergedList)
setPage(pageNo)
// 只要任意一个请求还有数据就允许继续加载
const maxLen = Math.max(...allResults.map(r => (r?.list || []).length))
setHasMore(maxLen >= pageSize)
} catch (e: any) {
Taro.showToast({title: e.message || '加载失败', icon: 'none'})
if (!append) setOrderList([])
} finally {
loadingRef.current = false
setLoading(false)
}
}, [searchKeyword])
// 切换 tab 时重新加载
useEffect(() => {
loadOrders(activeTab, 1)
}, [activeTab, loadOrders])
// 页面重新显示时刷新
useDidShow(() => {
loadOrders(activeTab, 1)
})
/** 加载更多 */
const handleLoadMore = () => {
if (hasMore && !loadingRef.current) {
loadOrders(activeTab, page + 1, true)
}
}
/** 提交搜索(点击搜索按钮或键盘回车) */
const handleSearch = () => {
setSearchKeyword(searchInput.trim())
}
/** 清空搜索 */
const handleClearSearch = () => {
setSearchInput('')
setSearchKeyword('')
}
/** 打开操作弹窗 */
const openModal = (order: ShopOrder, type: OpType) => {
// 改价走独立弹窗
if (type === 'editPrice') {
openEditPriceModal(order)
return
}
setCurrentOrder(order)
setOpType(type)
setProofImages([])
setPayRemarks('')
setShowModal(true)
}
/** 关闭弹窗 */
const closeModal = () => {
setShowModal(false)
setCurrentOrder(null)
setProofImages([])
setPayRemarks('')
}
/** 选择并上传凭证图片 */
const chooseProofImage = async () => {
const maxCount = opType === 'confirmPay' ? 1 : 3
const remaining = maxCount - proofImages.length
if (remaining <= 0) return
// 门店上传凭证需要访问相册/摄像头,先预检隐私协议授权
try {
await ensurePrivacyAuthorized()
} catch {
// 授权失败由 chooseImage 自行处理
}
Taro.chooseImage({
count: remaining,
sizeType: ['compressed'],
sourceType: ['camera', 'album'],
success: async (res) => {
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'})
}
}
},
})
}
/** 移除凭证图片 */
const removeProofImage = (idx: number) => {
setProofImages(prev => prev.filter((_, i) => i !== idx))
}
/** 提交操作 */
const submitOperation = async () => {
if (!currentOrder) return
// 确认送达必须上传凭证照片
if (opType === 'deliver' && proofImages.length === 0) {
Taro.showToast({title: '请上传送达凭证照片', icon: 'none'})
return
}
// 确认收款必须上传支付凭证
if (opType === 'confirmPay' && proofImages.length === 0) {
Taro.showToast({title: '请上传支付凭证', icon: 'none'})
return
}
setSubmitting(true)
try {
if (opType === 'confirmPay') {
// 确认线下收款
await confirmOfflinePayment(
currentOrder.orderId!,
payRemarks.trim() || undefined,
proofImages[0]
)
} else {
const updateData: any = {orderId: currentOrder.orderId}
if (opType === 'deliver') {
// 送达上传送达照片不改变付款状态、deliveryStatus 和订单完成状态)
updateData.deliveryTime = formatDateTime(new Date())
updateData.sendEndImg = JSON.stringify(proofImages)
} else if (opType === 'complete') {
// 确认完成:标记订单为已完成
updateData.orderStatus = 1 // 订单状态 → 已完成
}
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 openEditPriceModal = (order: ShopOrder) => {
setEditPriceOrder(order)
setEditPriceValue(String(order.payPrice || order.totalPrice || ''))
setEditPriceRemarks('')
setShowEditPriceModal(true)
}
/** 关闭改价弹窗 */
const closeEditPriceModal = () => {
setShowEditPriceModal(false)
setEditPriceOrder(null)
setEditPriceValue('')
setEditPriceRemarks('')
}
/** 提交改价 */
const submitEditPrice = async () => {
if (!editPriceOrder) return
const newPrice = parseFloat(editPriceValue)
if (isNaN(newPrice) || newPrice < 0) {
Taro.showToast({title: '请输入有效金额', icon: 'none'})
return
}
const oldPriceStr = String(editPriceOrder.payPrice || editPriceOrder.totalPrice || '0')
const oldPrice = parseFloat(oldPriceStr)
if (!isNaN(oldPrice) && Math.abs(newPrice - oldPrice) < 0.01) {
Taro.showToast({title: '金额未变化', icon: 'none'})
return
}
setEditingPrice(true)
try {
const changeRecord = `【门店修改金额】原实付¥${oldPriceStr} → 新实付¥${newPrice.toFixed(2)}`
await updateShopOrder({
orderId: editPriceOrder.orderId,
payPrice: newPrice.toFixed(2),
merchantRemarks: (editPriceOrder.merchantRemarks || '') + `\n${changeRecord}`,
} as ShopOrder)
Taro.showToast({title: '改价成功', icon: 'success'})
closeEditPriceModal()
loadOrders(activeTab, 1)
} catch (e: any) {
Taro.showToast({title: e.message || '改价失败', icon: 'none'})
} finally {
setEditingPrice(false)
}
}
/** 打开发货弹窗:拉取门店店员列表供选择发货人员 */
const openShipModal = async (order: ShopOrder) => {
setCurrentOrder(order)
setSelectedClerkId(null)
setClerkList([])
setShowShipModal(true)
setLoadingClerks(true)
try {
// 获取当前登录店员(含 storeId / userId
// 用 storeId 拉取本门店店员列表,避免 ?storeId=0 拉不到数据
const myClerk = await getMyClerk()
const storeId = myClerk?.storeId
if (!storeId) {
Taro.showToast({title: '门店信息未就绪,请稍后再试', icon: 'none'})
return
}
const res = await listShopStoreUser({storeId})
setClerkList(res || [])
// 默认选中与当前登录用户 userId 一致的店员
if (res && myClerk?.userId) {
const match = res.find(c => c.userId === myClerk!.userId)
if (match) setSelectedClerkId(match.id ?? null)
}
} catch (e) {
Taro.showToast({title: '加载店员失败', icon: 'none'})
} finally {
setLoadingClerks(false)
}
}
/** 确认发货:创建发货单并记录发货人员,订单置为已发货 */
const handleConfirmShip = async () => {
if (!currentOrder) return
const clerk = clerkList.find(c => c.id === selectedClerkId)
if (!clerk) {
Taro.showToast({title: '请选择发货人员', icon: 'none'})
return
}
setShipping(true)
try {
// 1) 创建发货单,记录发货人信息
await saveShopOrderDelivery({
orderId: currentOrder.orderId,
deliveryMethod: 20, // 20=无需物流(门店自送/自提,无快递单号)
sendName: clerk.name,
sendPhone: clerk.phone,
// 发货地址取门店名称(店员实体无地址字段)
sendAddress: currentOrder.storeName || '',
})
// 2) 订单置为已发货deliveryStatus=20物流页与列表状态同步更新
await updateShopOrder({
orderId: currentOrder.orderId,
deliveryStatus: 20,
deliveryTime: formatDateTime(new Date()),
})
Taro.showToast({title: '发货成功', icon: 'success'})
setShowShipModal(false)
loadOrders(activeTab, 1) // 刷新列表(发货按钮随之隐藏)
} catch (e: any) {
Taro.showToast({title: e.message || '发货失败', icon: 'none'})
} finally {
setShipping(false)
}
}
/** 关闭订单 */
const handleCloseOrder = (order: ShopOrder) => {
Taro.showModal({
title: '提示',
content: `确定要关闭订单 ${order.orderNo} 吗?关闭后无法恢复。`,
confirmColor: '#ee0a24',
success: async (res) => {
if (!res.confirm) return
try {
Taro.showLoading({title: '关闭中...'})
await updateShopOrder({orderId: order.orderId, orderStatus: 2} as ShopOrder)
Taro.hideLoading()
Taro.showToast({title: '关闭成功', icon: 'success'})
loadOrders(activeTab, 1)
} catch (e: any) {
Taro.hideLoading()
Taro.showToast({title: e.message || '关闭失败', icon: 'none'})
}
},
})
}
/** 一键导航到收货地址(调用微信内置地图) */
const handleNavigate = (order: ShopOrder) => {
const lat = parseFloat(order.addressLat || '')
const lng = parseFloat(order.addressLng || '')
if (isNaN(lat) || isNaN(lng)) {
Taro.showToast({title: '该订单未记录定位信息,无法导航', icon: 'none'})
return
}
Taro.openLocation({
latitude: lat,
longitude: lng,
name: order.realName || '收货地址',
address: order.address || '',
scale: 16,
})
}
/** 渲染单个订单卡片 */
const renderOrderCard = (order: ShopOrder) => {
const actions = getOrderActions(order)
const canDelete = isOrderActionable(order) // 仅未完成、未关闭的订单可关闭
const orderGoods = (order as any).orderGoods || []
return (
<View key={order.orderId} className='bg-white rounded-xl mx-3 mt-3 p-4'>
{/* 订单头部 */}
<View className='flex justify-between items-start mb-3'>
<View className='flex flex-col'>
<Text className='text-xs text-gray-400'>{order.orderNo}</Text>
{order.createTime ? (
<Text className='text-xs text-gray-400 mt-1'>{formatOrderTime(order.createTime)}</Text>
) : null}
</View>
<Text className='text-xs' style={{color: getStatusColor(order)}}>
{getStatusText(order)}
</Text>
</View>
{/* 商品列表 */}
{orderGoods.map((goods: any, idx: number) => (
<View key={idx} className='flex items-center gap-3 mb-3'>
{goods.image && (
<Image className='w-16 h-16 rounded-lg bg-gray-50' src={getCompressedImageUrl(goods.image, { width: 80 })}
mode='aspectFill'/>
)}
<View className='flex-1'>
<Text className='text-sm text-gray-800 block'>{goods.goodsName}</Text>
{goods.spec && (
<Text className='text-xs text-gray-400 mt-1 block'>{goods.spec}</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.totalNum}</Text>
</View>
</View>
</View>
))}
{/* 收货信息(点击区域一键导航) */}
{(order.realName || order.phone) && (
<View
className='bg-gray-50 rounded-lg p-3 mb-3 flex items-center'
onClick={() => handleNavigate(order)}
>
<View className='flex-1'>
<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='ml-2 flex flex-col items-center justify-center px-1'>
<Text className='text-lg text-green-500 leading-none'></Text>
<Text className='text-xs text-green-500 mt-0.5'></Text>
</View>
</View>
)}
{/* 买家备注 */}
{order.buyerRemarks && (
<View className='bg-orange-50 rounded-lg p-3 mb-3'>
<Text className='text-xs text-orange-500 mb-1 block'></Text>
<Text className='text-sm text-gray-600 block'>{order.buyerRemarks}</Text>
</View>
)}
{/* 付款凭证(线下付款确认收款后显示) */}
{order.paymentVoucher && (
<View className='bg-green-50 rounded-lg p-3 mb-3'>
<Text className='text-xs text-green-500 mb-2 block'></Text>
<Image
className='w-20 h-20 rounded-lg bg-gray-100'
src={getCompressedImageUrl(order.paymentVoucher)}
mode='aspectFill'
onClick={() => Taro.previewImage({
current: order.paymentVoucher!,
urls: [order.paymentVoucher!]
})}
/>
</View>
)}
{/* 送达凭证(已完成订单) */}
{order.sendEndImg && (() => {
const imgs = parseSendEndImg(order.sendEndImg)
return imgs.length > 0 && (
<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'>
{imgs.map((img, i) => (
<Image
key={i}
className='w-20 h-20 rounded-lg bg-gray-100'
src={getCompressedImageUrl(img)}
mode='aspectFill'
onClick={() => Taro.previewImage({
current: img,
urls: imgs
})}
/>
))}
</View>
</View>
)
})()}
{/* 金额 + 支付方式 */}
<View className='flex justify-between items-center mb-3'>
<Text className='text-xs text-gray-400'>
{order.payType === 8 ? '货到付款' : order.payType === 9 ? '线下付款' : order.payType === 4 ? '现金支付' : order.payType === 1 ? '微信支付' : '其他'}
</Text>
<Text className='text-sm text-gray-700'>
<Text className='text-red-500 font-medium'>¥{order.payPrice || order.totalPrice}</Text>
</Text>
</View>
{/* 操作按钮 */}
<View className='flex justify-between items-center border-t border-gray-50 pt-3'>
{/* 关闭按钮:仅未完成/未关闭的订单显示 */}
{canDelete ? (
<View
className='px-3 py-1.5'
onClick={() => handleCloseOrder(order)}
>
<Text className='text-xs text-gray-400'></Text>
</View>
) : (
<View/>
)}
<View className={`flex gap-2 ${!canDelete ? 'w-full justify-end' : ''}`}>
{actions.map(act => (
<View
key={act.type}
className={`px-4 py-2 rounded-lg ${
act.type === 'complete'
? 'bg-green-500'
: act.type === 'confirmPay'
? 'bg-blue-500'
: act.type === 'ship'
? 'bg-purple-500'
: act.type === 'editPrice'
? 'bg-orange-500'
: 'border border-blue-500'
}`}
onClick={() => act.type === 'ship'
? openShipModal(order)
: openModal(order, act.type)}
>
<Text className={`text-sm ${
act.type === 'complete' || act.type === 'confirmPay' || act.type === 'ship' || act.type === 'editPrice' ? 'text-white' : 'text-blue-500'
}`}>{act.label}</Text>
</View>
))}
</View>
</View>
</View>
)
}
// 访问校验中或已拒绝:不渲染页面内容
if (accessChecking) {
return <View className='min-h-full bg-gray-50' />
}
return (
<View className='min-h-full bg-gray-50'>
{/* 搜索栏 */}
<View className='bg-white px-3 py-2 flex items-center'>
<View className='flex-1 flex items-center bg-gray-100 rounded-full px-3 h-9'>
<Input
className='flex-1 text-sm text-gray-800'
value={searchInput}
onInput={(e) => setSearchInput(e.detail.value)}
onConfirm={handleSearch}
placeholder='搜索订单号 / 手机号 / 昵称'
confirmType='search'
/>
{searchInput ? (
<View
className='ml-2 w-5 h-5 flex items-center justify-center'
onClick={handleClearSearch}
>
<Text className='text-gray-400 text-base leading-none'>×</Text>
</View>
) : null}
</View>
<View className='ml-1 px-2 py-1' onClick={handleSearch}>
<Text className='text-sm text-green-500 font-medium'></Text>
</View>
</View>
{/* Tab 栏 */}
<View className='bg-white flex'>
{TABS.map(tab => (
<View
key={tab.key}
className={`flex-1 text-center py-3 border-b-2 relative ${
activeTab === tab.key ? 'border-green-500' : 'border-transparent'
}`}
onClick={() => {
setActiveTab(tab.key)
clearUnread()
}}
>
<Text
className={`text-sm ${activeTab === tab.key ? 'text-green-500 font-medium' : 'text-gray-500'}`}>
{tab.label}
</Text>
{/* 新订单角标:在"待处理"Tab 上显示 */}
{tab.key === 'pending' && newOrderCount > 0 && (
<View className='absolute -top-0.5 right-2 min-w-[18px] h-[18px] rounded-full bg-red-500 flex items-center justify-center px-1'>
<Text className='text-white text-[10px] font-bold'>
{newOrderCount > 99 ? '99+' : newOrderCount}
</Text>
</View>
)}
</View>
))}
</View>
{/* 订单列表 */}
<ScrollView
scrollY
style={{height: 'calc(100vh - 100px)'}}
onScrollToLower={handleLoadMore}
lowerThreshold={100}
>
{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'>
{searchKeyword ? `未找到与“${searchKeyword}”相关的订单` : '暂无订单'}
</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>
)}
{!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>
{/* 操作弹窗 */}
{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>
<Text className='text-xs text-gray-500 mt-2 block'>
{OP_DESC[opType]}
</Text>
</View>
)}
{/* 凭证上传(确认完成不需要凭证) */}
{opType !== 'complete' && (
<>
<Text className='text-sm text-gray-600 mb-3 block'>
{opType === 'confirmPay'
? '上传支付凭证(必填,如微信转账截图)'
: opType === 'deliver'
? '上传送达凭证必填配送货物到达后拍照上传最多3张'
: '上传凭证照片选填最多3张'}
</Text>
<View className='flex flex-wrap gap-3 mb-4'>
{proofImages.map((url, idx) => (
<View key={idx} className='relative'>
<Image className='w-20 h-20 rounded-lg bg-gray-50' src={getCompressedImageUrl(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 < (opType === 'confirmPay' ? 1 : 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>
</>
)}
{/* 备注(仅确认收款时显示) */}
{opType === 'confirmPay' && (
<View className='mb-6'>
<Text className='text-sm text-gray-600 mb-2 block'></Text>
<Textarea
className='w-full px-4 py-3 rounded-xl border border-gray-200 text-sm text-gray-800'
style={{minHeight: '60px'}}
value={payRemarks}
onInput={(e) => setPayRemarks(e.detail.value)}
placeholder='可填写备注(如:微信转账已收到)'
maxlength={200}
/>
</View>
)}
{/* 确认完成不需要备注时补充间距 */}
{opType !== 'confirmPay' && <View className='mb-2'/>}
{/* 操作按钮 */}
<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 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>
) : (
<Text className='text-sm text-white'>{OP_LABEL[opType]}</Text>
)}
</View>
</View>
</View>
</View>
)}
{/* 发货弹窗:选择发货人员 */}
{showShipModal && currentOrder && (
<View className='fixed inset-0 z-50 flex items-end justify-center'>
<View className='absolute inset-0 bg-black/50' onClick={() => setShowShipModal(false)}/>
<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'>
</Text>
{/* 订单信息 */}
<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>
{/* 店员列表 */}
<View className='max-h-80 overflow-y-auto mb-5'>
{loadingClerks ? (
<View className='py-8 flex justify-center'>
<Text className='text-sm text-gray-400'>...</Text>
</View>
) : clerkList.length === 0 ? (
<View className='py-8 flex justify-center'>
<Text className='text-sm text-gray-400'></Text>
</View>
) : (
clerkList.map(clerk => {
const selected = clerk.id === selectedClerkId
return (
<View
key={clerk.id}
className={`flex items-center justify-between px-4 py-3 rounded-xl mb-2 border ${
selected ? 'border-purple-500 bg-purple-50' : 'border-gray-200'
}`}
onClick={() => setSelectedClerkId(clerk.id ?? null)}
>
<View className='flex-1'>
<Text className='text-sm text-gray-800'>
{clerk.name}
<Text className='text-xs text-gray-400 ml-2'>
{clerk.roleType === 1 ? '经理' : '店员'}
</Text>
</Text>
<Text className='text-xs text-gray-500 mt-0.5 block'>{clerk.phone}</Text>
</View>
{selected && (
<Text className='text-purple-500 text-sm'></Text>
)}
</View>
)
})
)}
</View>
{/* 操作按钮 */}
<View className='flex gap-3'>
<View className='flex-1 py-3 rounded-xl bg-gray-100 text-center' onClick={() => setShowShipModal(false)}>
<Text className='text-sm text-gray-600'></Text>
</View>
<View
className={`flex-1 py-3 rounded-xl text-center flex items-center justify-center ${
selectedClerkId != null ? 'bg-purple-500' : 'bg-gray-300'
}`}
onClick={selectedClerkId != null && !shipping ? handleConfirmShip : undefined}
>
{shipping ? (
<Text className='text-sm text-white'>...</Text>
) : (
<Text className='text-sm text-white'></Text>
)}
</View>
</View>
</View>
</View>
)}
{/* 改价弹窗 */}
{showEditPriceModal && editPriceOrder && (
<View className='fixed inset-0 z-50 flex items-end justify-center'>
<View className='absolute inset-0 bg-black/50' onClick={closeEditPriceModal}/>
<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'>
</Text>
{/* 订单信息 */}
<View className='bg-gray-50 rounded-xl p-4 mb-5'>
<Text className='text-sm text-gray-700 block'>{editPriceOrder.orderNo}</Text>
<Text className='text-sm text-gray-700 mt-1 block'>
<Text className='text-red-500 font-medium'>¥{editPriceOrder.payPrice || editPriceOrder.totalPrice}</Text>
</Text>
</View>
{/* 新金额输入 */}
<View className='mb-6'>
<Text className='text-sm text-gray-600 mb-2 block'></Text>
<Input
type='digit'
className='w-full px-4 py-3 rounded-xl border border-gray-200 text-lg text-gray-800'
value={editPriceValue}
onInput={(e) => setEditPriceValue(e.detail.value)}
placeholder='请输入新的实付金额'
/>
</View>
{/* 改价备注 */}
<View className='mb-6'>
<Text className='text-sm text-gray-600 mb-2 block'></Text>
<Textarea
className='w-full px-4 py-3 rounded-xl border border-gray-200 text-sm text-gray-800'
style={{minHeight: '60px'}}
value={editPriceRemarks}
onInput={(e) => setEditPriceRemarks(e.detail.value)}
placeholder='如:客户协商优惠、多收退款等'
maxlength={200}
/>
</View>
{/* 操作按钮 */}
<View className='flex gap-3'>
<View className='flex-1 py-3 rounded-xl bg-gray-100 text-center' onClick={closeEditPriceModal}>
<Text className='text-sm text-gray-600'></Text>
</View>
<View
className='flex-1 py-3 rounded-xl bg-orange-500 text-center flex items-center justify-center'
onClick={editingPrice ? undefined : submitEditPrice}
>
{editingPrice ? (
<Text className='text-sm text-white'>...</Text>
) : (
<Text className='text-sm text-white'></Text>
)}
</View>
</View>
</View>
</View>
)}
</View>
)
}