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[] }[] = [ // 待处理:合并 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 = { confirmPay: '收款', complete: '已完成', editPrice: '金额', ship: '发货', } const OP_DESC: Record = { confirmPay: '确认已收到线下付款,订单将进入待发货状态', complete: '同时变更支付状态为已付款、收货状态为已收货、订单状态为已完成', editPrice: '修改订单实付金额', ship: '选择发货人员并生成发货单,订单将进入已发货状态', } // ─── 图片上传 ───────────────────────────────────────────────────── const uploadImage = (filePath: string): Promise => { 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.payStatus === false || order.payStatus === null) { // 线下付款待确认收款 if (order.payType === 9) return '待确认收款' return '待收款' } if (order.payStatus && order.deliveryStatus === 10) return '待发货' if (order.payStatus && order.deliveryStatus === 20) return '待收货' return '进行中' } function getStatusColor(order: ShopOrder): string { if (order.orderStatus === 1) return '#0e932e' if (order.orderStatus === 2) return '#999' if (order.payStatus === false || order.payStatus === null) return '#ee0a24' 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'}) } // 发货:确认收款后、且尚未发货(deliveryStatus<20)时显示 if (order.payStatus && (order.deliveryStatus == null || order.deliveryStatus < 20) && order.orderStatus !== 1) { actions.push({label: '发货', type: 'ship'}) } // 确认完成:仅在确认收款后显示(已付款才能确认完成) if (order.payStatus) { actions.push({label: '确认完成', type: 'complete'}) } return actions } // ─── 页面组件 ────────────────────────────────────────────────────── export default function StoreOrdersPage() { const [activeTab, setActiveTab] = useState('pending') // 订单列表与分页 const [orderList, setOrderList] = useState([]) const [page, setPage] = useState(1) const [hasMore, setHasMore] = useState(true) const [loading, setLoading] = useState(false) // 操作弹窗状态 const [showModal, setShowModal] = useState(false) const [currentOrder, setCurrentOrder] = useState(null) const [opType, setOpType] = useState('pay') const [proofImages, setProofImages] = useState([]) 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([]) const [selectedClerkId, setSelectedClerkId] = useState(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((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 ( {/* 订单头部 */} 订单号:{order.orderNo} {getStatusText(order)} {/* 商品列表 */} {orderGoods.map((goods: any, idx: number) => ( {goods.image && ( )} {goods.goodsName} {goods.spec && ( {goods.spec} )} ¥{goods.price} x{goods.totalNum} ))} {/* 收货信息(点击区域一键导航) */} {(order.realName || order.phone) && ( handleNavigate(order)} > {order.realName} {order.phone} {order.address && ( {order.address} )} 导航 )} {/* 买家备注 */} {order.buyerRemarks && ( 买家备注 {order.buyerRemarks} )} {/* 送达凭证(已完成订单) */} {order.sendEndImg && (() => { const imgs = parseSendEndImg(order.sendEndImg) return imgs.length > 0 && ( 配送凭证: {imgs.map((img, i) => ( Taro.previewImage({ current: img, urls: imgs })} /> ))} ) })()} {/* 金额 + 支付方式 */} {order.payType === 8 ? '货到付款' : order.payType === 9 ? '线下付款' : order.payType === 4 ? '现金支付' : order.payType === 1 ? '微信支付' : '其他'} 实付:¥{order.payPrice || order.totalPrice} {/* 操作按钮 */} {/* 关闭按钮:仅未完成/未关闭的订单显示 */} {canDelete ? ( handleCloseOrder(order)} > 关闭订单 ) : ( )} {actions.map(act => ( act.type === 'editPrice' ? openEditPriceModal(order) : act.type === 'ship' ? openShipModal(order) : openModal(order, act.type)} > {act.label} ))} ) } // 访问校验中或已拒绝:不渲染页面内容 if (accessChecking) { return } return ( {/* 搜索栏 */} setSearchInput(e.detail.value)} onConfirm={handleSearch} placeholder='搜索订单号 / 手机号 / 昵称' confirmType='search' /> {searchInput ? ( × ) : null} 搜索 {/* Tab 栏 */} {TABS.map(tab => ( { setActiveTab(tab.key) clearUnread() }} > {tab.label} {/* 新订单角标:在"待处理"Tab 上显示 */} {tab.key === 'pending' && newOrderCount > 0 && ( {newOrderCount > 99 ? '99+' : newOrderCount} )} ))} {/* 订单列表 */} {loading && orderList.length === 0 ? ( 加载中... ) : orderList.length === 0 ? ( {searchKeyword ? `未找到与“${searchKeyword}”相关的订单` : '暂无订单'} ) : ( orderList.map(renderOrderCard) )} {loading && orderList.length > 0 && ( 加载中... )} {!hasMore && orderList.length > 0 && ( 没有更多了 )} {/* 操作弹窗 */} {showModal && ( {/* 遮罩 */} {/* 弹窗内容 */} 确认{OP_LABEL[opType]} {/* 订单信息 */} {currentOrder && ( 订单号:{currentOrder.orderNo} 实付金额:¥{currentOrder.payPrice || currentOrder.totalPrice} 操作说明:{OP_DESC[opType]} )} {/* 凭证上传 */} {opType === 'confirmPay' ? '上传支付凭证(必填,如微信转账截图)' : opType === 'complete' ? '上传凭证照片(必填,配送货物到达后拍照上传,最多3张)' : '上传凭证照片(选填,最多3张)'} {proofImages.map((url, idx) => ( removeProofImage(idx)} > × ))} {proofImages.length < (opType === 'confirmPay' ? 1 : 3) && ( + )} {/* 备注(仅确认收款时显示) */} {opType === 'confirmPay' && ( 备注(选填)