feat(store): 门店订单页新增发货按钮及选择发货人功能
- 物流页发货信息依赖 shopOrderDelivery,之前门店确认完成不创建发货单导致显示为空 - 在门店订单页确认收款后增加发货按钮,允许选择门店店员作为发货人并创建发货单 - 新增接口 saveShopOrderDelivery 调用 POST /shop/shop-order-delivery 创建发货单 - 补全 shopStoreUser 模型字段,支持店员姓名、电话、角色等信息 - 订单列表支持发货操作按钮显示及样式,增加对应操作逻辑及UI弹窗选择发货人员 - 发货后订单状态置为已发货(deliveryStatus=20),同步更新发货时间和物流页显示 - 用户可选择门店全部店员(含经理)作为发货人员,确认完成按钮可与发货按钮共存,行为保持兼容 - 完成相关接口调用错误提示与加载状态处理,确保用户体验 - taro 构建通过,无类型错误
This commit is contained in:
18
src/api/shop/shopOrderDelivery/index.ts
Normal file
18
src/api/shop/shopOrderDelivery/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import request from '@/utils/request'
|
||||
import type { ApiResult } from '@/api'
|
||||
import type { ShopOrderDelivery } from '@/api/shop/shopOrder/model'
|
||||
|
||||
/**
|
||||
* 创建发货单(门店发货:记录发货人员)
|
||||
* 后端 POST /shop/shop-order-delivery(save)
|
||||
*/
|
||||
export async function saveShopOrderDelivery(data: ShopOrderDelivery) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/shop/shop-order-delivery',
|
||||
data
|
||||
)
|
||||
if (res.code === 0) {
|
||||
return res.message
|
||||
}
|
||||
return Promise.reject(new Error(res.message))
|
||||
}
|
||||
@@ -10,6 +10,16 @@ export interface ShopStoreUser {
|
||||
storeId?: number;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 姓名
|
||||
name?: string;
|
||||
// 手机号
|
||||
phone?: string;
|
||||
// 头像
|
||||
image?: string;
|
||||
// 角色类型: 1-门店经理, 2-店员
|
||||
roleType?: number;
|
||||
// 门店名称(关联字段)
|
||||
storeName?: string;
|
||||
// 备注
|
||||
comments?: string;
|
||||
// 排序号
|
||||
|
||||
@@ -3,10 +3,12 @@ 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 } from '@/api/shop/shopStoreUser'
|
||||
import { getMyClerk, listShopStoreUser } from '@/api/shop/shopStoreUser'
|
||||
import type {ShopStoreUser} from '@/api/shop/shopStoreUser/model'
|
||||
import { ensurePrivacyAuthorized } from '@/api/system/file'
|
||||
|
||||
definePageConfig({
|
||||
@@ -26,18 +28,20 @@ const TABS: { key: TabKey; label: string; params: Partial<ShopOrderParam>[] }[]
|
||||
]
|
||||
|
||||
// ─── 操作类型 ─────────────────────────────────────────────────────
|
||||
type OpType = 'confirmPay' | 'complete' | 'editPrice'
|
||||
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: '选择发货人员并生成发货单,订单将进入已发货状态',
|
||||
}
|
||||
|
||||
// ─── 图片上传 ─────────────────────────────────────────────────────
|
||||
@@ -135,6 +139,10 @@ function getOrderActions(order: ShopOrder): { label: string; type: OpType }[] {
|
||||
if (order.payStatus) {
|
||||
actions.push({label: '确认完成', type: 'complete'})
|
||||
}
|
||||
// 发货:确认收款后、且尚未发货(deliveryStatus<20)时显示
|
||||
if (order.payStatus && (order.deliveryStatus == null || order.deliveryStatus < 20) && order.orderStatus !== 1) {
|
||||
actions.push({label: '发货', type: 'ship'})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
@@ -161,6 +169,13 @@ export default function StoreOrdersPage() {
|
||||
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('')
|
||||
@@ -439,6 +454,58 @@ export default function StoreOrdersPage() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开发货弹窗:拉取门店店员列表供选择发货人员 */
|
||||
const openShipModal = async (order: ShopOrder) => {
|
||||
setCurrentOrder(order)
|
||||
setSelectedClerkId(null)
|
||||
setClerkList([])
|
||||
setShowShipModal(true)
|
||||
setLoadingClerks(true)
|
||||
try {
|
||||
const res = await listShopStoreUser({storeId: order.storeId})
|
||||
setClerkList(res || [])
|
||||
} 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({
|
||||
@@ -600,12 +667,18 @@ export default function StoreOrdersPage() {
|
||||
? '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) : openModal(order, act.type)}
|
||||
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' ? 'text-white' : 'text-blue-500'
|
||||
act.type === 'complete' || act.type === 'editPrice' || act.type === 'confirmPay' || act.type === 'ship' ? 'text-white' : 'text-blue-500'
|
||||
}`}>{act.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
@@ -867,6 +940,85 @@ export default function StoreOrdersPage() {
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user