Files
xinlong-shop-taro/src/pages/store/orders/index.tsx
赵忠林 147802dbe9 fix(order): 统一线下付款已发货未付款状态显示
- 用户端订单卡的已发货待收款状态调整,线下付款且未付款状态显示红色标识
- 用户端订单详情新增该状态的红色展示和提示信息
- 门店端订单列表状态文案和颜色同步更新,保持与用户端一致
- 门店端订单操作按钮逻辑调整,未付款已发货订单显示“确认完成”按钮
- 解决线下付款先发货后付款订单的状态显示和按钮操作不一致问题
2026-07-17 15:51:22 +08:00

1049 lines
48 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' | 'complete' | 'editPrice' | 'ship'
const OP_LABEL: Record<OpType, string> = {
confirmPay: '收款',
complete: '已完成',
editPrice: '金额',
ship: '发货',
}
const OP_DESC: Record<OpType, string> = {
confirmPay: '确认已收到线下付款',
complete: '同时变更支付状态为已付款、收货状态为已收货、订单状态为已完成',
editPrice: '修改订单实付金额',
ship: '选择发货人员并生成发货单,订单将进入已发货状态',
}
// ─── 图片上传 ─────────────────────────────────────────────────────
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}`
}
/** 解析 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) {
actions.push({label: '改价', type: 'editPrice'})
}
// 线下付款·待确认收款:显示"确认收款"按钮
if (!order.payStatus && order.payType === 9 && order.orderStatus === 0) {
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'})
}
}
// 确认完成:已发货即可确认完成(无论是否付款,线下付款先发货后结款场景也需要上传送达照片)
const shipped = order.deliveryStatus != null && order.deliveryStatus >= 20
if (shipped) {
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 [editPayPrice, setEditPayPrice] = useState('')
const [editReason, setEditReason] = useState('')
// 发货弹窗状态
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) => {
setCurrentOrder(order)
setOpType(type)
setProofImages([])
setPayRemarks('')
setShowModal(true)
}
/** 关闭弹窗 */
const closeModal = () => {
setShowModal(false)
setCurrentOrder(null)
setProofImages([])
setPayRemarks('')
}
/** 打开修改金额弹窗 */
const openEditPriceModal = (order: ShopOrder) => {
setCurrentOrder(order)
setEditPayPrice(String(order.payPrice || order.totalPrice || ''))
setEditReason('')
setShowEditPriceModal(true)
}
/** 关闭修改金额弹窗 */
const closeEditPriceModal = () => {
setShowEditPriceModal(false)
setCurrentOrder(null)
setEditPayPrice('')
setEditReason('')
}
/** 提交修改金额 */
const submitEditPrice = async () => {
if (!currentOrder) return
const newPrice = parseFloat(editPayPrice)
if (isNaN(newPrice) || newPrice < 0) {
Taro.showToast({title: '请输入有效的金额', icon: 'none'})
return
}
if (!editReason.trim()) {
Taro.showToast({title: '请输入修改原因', icon: 'none'})
return
}
setSubmitting(true)
try {
const oldPrice = currentOrder.payPrice || currentOrder.totalPrice || '0'
const changeRecord = `【门店修改金额】原实付¥${oldPrice} → 新实付¥${newPrice.toFixed(2)},原因:${editReason.trim()}`
await updateShopOrder({
orderId: currentOrder.orderId,
payPrice: newPrice.toFixed(2),
merchantRemarks: (currentOrder.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 {
setSubmitting(false)
}
}
/** 选择并上传凭证图片 */
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 === 'complete' && 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 === 'complete') {
// 确认完成:用户已收到货
updateData.payStatus = true
updateData.payTime = formatDateTime(new Date())
updateData.deliveryStatus = 20 // 发货状态 → 已收货
updateData.deliveryTime = formatDateTime(new Date()) // 收货时间
updateData.orderStatus = 1 // 订单状态 → 已完成
updateData.sendEndImg = JSON.stringify(proofImages) // 配送员送达拍照JSON数组
}
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 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-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>
{/* 商品列表 */}
{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.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 === 'editPrice'
? 'bg-orange-500'
: act.type === 'ship'
? 'bg-purple-500'
: 'border border-blue-500'
}`}
onClick={() => act.type === 'editPrice'
? openEditPriceModal(order)
: act.type === 'ship'
? openShipModal(order)
: openModal(order, act.type)}
>
<Text className={`text-sm ${
act.type === 'complete' || act.type === 'editPrice' || act.type === 'confirmPay' || act.type === 'ship' ? '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>
)}
{/* 凭证上传 */}
<Text className='text-sm text-gray-600 mb-3 block'>
{opType === 'confirmPay'
? '上传支付凭证(必填,如微信转账截图)'
: opType === 'complete'
? '上传凭证照片必填配送货物到达后拍照上传最多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>
)}
{/* 修改金额弹窗 */}
{showEditPriceModal && currentOrder && (
<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'>{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='mb-5'>
<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={editPayPrice}
onInput={(e) => setEditPayPrice(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={editReason}
onInput={(e) => setEditReason(e.detail.value)}
placeholder='请输入修改原因,如:商品缺货调整、协商降价等'
maxlength={100}
/>
</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={submitting ? undefined : submitEditPrice}
>
{submitting ? (
<Text className='text-sm text-white'>...</Text>
) : (
<Text className='text-sm text-white'></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>
)}
</View>
)
}