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` 在两行各显示一次(用户明知并接受)
|
- **字段确认**:用户选择「送达时间」用 `order.deliveryTime`,且**保留**现有「发货时间」行 → `deliveryTime` 在两行各显示一次(用户明知并接受)
|
||||||
- **改动**:第408-413行 `{order.expirationTime && ...过期时间}` 改为 `{order.deliveryTime && ...送达时间}`,数据用 `formatTime(order.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` 构建成功
|
- `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;
|
storeId?: number;
|
||||||
// 用户ID
|
// 用户ID
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
// 姓名
|
||||||
|
name?: string;
|
||||||
|
// 手机号
|
||||||
|
phone?: string;
|
||||||
|
// 头像
|
||||||
|
image?: string;
|
||||||
|
// 角色类型: 1-门店经理, 2-店员
|
||||||
|
roleType?: number;
|
||||||
|
// 门店名称(关联字段)
|
||||||
|
storeName?: string;
|
||||||
// 备注
|
// 备注
|
||||||
comments?: string;
|
comments?: string;
|
||||||
// 排序号
|
// 排序号
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import {View, Text, Image, ScrollView, Input, Textarea} from '@tarojs/components
|
|||||||
import Taro, {useDidShow} from '@tarojs/taro'
|
import Taro, {useDidShow} from '@tarojs/taro'
|
||||||
import {pageShopOrder, updateShopOrder, confirmOfflinePayment} from '@/api/shop/shopOrder'
|
import {pageShopOrder, updateShopOrder, confirmOfflinePayment} from '@/api/shop/shopOrder'
|
||||||
import type {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model'
|
import type {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model'
|
||||||
|
import {saveShopOrderDelivery} from '@/api/shop/shopOrderDelivery'
|
||||||
import {TenantId} from '@/config/app'
|
import {TenantId} from '@/config/app'
|
||||||
import {useNewOrderDetector} from '@/hooks/useNewOrderDetector'
|
import {useNewOrderDetector} from '@/hooks/useNewOrderDetector'
|
||||||
import { getCompressedImageUrl } from '@/utils/image'
|
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'
|
import { ensurePrivacyAuthorized } from '@/api/system/file'
|
||||||
|
|
||||||
definePageConfig({
|
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> = {
|
const OP_LABEL: Record<OpType, string> = {
|
||||||
confirmPay: '收款',
|
confirmPay: '收款',
|
||||||
complete: '已完成',
|
complete: '已完成',
|
||||||
editPrice: '金额',
|
editPrice: '金额',
|
||||||
|
ship: '发货',
|
||||||
}
|
}
|
||||||
|
|
||||||
const OP_DESC: Record<OpType, string> = {
|
const OP_DESC: Record<OpType, string> = {
|
||||||
confirmPay: '确认已收到线下付款,订单将进入待发货状态',
|
confirmPay: '确认已收到线下付款,订单将进入待发货状态',
|
||||||
complete: '同时变更支付状态为已付款、收货状态为已收货、订单状态为已完成',
|
complete: '同时变更支付状态为已付款、收货状态为已收货、订单状态为已完成',
|
||||||
editPrice: '修改订单实付金额',
|
editPrice: '修改订单实付金额',
|
||||||
|
ship: '选择发货人员并生成发货单,订单将进入已发货状态',
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 图片上传 ─────────────────────────────────────────────────────
|
// ─── 图片上传 ─────────────────────────────────────────────────────
|
||||||
@@ -135,6 +139,10 @@ function getOrderActions(order: ShopOrder): { label: string; type: OpType }[] {
|
|||||||
if (order.payStatus) {
|
if (order.payStatus) {
|
||||||
actions.push({label: '确认完成', type: 'complete'})
|
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
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,6 +169,13 @@ export default function StoreOrdersPage() {
|
|||||||
const [editPayPrice, setEditPayPrice] = useState('')
|
const [editPayPrice, setEditPayPrice] = useState('')
|
||||||
const [editReason, setEditReason] = 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 为已提交的搜索词
|
// 搜索:searchInput 受控输入框,searchKeyword 为已提交的搜索词
|
||||||
const [searchInput, setSearchInput] = useState('')
|
const [searchInput, setSearchInput] = useState('')
|
||||||
const [searchKeyword, setSearchKeyword] = 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) => {
|
const handleCloseOrder = (order: ShopOrder) => {
|
||||||
Taro.showModal({
|
Taro.showModal({
|
||||||
@@ -600,12 +667,18 @@ export default function StoreOrdersPage() {
|
|||||||
? 'bg-blue-500'
|
? 'bg-blue-500'
|
||||||
: act.type === 'editPrice'
|
: act.type === 'editPrice'
|
||||||
? 'bg-orange-500'
|
? 'bg-orange-500'
|
||||||
|
: act.type === 'ship'
|
||||||
|
? 'bg-purple-500'
|
||||||
: 'border border-blue-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 ${
|
<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>
|
}`}>{act.label}</Text>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
@@ -867,6 +940,85 @@ export default function StoreOrdersPage() {
|
|||||||
</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>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user