feat(store): 门店订单页新增发货按钮及选择发货人功能
- 物流页发货信息依赖 shopOrderDelivery,之前门店确认完成不创建发货单导致显示为空 - 在门店订单页确认收款后增加发货按钮,允许选择门店店员作为发货人并创建发货单 - 新增接口 saveShopOrderDelivery 调用 POST /shop/shop-order-delivery 创建发货单 - 补全 shopStoreUser 模型字段,支持店员姓名、电话、角色等信息 - 订单列表支持发货操作按钮显示及样式,增加对应操作逻辑及UI弹窗选择发货人员 - 发货后订单状态置为已发货(deliveryStatus=20),同步更新发货时间和物流页显示 - 用户可选择门店全部店员(含经理)作为发货人员,确认完成按钮可与发货按钮共存,行为保持兼容 - 完成相关接口调用错误提示与加载状态处理,确保用户体验 - taro 构建通过,无类型错误
This commit is contained in:
@@ -74,5 +74,23 @@
|
||||
- **字段确认**:用户选择「送达时间」用 `order.deliveryTime`,且**保留**现有「发货时间」行 → `deliveryTime` 在两行各显示一次(用户明知并接受)
|
||||
- **改动**:第408-413行 `{order.expirationTime && ...过期时间}` 改为 `{order.deliveryTime && ...送达时间}`,数据用 `formatTime(order.deliveryTime)`
|
||||
|
||||
## 门店订单页「发货」按钮 + 选发货人员写 shopOrderDelivery(解决物流页发货信息为空)
|
||||
- **根因**:物流页 `pages/order/logistics.tsx` 的「发货信息」卡片依赖 `shopOrderDelivery` 发货单;门店「确认完成」只改订单状态、不建发货单 → `delivery` 为 null → 卡片不渲染(空白)
|
||||
- **方案**:在「确认收款」之后加「发货」按钮,选门店店员作为发货人,写入 `shopOrderDelivery`,从源头让物流页有数据
|
||||
- **后端可行性(已确认)**:`POST /shop/shop-order-delivery`(save) 已存在,实体字段全可空最少传 orderId;`listShopStoreUser({storeId})` 已存在;请求层自动注入 `TenantId` 请求头(无需手动传租户);`ShopOrderDelivery` 实体有 orderId/deliveryMethod/sendName/sendPhone/sendAddress
|
||||
- **前端改动**:
|
||||
- 新增 `src/api/shop/shopOrderDelivery/index.ts`:`saveShopOrderDelivery(data)` 封装 `POST /shop/shop-order-delivery`
|
||||
- 补全 `src/api/shop/shopStoreUser/model` 的 `ShopStoreUser` 类型缺的 `name`/`phone`/`roleType`/`storeName`/`image`(前端模型此前不完整,后端实体有)
|
||||
- `src/pages/store/orders/index.tsx`:
|
||||
- `OpType` 增加 `'ship'`;`OP_LABEL`/`OP_DESC` 补 ship
|
||||
- `getOrderActions`:已付款 且 `deliveryStatus<20`(或未发货)且 `orderStatus!==1` 时显示「发货」(紫色 `bg-purple-500`)
|
||||
- 新增状态 `showShipModal`/`clerkList`/`selectedClerkId`/`loadingClerks`/`shipping`
|
||||
- 新增 `openShipModal(order)`(拉 `listShopStoreUser({storeId: order.storeId})`)与 `handleConfirmShip()`(先 `saveShopOrderDelivery({orderId, deliveryMethod:20, sendName:clerk.name, sendPhone:clerk.phone, sendAddress:order.storeName})`,再 `updateShopOrder({orderId, deliveryStatus:20, deliveryTime})`)
|
||||
- 渲染「选择发货人员」底部弹层(店员列表可点选,确认发货按钮置灰直到选中)
|
||||
- **设计决策(用户确认)**:① 发货即把订单置「已发货」(deliveryStatus=20);② 发货人可选门店全部店员(经理+店员)
|
||||
- **物流页**:无需改动,`shopOrderDelivery` 存在即自动渲染发货人
|
||||
- **验证**:tsc 仅剩预存 `@tarojs/taro` 类型缺失环境报错,本文件无新错误
|
||||
- ⚠️ 行为说明:已付款未发货时「发货」与「确认完成」两个按钮同时显示;店员可跳过发货直接确认完成(该订单仍无发货单,物流页空白,与旧行为一致);如需强制「先发货才能确认完成」,可隐藏未发货订单的「确认完成」按钮(一行判断改动),待用户决定
|
||||
|
||||
### 验证
|
||||
- `npx taro build --type weapp` 构建成功
|
||||
|
||||
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