feat: 运单操作日志 Tab,并完善揽收下单与详情编辑体验
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m37s
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m37s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
640562654d
commit
5b6cb43457
@ -1181,6 +1181,71 @@ function mapExceptionEventItem(raw: any): OrderExceptionEventItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OrderOperationLogChangeItem {
|
||||||
|
path: string
|
||||||
|
label: string
|
||||||
|
before?: string | null
|
||||||
|
after?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderOperationLogItem {
|
||||||
|
id: number
|
||||||
|
orderId: number
|
||||||
|
serviceNo?: string
|
||||||
|
action: string
|
||||||
|
actionName: string
|
||||||
|
operatorId: number
|
||||||
|
operatorName?: string
|
||||||
|
operatorType?: string
|
||||||
|
source?: string
|
||||||
|
changes: OrderOperationLogChangeItem[]
|
||||||
|
remark?: string
|
||||||
|
createTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderOperationLogListResult {
|
||||||
|
items: OrderOperationLogItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapOperationLogItem(raw: any): OrderOperationLogItem {
|
||||||
|
const changesRaw = raw?.changes ?? raw?.Changes ?? []
|
||||||
|
const changes = Array.isArray(changesRaw)
|
||||||
|
? changesRaw.map((c: any) => ({
|
||||||
|
path: String(c?.path ?? c?.Path ?? ''),
|
||||||
|
label: String(c?.label ?? c?.Label ?? c?.path ?? c?.Path ?? ''),
|
||||||
|
before: c?.before ?? c?.Before ?? null,
|
||||||
|
after: c?.after ?? c?.After ?? null,
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
return {
|
||||||
|
id: Number(raw?.id ?? raw?.Id ?? 0),
|
||||||
|
orderId: Number(raw?.orderId ?? raw?.OrderId ?? 0),
|
||||||
|
serviceNo: raw?.serviceNo ?? raw?.ServiceNo,
|
||||||
|
action: String(raw?.action ?? raw?.Action ?? ''),
|
||||||
|
actionName: String(raw?.actionName ?? raw?.ActionName ?? ''),
|
||||||
|
operatorId: Number(raw?.operatorId ?? raw?.OperatorId ?? 0),
|
||||||
|
operatorName: raw?.operatorName ?? raw?.OperatorName,
|
||||||
|
operatorType: raw?.operatorType ?? raw?.OperatorType,
|
||||||
|
source: raw?.source ?? raw?.Source,
|
||||||
|
changes,
|
||||||
|
remark: raw?.remark ?? raw?.Remark,
|
||||||
|
createTime: raw?.createTime ?? raw?.CreateTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /order/operation-log/list */
|
||||||
|
export async function getOrderOperationLogList(
|
||||||
|
orderId: number | string,
|
||||||
|
): Promise<OrderOperationLogListResult> {
|
||||||
|
const res = await axiosInstance.get(orderUrl('/order/operation-log/list'), {
|
||||||
|
params: { orderId: Number(orderId) },
|
||||||
|
})
|
||||||
|
const data = unwrapData<any>(res.data) ?? {}
|
||||||
|
const itemsRaw = data?.items ?? data?.Items ?? []
|
||||||
|
const items = Array.isArray(itemsRaw) ? itemsRaw.map(mapOperationLogItem) : []
|
||||||
|
return { items }
|
||||||
|
}
|
||||||
|
|
||||||
/** GET /order/exception-events/list */
|
/** GET /order/exception-events/list */
|
||||||
export async function getOrderExceptionEventList(orderId: number | string): Promise<OrderExceptionEventListResult> {
|
export async function getOrderExceptionEventList(orderId: number | string): Promise<OrderExceptionEventListResult> {
|
||||||
const res = await axiosInstance.get(orderUrl('/order/exception-events/list'), {
|
const res = await axiosInstance.get(orderUrl('/order/exception-events/list'), {
|
||||||
|
|||||||
@ -73,6 +73,25 @@ export const schedulePickupOrder = (body: {
|
|||||||
export const reschedulePickupOrder = (body: { id: number | string; expectPickupTime: string }) =>
|
export const reschedulePickupOrder = (body: { id: number | string; expectPickupTime: string }) =>
|
||||||
postOk('/order/pickup-order/reschedule', body as unknown as Record<string, unknown>)
|
postOk('/order/pickup-order/reschedule', body as unknown as Record<string, unknown>)
|
||||||
|
|
||||||
|
export const updatePickupOrderInfo = (body: {
|
||||||
|
id: number | string
|
||||||
|
warehouseId?: number
|
||||||
|
senderAddressId?: number
|
||||||
|
expectPickupTime?: string
|
||||||
|
carrierService?: string
|
||||||
|
carrierWaybillNo?: string
|
||||||
|
remark?: string
|
||||||
|
}) => postOk('/order/pickup-order/update-info', body as unknown as Record<string, unknown>)
|
||||||
|
|
||||||
|
export const updatePickupOrderMeasure = (body: {
|
||||||
|
id: number | string
|
||||||
|
totalActualWeight: number
|
||||||
|
packageSizes: Array<{ length: number; width: number; height: number; count: number }>
|
||||||
|
}) => postOk('/order/pickup-order/update-measure', body as unknown as Record<string, unknown>)
|
||||||
|
|
||||||
|
export const recalcPickupOrderFee = (id: number | string) =>
|
||||||
|
postOk('/order/pickup-order/recalc-fee', { id })
|
||||||
|
|
||||||
export const markPickupPicked = (id: number | string) =>
|
export const markPickupPicked = (id: number | string) =>
|
||||||
postOk('/order/pickup-order/mark-picked', { id })
|
postOk('/order/pickup-order/mark-picked', { id })
|
||||||
|
|
||||||
|
|||||||
@ -896,6 +896,26 @@ export default {
|
|||||||
'order.pickupOrder.settlementStatus.pending': 'Pending',
|
'order.pickupOrder.settlementStatus.pending': 'Pending',
|
||||||
'order.pickupOrder.settlementStatus.settled': 'Settled',
|
'order.pickupOrder.settlementStatus.settled': 'Settled',
|
||||||
'order.pickupOrder.actions.reschedule': 'Reschedule',
|
'order.pickupOrder.actions.reschedule': 'Reschedule',
|
||||||
|
'order.pickupOrder.actions.editInfo': 'Edit pickup info',
|
||||||
|
'order.pickupOrder.actions.saveInfo': 'Save',
|
||||||
|
'order.pickupOrder.actions.cancelEdit': 'Cancel',
|
||||||
|
'order.pickupOrder.actions.editMeasure': 'Edit dimensions',
|
||||||
|
'order.pickupOrder.actions.saveMeasure': 'Save dimensions',
|
||||||
|
'order.pickupOrder.actions.recalcFee': 'Recalculate fee',
|
||||||
|
'order.pickupOrder.actions.addSize': 'Add size',
|
||||||
|
'order.pickupOrder.actions.removeSize': 'Remove',
|
||||||
|
'order.pickupOrder.form.carrierServicePlaceholder': 'Select carrier service',
|
||||||
|
'order.pickupOrder.form.sizeLength': 'L',
|
||||||
|
'order.pickupOrder.form.sizeWidth': 'W',
|
||||||
|
'order.pickupOrder.form.sizeHeight': 'H',
|
||||||
|
'order.pickupOrder.form.sizeCount': 'Qty',
|
||||||
|
'order.pickupOrder.message.infoSaveOk': 'Pickup info saved',
|
||||||
|
'order.pickupOrder.message.measureSaveOk': 'Dimensions saved',
|
||||||
|
'order.pickupOrder.message.recalcOk': 'Fee recalculated',
|
||||||
|
'order.pickupOrder.message.recalcNeedMeasure': 'Save weight and sizes before recalculating',
|
||||||
|
'order.pickupOrder.message.packageSizeRequired': 'Enter at least one valid size',
|
||||||
|
'order.pickupOrder.message.expectTimeRequired': 'Enter expected pickup time',
|
||||||
|
'order.pickupOrder.message.optionsLoadFailed': 'Failed to load options',
|
||||||
'order.pickupOrder.actions.batchConfirmFreight': 'Batch Confirm Freight',
|
'order.pickupOrder.actions.batchConfirmFreight': 'Batch Confirm Freight',
|
||||||
'order.pickupOrder.actions.batchSchedule': 'Batch Draft Forecast',
|
'order.pickupOrder.actions.batchSchedule': 'Batch Draft Forecast',
|
||||||
'order.pickupOrder.actions.copyServiceNos': 'Copy all service Nos.',
|
'order.pickupOrder.actions.copyServiceNos': 'Copy all service Nos.',
|
||||||
@ -1799,7 +1819,7 @@ export default {
|
|||||||
'order.myOrders.actions.markProcessed': 'Mark processed',
|
'order.myOrders.actions.markProcessed': 'Mark processed',
|
||||||
'order.myOrders.reallocate.modalTitle': 'Reallocate ({count} selected)',
|
'order.myOrders.reallocate.modalTitle': 'Reallocate ({count} selected)',
|
||||||
'order.myOrders.reallocate.hint':
|
'order.myOrders.reallocate.hint':
|
||||||
'Only recalculates payable and updates the service channel. Receivable and dimensions are unchanged. Paid orders are skipped. Changing channel clears provider service no. / tracking no.; re-forward downstream if needed.',
|
'Only recalculates payable and updates the service channel. Receivable and dimensions are unchanged. Available for predeclared / pending allocation / payment / ship (including paid). Changing channel clears provider service no. / tracking no.; re-forward downstream if needed.',
|
||||||
'order.myOrders.reallocate.section.scheme': 'Allocation scheme',
|
'order.myOrders.reallocate.section.scheme': 'Allocation scheme',
|
||||||
'order.myOrders.reallocate.section.channel': 'Designated channel',
|
'order.myOrders.reallocate.section.channel': 'Designated channel',
|
||||||
'order.myOrders.reallocate.schemeLabel': 'Allocation scheme',
|
'order.myOrders.reallocate.schemeLabel': 'Allocation scheme',
|
||||||
@ -2110,6 +2130,15 @@ export default {
|
|||||||
'order.myOrders.drawer.tabs.track': 'Tracking',
|
'order.myOrders.drawer.tabs.track': 'Tracking',
|
||||||
'order.myOrders.drawer.tabs.attachments': 'Attachments',
|
'order.myOrders.drawer.tabs.attachments': 'Attachments',
|
||||||
'order.myOrders.drawer.tabs.stockBind': 'Bound stock',
|
'order.myOrders.drawer.tabs.stockBind': 'Bound stock',
|
||||||
|
'order.myOrders.drawer.tabs.operationLog': 'Operation log',
|
||||||
|
'order.myOrders.drawer.operationLog.empty': 'No operation logs',
|
||||||
|
'order.myOrders.drawer.operationLog.refresh': 'Refresh',
|
||||||
|
'order.myOrders.drawer.operationLog.count': '{count} items',
|
||||||
|
'order.myOrders.drawer.operationLog.field': 'Field',
|
||||||
|
'order.myOrders.drawer.operationLog.before': 'Before',
|
||||||
|
'order.myOrders.drawer.operationLog.after': 'After',
|
||||||
|
'order.myOrders.drawer.operationLog.noChanges': 'No field changes',
|
||||||
|
'order.myOrders.drawer.operationLog.operator': 'Operator',
|
||||||
'order.myOrders.drawer.tabs.issueHandling': 'Issue handling',
|
'order.myOrders.drawer.tabs.issueHandling': 'Issue handling',
|
||||||
'order.myOrders.drawer.issueHandling.pendingCount': 'Pending {count}',
|
'order.myOrders.drawer.issueHandling.pendingCount': 'Pending {count}',
|
||||||
'order.myOrders.drawer.issueHandling.processedCount': 'Processed {count}',
|
'order.myOrders.drawer.issueHandling.processedCount': 'Processed {count}',
|
||||||
|
|||||||
@ -885,6 +885,26 @@ export default {
|
|||||||
'order.pickupOrder.settlementStatus.pending': '待结算',
|
'order.pickupOrder.settlementStatus.pending': '待结算',
|
||||||
'order.pickupOrder.settlementStatus.settled': '已结算',
|
'order.pickupOrder.settlementStatus.settled': '已结算',
|
||||||
'order.pickupOrder.actions.reschedule': '改约',
|
'order.pickupOrder.actions.reschedule': '改约',
|
||||||
|
'order.pickupOrder.actions.editInfo': '编辑揽收信息',
|
||||||
|
'order.pickupOrder.actions.saveInfo': '保存',
|
||||||
|
'order.pickupOrder.actions.cancelEdit': '取消',
|
||||||
|
'order.pickupOrder.actions.editMeasure': '编辑件重尺',
|
||||||
|
'order.pickupOrder.actions.saveMeasure': '保存件重尺',
|
||||||
|
'order.pickupOrder.actions.recalcFee': '重算费用',
|
||||||
|
'order.pickupOrder.actions.addSize': '添加尺寸',
|
||||||
|
'order.pickupOrder.actions.removeSize': '删除',
|
||||||
|
'order.pickupOrder.form.carrierServicePlaceholder': '请选择承运服务',
|
||||||
|
'order.pickupOrder.form.sizeLength': '长',
|
||||||
|
'order.pickupOrder.form.sizeWidth': '宽',
|
||||||
|
'order.pickupOrder.form.sizeHeight': '高',
|
||||||
|
'order.pickupOrder.form.sizeCount': '件数',
|
||||||
|
'order.pickupOrder.message.infoSaveOk': '揽收信息已保存',
|
||||||
|
'order.pickupOrder.message.measureSaveOk': '件重尺已保存',
|
||||||
|
'order.pickupOrder.message.recalcOk': '费用已重算',
|
||||||
|
'order.pickupOrder.message.recalcNeedMeasure': '请先保存实重与尺寸后再重算',
|
||||||
|
'order.pickupOrder.message.packageSizeRequired': '请至少填写一条有效尺寸',
|
||||||
|
'order.pickupOrder.message.expectTimeRequired': '请填写期望揽收时间',
|
||||||
|
'order.pickupOrder.message.optionsLoadFailed': '加载选项失败',
|
||||||
'order.pickupOrder.actions.batchConfirmFreight': '批量确认运费',
|
'order.pickupOrder.actions.batchConfirmFreight': '批量确认运费',
|
||||||
'order.pickupOrder.actions.batchSchedule': '批量草稿预报',
|
'order.pickupOrder.actions.batchSchedule': '批量草稿预报',
|
||||||
'order.pickupOrder.actions.copyServiceNos': '复制全部运单号',
|
'order.pickupOrder.actions.copyServiceNos': '复制全部运单号',
|
||||||
@ -1626,7 +1646,7 @@ export default {
|
|||||||
'order.waybillManage.importDownstream.failRow': '第 {row} 行({waybill}):{reason}',
|
'order.waybillManage.importDownstream.failRow': '第 {row} 行({waybill}):{reason}',
|
||||||
'order.myOrders.reallocate.modalTitle': '重新排货(已选 {count} 单)',
|
'order.myOrders.reallocate.modalTitle': '重新排货(已选 {count} 单)',
|
||||||
'order.myOrders.reallocate.hint':
|
'order.myOrders.reallocate.hint':
|
||||||
'仅重算应付并更新服务渠道,不改应收与件重尺;已付款订单会自动跳过。换渠道会清空服务商单号/跟踪号,需自行重新预报下游。',
|
'仅重算应付并更新服务渠道,不改应收与件重尺;已预报/待排货/待支付/待发货可用(含已付款)。换渠道会清空服务商单号/跟踪号,需自行重新预报下游。',
|
||||||
'order.myOrders.reallocate.section.scheme': '排货方案',
|
'order.myOrders.reallocate.section.scheme': '排货方案',
|
||||||
'order.myOrders.reallocate.section.channel': '指定渠道',
|
'order.myOrders.reallocate.section.channel': '指定渠道',
|
||||||
'order.myOrders.reallocate.schemeLabel': '排货方案',
|
'order.myOrders.reallocate.schemeLabel': '排货方案',
|
||||||
@ -1971,6 +1991,15 @@ export default {
|
|||||||
'order.myOrders.drawer.tabs.track': '轨迹',
|
'order.myOrders.drawer.tabs.track': '轨迹',
|
||||||
'order.myOrders.drawer.tabs.attachments': '附件',
|
'order.myOrders.drawer.tabs.attachments': '附件',
|
||||||
'order.myOrders.drawer.tabs.stockBind': '绑定包裹/库存',
|
'order.myOrders.drawer.tabs.stockBind': '绑定包裹/库存',
|
||||||
|
'order.myOrders.drawer.tabs.operationLog': '操作日志',
|
||||||
|
'order.myOrders.drawer.operationLog.empty': '暂无操作日志',
|
||||||
|
'order.myOrders.drawer.operationLog.refresh': '刷新',
|
||||||
|
'order.myOrders.drawer.operationLog.count': '{count} 条',
|
||||||
|
'order.myOrders.drawer.operationLog.field': '字段',
|
||||||
|
'order.myOrders.drawer.operationLog.before': '修改前',
|
||||||
|
'order.myOrders.drawer.operationLog.after': '修改后',
|
||||||
|
'order.myOrders.drawer.operationLog.noChanges': '无字段变更',
|
||||||
|
'order.myOrders.drawer.operationLog.operator': '操作人',
|
||||||
'order.myOrders.drawer.tabs.issueHandling': '问题处理',
|
'order.myOrders.drawer.tabs.issueHandling': '问题处理',
|
||||||
'order.myOrders.drawer.issueHandling.pendingCount': '待处理 {count}',
|
'order.myOrders.drawer.issueHandling.pendingCount': '待处理 {count}',
|
||||||
'order.myOrders.drawer.issueHandling.processedCount': '已处理 {count}',
|
'order.myOrders.drawer.issueHandling.processedCount': '已处理 {count}',
|
||||||
|
|||||||
@ -38,11 +38,10 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :label="t('order.myOrders.createPickup.carrierService')">
|
<a-form-item :label="t('order.myOrders.createPickup.carrierService')" required>
|
||||||
<a-select
|
<a-select
|
||||||
v-model="form.carrierService"
|
v-model="form.carrierService"
|
||||||
allow-search
|
allow-search
|
||||||
allow-clear
|
|
||||||
:loading="loadingRefs"
|
:loading="loadingRefs"
|
||||||
:placeholder="t('order.myOrders.createPickup.carrierServicePh')"
|
:placeholder="t('order.myOrders.createPickup.carrierServicePh')"
|
||||||
>
|
>
|
||||||
@ -178,7 +177,7 @@ async function open(rows: Array<{ id: number | string; serviceNo?: string; wareh
|
|||||||
}))
|
}))
|
||||||
form.warehouseId = whIds[0]
|
form.warehouseId = whIds[0]
|
||||||
form.expectPickupTime = defaultPickupTime()
|
form.expectPickupTime = defaultPickupTime()
|
||||||
form.carrierService = ''
|
form.carrierService = carrierServiceOptions.value[0]?.value || ''
|
||||||
form.remark = ''
|
form.remark = ''
|
||||||
form.senderAddressId = undefined
|
form.senderAddressId = undefined
|
||||||
|
|
||||||
@ -200,6 +199,10 @@ async function handleOk() {
|
|||||||
$baseMessage(t('order.myOrders.createPickup.needTime'), 'warning')
|
$baseMessage(t('order.myOrders.createPickup.needTime'), 'warning')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!form.carrierService?.trim()) {
|
||||||
|
$baseMessage(t('order.pickupOrder.message.carrierServiceRequired'), 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
@ -216,7 +219,7 @@ async function handleOk() {
|
|||||||
senderAddressId: form.senderAddressId,
|
senderAddressId: form.senderAddressId,
|
||||||
direction: 'forward',
|
direction: 'forward',
|
||||||
warehouseId: form.warehouseId,
|
warehouseId: form.warehouseId,
|
||||||
carrierService: form.carrierService || undefined,
|
carrierService: form.carrierService.trim(),
|
||||||
expectPickupTime: form.expectPickupTime,
|
expectPickupTime: form.expectPickupTime,
|
||||||
orderIds: orderIds.value,
|
orderIds: orderIds.value,
|
||||||
remark: form.remark || undefined,
|
remark: form.remark || undefined,
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="activeTab === 'basic' || activeTab === 'track' || activeTab === 'stockBind'"
|
v-if="activeTab === 'basic' || activeTab === 'track' || activeTab === 'stockBind' || activeTab === 'operationLog'"
|
||||||
class="drawer-tab-toolbar"
|
class="drawer-tab-toolbar"
|
||||||
>
|
>
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
@ -65,6 +65,17 @@
|
|||||||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="activeTab === 'operationLog'">
|
||||||
|
<a-tag v-if="operationLogList.length" size="small" color="arcoblue">
|
||||||
|
{{ $t('order.myOrders.drawer.operationLog.count', { count: operationLogList.length }) }}
|
||||||
|
</a-tag>
|
||||||
|
<a-button
|
||||||
|
class="app-btn-search"
|
||||||
|
:loading="operationLogLoading"
|
||||||
|
@click="loadOperationLogs"
|
||||||
|
>{{ $t('order.myOrders.drawer.operationLog.refresh') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -566,6 +577,63 @@
|
|||||||
hide-refresh
|
hide-refresh
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作日志 -->
|
||||||
|
<div v-if="activeTab === 'operationLog'" class="tab-panel operation-log-info">
|
||||||
|
<a-spin :loading="operationLogLoading" class="operation-log-spin">
|
||||||
|
<div v-if="operationLogList.length" class="operation-log-list">
|
||||||
|
<div
|
||||||
|
v-for="item in operationLogList"
|
||||||
|
:key="item.id"
|
||||||
|
class="operation-log-item"
|
||||||
|
>
|
||||||
|
<div class="operation-log-head" @click="toggleOperationLogExpand(item.id)">
|
||||||
|
<div class="operation-log-head-main">
|
||||||
|
<span class="operation-log-action">{{ item.actionName || item.action }}</span>
|
||||||
|
<a-tag v-if="item.changes?.length" size="small" color="orangered">
|
||||||
|
{{ item.changes.length }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
<div class="operation-log-head-meta">
|
||||||
|
<span>{{ item.operatorName || $t('order.myOrders.drawer.operationLog.operator') }}</span>
|
||||||
|
<span>{{ formatOperationLogTime(item.createTime) }}</span>
|
||||||
|
<IconDown v-if="!isOperationLogExpanded(item.id)" />
|
||||||
|
<IconUp v-else />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isOperationLogExpanded(item.id)" class="operation-log-body">
|
||||||
|
<p v-if="item.remark" class="operation-log-remark">{{ item.remark }}</p>
|
||||||
|
<div v-if="item.changes?.length" class="operation-log-changes">
|
||||||
|
<div class="operation-log-changes-header">
|
||||||
|
<span>{{ $t('order.myOrders.drawer.operationLog.field') }}</span>
|
||||||
|
<span>{{ $t('order.myOrders.drawer.operationLog.before') }}</span>
|
||||||
|
<span>{{ $t('order.myOrders.drawer.operationLog.after') }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(change, idx) in item.changes"
|
||||||
|
:key="`${item.id}-${change.path}-${idx}`"
|
||||||
|
class="operation-log-changes-row"
|
||||||
|
>
|
||||||
|
<span>{{ change.label || change.path }}</span>
|
||||||
|
<span>{{ change.before ?? '--' }}</span>
|
||||||
|
<span class="operation-log-after">{{ change.after ?? '--' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-empty
|
||||||
|
v-else
|
||||||
|
:description="$t('order.myOrders.drawer.operationLog.noChanges')"
|
||||||
|
:image-style="{ height: '40px' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-empty
|
||||||
|
v-else-if="!operationLogLoading"
|
||||||
|
:description="$t('order.myOrders.drawer.operationLog.empty')"
|
||||||
|
:image-style="{ height: '60px' }"
|
||||||
|
/>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DrawerPro>
|
</DrawerPro>
|
||||||
@ -608,6 +676,8 @@ import {
|
|||||||
postOrderWeightUpdate,
|
postOrderWeightUpdate,
|
||||||
postOrderRecalculateReceivable,
|
postOrderRecalculateReceivable,
|
||||||
postOrderRecalculatePayable,
|
postOrderRecalculatePayable,
|
||||||
|
getOrderOperationLogList,
|
||||||
|
type OrderOperationLogItem,
|
||||||
} from '@/api/order';
|
} from '@/api/order';
|
||||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||||
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
||||||
@ -983,8 +1053,13 @@ const tabList = computed(() => [
|
|||||||
{ key: 'track', label: t('order.myOrders.drawer.tabs.track') },
|
{ key: 'track', label: t('order.myOrders.drawer.tabs.track') },
|
||||||
{ key: 'attachments', label: t('order.myOrders.drawer.tabs.attachments') },
|
{ key: 'attachments', label: t('order.myOrders.drawer.tabs.attachments') },
|
||||||
{ key: 'stockBind', label: t('order.myOrders.drawer.tabs.stockBind') },
|
{ key: 'stockBind', label: t('order.myOrders.drawer.tabs.stockBind') },
|
||||||
|
{ key: 'operationLog', label: t('order.myOrders.drawer.tabs.operationLog') },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const operationLogList = ref<OrderOperationLogItem[]>([]);
|
||||||
|
const operationLogLoading = ref(false);
|
||||||
|
const operationLogExpandedIds = ref<Set<number>>(new Set());
|
||||||
|
|
||||||
const slidingTabItems = computed(() =>
|
const slidingTabItems = computed(() =>
|
||||||
tabList.value.map((tab) => ({ key: tab.key, label: tab.label })),
|
tabList.value.map((tab) => ({ key: tab.key, label: tab.label })),
|
||||||
);
|
);
|
||||||
@ -1987,6 +2062,45 @@ async function handleRecalculatePayable() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadOperationLogs() {
|
||||||
|
const id = orderId.value;
|
||||||
|
if (!id) {
|
||||||
|
operationLogList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
operationLogLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getOrderOperationLogList(id);
|
||||||
|
operationLogList.value = res.items || [];
|
||||||
|
operationLogExpandedIds.value = new Set(
|
||||||
|
operationLogList.value.slice(0, 3).map((x) => x.id),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
operationLogList.value = [];
|
||||||
|
} finally {
|
||||||
|
operationLogLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOperationLogExpanded(id: number) {
|
||||||
|
return operationLogExpandedIds.value.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOperationLogExpand(id: number) {
|
||||||
|
const next = new Set(operationLogExpandedIds.value);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
operationLogExpandedIds.value = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatOperationLogTime(value?: string) {
|
||||||
|
if (!value) return '--';
|
||||||
|
const d = new Date(value);
|
||||||
|
if (Number.isNaN(d.getTime())) return String(value);
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
function handleTabChange(tabKey: string) {
|
function handleTabChange(tabKey: string) {
|
||||||
const next = resolveDrawerTab(tabKey);
|
const next = resolveDrawerTab(tabKey);
|
||||||
if (next !== tabKey) {
|
if (next !== tabKey) {
|
||||||
@ -2003,6 +2117,9 @@ function handleTabChange(tabKey: string) {
|
|||||||
weightViewMode.value = 'all';
|
weightViewMode.value = 'all';
|
||||||
void ensureFeeItemDictLoaded();
|
void ensureFeeItemDictLoaded();
|
||||||
}
|
}
|
||||||
|
if (next === 'operationLog') {
|
||||||
|
void loadOperationLogs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
@ -2024,13 +2141,16 @@ watch(
|
|||||||
if (activeTab.value === 'weightFee') {
|
if (activeTab.value === 'weightFee') {
|
||||||
void ensureFeeItemDictLoaded();
|
void ensureFeeItemDictLoaded();
|
||||||
}
|
}
|
||||||
|
if (activeTab.value === 'operationLog') {
|
||||||
|
void loadOperationLogs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true, deep: true }
|
{ immediate: true, deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openDrawer: (opts?: { tab?: 'basic' | 'weightFee' | 'weight' | 'fee' | 'track' | 'attachments' | 'stockBind' }) => {
|
openDrawer: (opts?: { tab?: 'basic' | 'weightFee' | 'weight' | 'fee' | 'track' | 'attachments' | 'stockBind' | 'operationLog' }) => {
|
||||||
const tab = resolveDrawerTab(opts?.tab);
|
const tab = resolveDrawerTab(opts?.tab);
|
||||||
activeTab.value = tab;
|
activeTab.value = tab;
|
||||||
basicViewMode.value = 'all';
|
basicViewMode.value = 'all';
|
||||||
@ -3117,4 +3237,102 @@ defineExpose({
|
|||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.operation-log-spin {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-item {
|
||||||
|
border: 1px solid var(--color-border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-fill-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-head-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-action {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-head-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-body {
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
border-top: 1px solid var(--color-border-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-remark {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-changes {
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 1px solid var(--color-border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-changes-header,
|
||||||
|
.operation-log-changes-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.1fr 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-changes-header {
|
||||||
|
background: var(--color-fill-2);
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-changes-row {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
border-top: 1px solid var(--color-border-2);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-after {
|
||||||
|
color: var(--app-primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,13 +1,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
import DrawerPro from '@/components/DrawerPro/index.vue'
|
import DrawerPro from '@/components/DrawerPro/index.vue'
|
||||||
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||||
import useDrawer from '@/components/DrawerPro/useDrawer'
|
import useDrawer from '@/components/DrawerPro/useDrawer'
|
||||||
import { formatDateTime } from '@/utils/warehouseListMapper'
|
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
|
||||||
|
import { getSenderAddressList } from '@/api/order/senderAddress'
|
||||||
|
import { getWarehouseList } from '@/api/system/warehouse'
|
||||||
|
import {
|
||||||
|
recalcPickupOrderFee,
|
||||||
|
updatePickupOrderInfo,
|
||||||
|
updatePickupOrderMeasure,
|
||||||
|
} from '@/api/order/pickupOrder'
|
||||||
|
import { loadDomesticPickupProductOptions } from '@/utils/domesticPickupProducts'
|
||||||
|
|
||||||
defineOptions({ name: 'PickupOrderDetailDrawer' })
|
defineOptions({ name: 'PickupOrderDetailDrawer' })
|
||||||
|
|
||||||
|
type SelectOpt = { value: number | string; label: string }
|
||||||
|
type SizeRow = { length?: number; width?: number; height?: number; count?: number }
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
loading: boolean
|
loading: boolean
|
||||||
detail: any
|
detail: any
|
||||||
@ -25,17 +38,58 @@ const emit = defineEmits<{
|
|||||||
close: []
|
close: []
|
||||||
copyServiceNos: []
|
copyServiceNos: []
|
||||||
openOrder: [orderId: number]
|
openOrder: [orderId: number]
|
||||||
|
updated: [detail: any]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const [register, { openDrawer, closeDrawer, setDrawerProps }] = useDrawer() as any
|
const [register, { openDrawer, closeDrawer, setDrawerProps }] = useDrawer() as any
|
||||||
const detailTab = ref('info')
|
const detailTab = ref('info')
|
||||||
|
|
||||||
|
const infoEditing = ref(false)
|
||||||
|
const measureEditing = ref(false)
|
||||||
|
const infoSaving = ref(false)
|
||||||
|
const measureSaving = ref(false)
|
||||||
|
const recalcLoading = ref(false)
|
||||||
|
const optionsLoading = ref(false)
|
||||||
|
|
||||||
|
const warehouseOptions = ref<SelectOpt[]>([])
|
||||||
|
const senderOptions = ref<SelectOpt[]>([])
|
||||||
|
const carrierServiceOptions = ref<SelectOpt[]>([])
|
||||||
|
|
||||||
|
const infoForm = reactive({
|
||||||
|
warehouseId: undefined as number | undefined,
|
||||||
|
senderAddressId: undefined as number | undefined,
|
||||||
|
expectPickupTime: '' as string,
|
||||||
|
carrierService: '' as string,
|
||||||
|
carrierWaybillNo: '' as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
const measureForm = reactive({
|
||||||
|
totalActualWeight: undefined as number | undefined,
|
||||||
|
packageSizes: [] as SizeRow[],
|
||||||
|
})
|
||||||
|
|
||||||
const detailPayStatus = computed(() =>
|
const detailPayStatus = computed(() =>
|
||||||
String(props.detail?.payStatus ?? props.detail?.PayStatus ?? ''),
|
String(props.detail?.payStatus ?? props.detail?.PayStatus ?? ''),
|
||||||
)
|
)
|
||||||
const detailStatus = computed(() => String(props.detail?.status ?? props.detail?.Status ?? ''))
|
const detailStatus = computed(() => String(props.detail?.status ?? props.detail?.Status ?? ''))
|
||||||
|
|
||||||
|
const canEditBase = computed(() => {
|
||||||
|
const pay = detailPayStatus.value.toLowerCase()
|
||||||
|
const status = detailStatus.value.toLowerCase()
|
||||||
|
if (pay === 'paid') return false
|
||||||
|
if (status === 'cancelled' || status === 'failed') return false
|
||||||
|
return Boolean(props.detail)
|
||||||
|
})
|
||||||
|
|
||||||
|
const canEditInfo = computed(() => {
|
||||||
|
if (!canEditBase.value) return false
|
||||||
|
const status = detailStatus.value.toLowerCase()
|
||||||
|
return status === 'draft' || status === 'scheduled'
|
||||||
|
})
|
||||||
|
|
||||||
|
const canEditMeasure = computed(() => canEditBase.value)
|
||||||
|
|
||||||
const orderColumns = computed(() => [
|
const orderColumns = computed(() => [
|
||||||
{
|
{
|
||||||
title: t('order.pickupOrder.table.serviceNo'),
|
title: t('order.pickupOrder.table.serviceNo'),
|
||||||
@ -69,6 +123,7 @@ watch(
|
|||||||
() => props.detail?.id ?? props.detail?.Id,
|
() => props.detail?.id ?? props.detail?.Id,
|
||||||
() => {
|
() => {
|
||||||
detailTab.value = 'info'
|
detailTab.value = 'info'
|
||||||
|
resetEditState()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -78,6 +133,10 @@ watch(tabItems, (items) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(detailTab, () => {
|
||||||
|
resetEditState()
|
||||||
|
})
|
||||||
|
|
||||||
function pick(d: any, a: string, b: string) {
|
function pick(d: any, a: string, b: string) {
|
||||||
return d?.[a] ?? d?.[b]
|
return d?.[a] ?? d?.[b]
|
||||||
}
|
}
|
||||||
@ -94,18 +153,257 @@ function fmtMoney(v: unknown) {
|
|||||||
return Number.isFinite(n) ? n.toFixed(2) : String(v)
|
return Number.isFinite(n) ? n.toFixed(2) : String(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePackageSizes(raw: unknown): SizeRow[] {
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return raw.map((x: any) => ({
|
||||||
|
length: Number(x?.length ?? x?.Length) || undefined,
|
||||||
|
width: Number(x?.width ?? x?.Width) || undefined,
|
||||||
|
height: Number(x?.height ?? x?.Height) || undefined,
|
||||||
|
count: Number(x?.count ?? x?.Count) || 1,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (typeof raw === 'string' && raw.trim()) {
|
||||||
|
return raw
|
||||||
|
.split(/[;;]/)
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((p) => {
|
||||||
|
const m = p.match(/([\d.]+)\s*[x×*]\s*([\d.]+)\s*[x×*]\s*([\d.]+)(?:\s*[x×*]\s*(\d+))?/i)
|
||||||
|
if (!m) return { length: undefined, width: undefined, height: undefined, count: 1 }
|
||||||
|
return {
|
||||||
|
length: Number(m[1]) || undefined,
|
||||||
|
width: Number(m[2]) || undefined,
|
||||||
|
height: Number(m[3]) || undefined,
|
||||||
|
count: Number(m[4]) || 1,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
function formatPackageSizes(raw: unknown) {
|
function formatPackageSizes(raw: unknown) {
|
||||||
const text = String(raw ?? '').trim()
|
const rows = normalizePackageSizes(raw)
|
||||||
if (!text) return '-'
|
if (!rows.length) {
|
||||||
return text
|
const text = typeof raw === 'string' ? raw.trim() : ''
|
||||||
.split(';')
|
return text || '-'
|
||||||
.map((p) => p.trim())
|
}
|
||||||
|
return rows
|
||||||
|
.map((r) => {
|
||||||
|
const dims = [r.length, r.width, r.height].filter((n) => n != null && Number(n) > 0)
|
||||||
|
if (dims.length < 3) return ''
|
||||||
|
const base = `${r.length}×${r.width}×${r.height}`
|
||||||
|
return r.count && r.count > 1 ? `${base}×${r.count}` : base
|
||||||
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(';')
|
.join(';') || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetEditState() {
|
||||||
|
infoEditing.value = false
|
||||||
|
measureEditing.value = false
|
||||||
|
infoSaving.value = false
|
||||||
|
measureSaving.value = false
|
||||||
|
recalcLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureEditOptions() {
|
||||||
|
if (warehouseOptions.value.length && senderOptions.value.length && carrierServiceOptions.value.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
optionsLoading.value = true
|
||||||
|
try {
|
||||||
|
const customerId = Number(pick(props.detail, 'customerId', 'CustomerId') || 0)
|
||||||
|
const senderBody: Record<string, unknown> = {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 200,
|
||||||
|
addressType: 'pickup',
|
||||||
|
}
|
||||||
|
if (props.isAdmin && customerId > 0) {
|
||||||
|
senderBody.customerId = customerId
|
||||||
|
}
|
||||||
|
|
||||||
|
const [senderRes, warehouseRes, pickupProducts] = await Promise.all([
|
||||||
|
getSenderAddressList(senderBody),
|
||||||
|
getWarehouseList({ page: 1, pageSize: 500 }),
|
||||||
|
loadDomesticPickupProductOptions(),
|
||||||
|
])
|
||||||
|
|
||||||
|
senderOptions.value = (senderRes.items || [])
|
||||||
|
.map((item: any) => {
|
||||||
|
const row = normalizeListRow(item)
|
||||||
|
const id = Number(pickField(row, 'id', 'Id') || 0)
|
||||||
|
const name = String(
|
||||||
|
pickField(row, 'addressName', 'AddressName') ||
|
||||||
|
pickField(row, 'senderName', 'SenderName') ||
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
const phone = String(pickField(row, 'phone', 'Phone') || '')
|
||||||
|
return { value: id, label: phone ? `${name} · ${phone}` : name }
|
||||||
|
})
|
||||||
|
.filter((x) => Number(x.value) > 0)
|
||||||
|
|
||||||
|
warehouseOptions.value = (warehouseRes.items || [])
|
||||||
|
.map((item: any) => {
|
||||||
|
const id = Number(item?.id ?? item?.Id ?? 0)
|
||||||
|
const name = String(item?.warehouseName ?? item?.WarehouseName ?? id)
|
||||||
|
return { value: id, label: name }
|
||||||
|
})
|
||||||
|
.filter((x) => Number(x.value) > 0)
|
||||||
|
|
||||||
|
carrierServiceOptions.value = pickupProducts.map((p) => ({ value: p.value, label: p.label }))
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.warning(e?.message || t('order.pickupOrder.message.optionsLoadFailed'))
|
||||||
|
} finally {
|
||||||
|
optionsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startInfoEdit() {
|
||||||
|
if (!canEditInfo.value || !props.detail) return
|
||||||
|
await ensureEditOptions()
|
||||||
|
const d = props.detail
|
||||||
|
infoForm.warehouseId = Number(pick(d, 'warehouseId', 'WarehouseId') || 0) || undefined
|
||||||
|
infoForm.senderAddressId = Number(pick(d, 'senderAddressId', 'SenderAddressId') || 0) || undefined
|
||||||
|
const t0 = pick(d, 'expectPickupTime', 'ExpectPickupTime')
|
||||||
|
infoForm.expectPickupTime = t0 ? dayjs(t0).format('YYYY-MM-DD HH:mm:ss') : ''
|
||||||
|
infoForm.carrierService = String(pick(d, 'carrierService', 'CarrierService') || '')
|
||||||
|
infoForm.carrierWaybillNo = String(pick(d, 'carrierWaybillNo', 'CarrierWaybillNo') || '')
|
||||||
|
infoEditing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelInfoEdit() {
|
||||||
|
infoEditing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveInfoEdit() {
|
||||||
|
if (!props.detail) return
|
||||||
|
if (!infoForm.warehouseId) {
|
||||||
|
Message.warning(t('order.pickupOrder.message.warehouseRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!infoForm.senderAddressId) {
|
||||||
|
Message.warning(t('order.pickupOrder.message.senderRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!infoForm.expectPickupTime) {
|
||||||
|
Message.warning(t('order.pickupOrder.message.expectTimeRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
infoSaving.value = true
|
||||||
|
try {
|
||||||
|
const id = pick(props.detail, 'id', 'Id')
|
||||||
|
const res = await updatePickupOrderInfo({
|
||||||
|
id,
|
||||||
|
warehouseId: infoForm.warehouseId,
|
||||||
|
senderAddressId: infoForm.senderAddressId,
|
||||||
|
expectPickupTime: infoForm.expectPickupTime,
|
||||||
|
carrierService: infoForm.carrierService || undefined,
|
||||||
|
carrierWaybillNo: infoForm.carrierWaybillNo,
|
||||||
|
})
|
||||||
|
infoEditing.value = false
|
||||||
|
emit('updated', res)
|
||||||
|
Message.success(t('order.pickupOrder.message.infoSaveOk'))
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || t('order.pickupOrder.message.actionFailed'))
|
||||||
|
} finally {
|
||||||
|
infoSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMeasureEdit() {
|
||||||
|
if (!canEditMeasure.value || !props.detail) return
|
||||||
|
const d = props.detail
|
||||||
|
const weight = Number(pick(d, 'totalActualWeight', 'TotalActualWeight'))
|
||||||
|
measureForm.totalActualWeight = Number.isFinite(weight) && weight > 0 ? weight : undefined
|
||||||
|
const sizes = normalizePackageSizes(pick(d, 'packageSizes', 'PackageSizes'))
|
||||||
|
measureForm.packageSizes = sizes.length
|
||||||
|
? sizes.map((x) => ({ ...x }))
|
||||||
|
: [{ length: undefined, width: undefined, height: undefined, count: 1 }]
|
||||||
|
measureEditing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelMeasureEdit() {
|
||||||
|
measureEditing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSizeRow() {
|
||||||
|
measureForm.packageSizes.push({ length: undefined, width: undefined, height: undefined, count: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSizeRow(index: number) {
|
||||||
|
if (measureForm.packageSizes.length <= 1) return
|
||||||
|
measureForm.packageSizes.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveMeasureEdit() {
|
||||||
|
if (!props.detail) return
|
||||||
|
const weight = Number(measureForm.totalActualWeight)
|
||||||
|
if (!(weight > 0)) {
|
||||||
|
Message.warning(t('order.pickupOrder.message.weightRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sizes = measureForm.packageSizes
|
||||||
|
.map((r) => ({
|
||||||
|
length: Number(r.length) || 0,
|
||||||
|
width: Number(r.width) || 0,
|
||||||
|
height: Number(r.height) || 0,
|
||||||
|
count: Number(r.count) > 0 ? Number(r.count) : 1,
|
||||||
|
}))
|
||||||
|
.filter((r) => r.length > 0 && r.width > 0 && r.height > 0)
|
||||||
|
if (!sizes.length) {
|
||||||
|
Message.warning(t('order.pickupOrder.message.packageSizeRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
measureSaving.value = true
|
||||||
|
try {
|
||||||
|
const id = pick(props.detail, 'id', 'Id')
|
||||||
|
const res = await updatePickupOrderMeasure({
|
||||||
|
id,
|
||||||
|
totalActualWeight: weight,
|
||||||
|
packageSizes: sizes,
|
||||||
|
})
|
||||||
|
measureEditing.value = false
|
||||||
|
emit('updated', res)
|
||||||
|
Message.success(t('order.pickupOrder.message.measureSaveOk'))
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || t('order.pickupOrder.message.actionFailed'))
|
||||||
|
} finally {
|
||||||
|
measureSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRecalcFee() {
|
||||||
|
if (!props.detail || !canEditMeasure.value) return
|
||||||
|
const weight = Number(pick(props.detail, 'totalActualWeight', 'TotalActualWeight'))
|
||||||
|
const sizes = normalizePackageSizes(pick(props.detail, 'packageSizes', 'PackageSizes'))
|
||||||
|
const carrier = String(pick(props.detail, 'carrierService', 'CarrierService') || '').trim()
|
||||||
|
if (!(weight > 0) || !sizes.length) {
|
||||||
|
Message.warning(t('order.pickupOrder.message.recalcNeedMeasure'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!carrier) {
|
||||||
|
Message.warning(t('order.pickupOrder.message.carrierServiceRequired'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recalcLoading.value = true
|
||||||
|
try {
|
||||||
|
const id = pick(props.detail, 'id', 'Id')
|
||||||
|
const res = await recalcPickupOrderFee(id)
|
||||||
|
emit('updated', res)
|
||||||
|
Message.success(t('order.pickupOrder.message.recalcOk'))
|
||||||
|
if (props.isAdmin) {
|
||||||
|
detailTab.value = 'fee'
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || t('order.pickupOrder.message.actionFailed'))
|
||||||
|
} finally {
|
||||||
|
recalcLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
detailTab.value = 'info'
|
detailTab.value = 'info'
|
||||||
|
resetEditState()
|
||||||
setDrawerProps({
|
setDrawerProps({
|
||||||
title: t('order.pickupOrder.detail.title'),
|
title: t('order.pickupOrder.detail.title'),
|
||||||
width: 720,
|
width: 720,
|
||||||
@ -117,6 +415,7 @@ function open() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
resetEditState()
|
||||||
closeDrawer()
|
closeDrawer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,49 +456,223 @@ defineExpose({ open, close })
|
|||||||
<SlidingTabs v-model="detailTab" :items="tabItems" size="mini" class="pk-detail-tabs" />
|
<SlidingTabs v-model="detailTab" :items="tabItems" size="mini" class="pk-detail-tabs" />
|
||||||
|
|
||||||
<div class="pk-detail-body">
|
<div class="pk-detail-body">
|
||||||
<div v-show="detailTab === 'info'" class="pk-detail-grid">
|
<div v-show="detailTab === 'info'">
|
||||||
<div class="pk-detail-field">
|
<div class="pk-detail-toolbar">
|
||||||
<span>{{ t('order.pickupOrder.table.carrierWaybillNo') }}</span>
|
<template v-if="canEditInfo && !infoEditing">
|
||||||
<b>{{ detail.carrierWaybillNo || detail.CarrierWaybillNo || '-' }}</b>
|
<a-button type="primary" size="mini" @click="startInfoEdit">
|
||||||
|
{{ t('order.pickupOrder.actions.editInfo') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="infoEditing">
|
||||||
|
<a-button size="mini" :disabled="infoSaving" @click="cancelInfoEdit">
|
||||||
|
{{ t('order.pickupOrder.actions.cancelEdit') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" size="mini" :loading="infoSaving" @click="saveInfoEdit">
|
||||||
|
{{ t('order.pickupOrder.actions.saveInfo') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="pk-detail-field">
|
|
||||||
<span>{{ t('order.pickupOrder.table.warehouse') }}</span>
|
<div v-if="!infoEditing" class="pk-detail-grid">
|
||||||
<b>{{ detail.warehouseName || detail.WarehouseName || '-' }}</b>
|
<div class="pk-detail-field">
|
||||||
</div>
|
<span>{{ t('order.pickupOrder.table.carrierWaybillNo') }}</span>
|
||||||
<div class="pk-detail-field">
|
<b>{{ detail.carrierWaybillNo || detail.CarrierWaybillNo || '-' }}</b>
|
||||||
<span>{{ t('order.pickupOrder.table.expectTime') }}</span>
|
</div>
|
||||||
<b>{{ formatDateTime(detail.expectPickupTime || detail.ExpectPickupTime) }}</b>
|
<div class="pk-detail-field">
|
||||||
</div>
|
<span>{{ t('order.pickupOrder.table.warehouse') }}</span>
|
||||||
<div class="pk-detail-field">
|
<b>{{ detail.warehouseName || detail.WarehouseName || '-' }}</b>
|
||||||
<span>{{ t('order.pickupOrder.table.carrierService') }}</span>
|
</div>
|
||||||
<b>{{ carrierServiceText }}</b>
|
<div class="pk-detail-field">
|
||||||
</div>
|
<span>{{ t('order.pickupOrder.table.expectTime') }}</span>
|
||||||
<div class="pk-detail-field">
|
<b>{{ formatDateTime(detail.expectPickupTime || detail.ExpectPickupTime) }}</b>
|
||||||
<span>{{ t('order.pickupOrder.table.settlement') }}</span>
|
</div>
|
||||||
<b>{{ settlementText }}</b>
|
<div class="pk-detail-field">
|
||||||
</div>
|
<span>{{ t('order.pickupOrder.table.carrierService') }}</span>
|
||||||
<div class="pk-detail-field pk-detail-grid-span">
|
<b>{{ carrierServiceText }}</b>
|
||||||
<span>{{ t('order.myOrders.createPickup.senderAddress') }}</span>
|
</div>
|
||||||
<b>{{ senderText }}</b>
|
<div class="pk-detail-field">
|
||||||
|
<span>{{ t('order.pickupOrder.table.settlement') }}</span>
|
||||||
|
<b>{{ settlementText }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="pk-detail-field pk-detail-grid-span">
|
||||||
|
<span>{{ t('order.myOrders.createPickup.senderAddress') }}</span>
|
||||||
|
<b>{{ senderText }}</b>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a-spin v-else :loading="optionsLoading">
|
||||||
|
<a-form layout="vertical" class="pk-edit-form">
|
||||||
|
<a-form-item :label="t('order.pickupOrder.table.warehouse')" required>
|
||||||
|
<a-select
|
||||||
|
v-model="infoForm.warehouseId"
|
||||||
|
allow-search
|
||||||
|
:placeholder="t('order.pickupOrder.form.warehousePlaceholder')"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="opt in warehouseOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('order.pickupOrder.form.senderAddress')" required>
|
||||||
|
<a-select
|
||||||
|
v-model="infoForm.senderAddressId"
|
||||||
|
allow-search
|
||||||
|
:placeholder="t('order.pickupOrder.form.senderAddressPlaceholder')"
|
||||||
|
>
|
||||||
|
<a-option v-for="opt in senderOptions" :key="opt.value" :value="opt.value">
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('order.pickupOrder.table.expectTime')" required>
|
||||||
|
<a-date-picker
|
||||||
|
v-model="infoForm.expectPickupTime"
|
||||||
|
show-time
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('order.pickupOrder.table.carrierService')">
|
||||||
|
<a-select
|
||||||
|
v-model="infoForm.carrierService"
|
||||||
|
allow-search
|
||||||
|
allow-clear
|
||||||
|
:placeholder="t('order.pickupOrder.form.carrierServicePlaceholder')"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="opt in carrierServiceOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('order.pickupOrder.table.carrierWaybillNo')">
|
||||||
|
<a-input
|
||||||
|
v-model="infoForm.carrierWaybillNo"
|
||||||
|
:placeholder="t('order.pickupOrder.form.carrierWaybillNoPh')"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-spin>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="detailTab === 'measure'" class="pk-detail-grid">
|
<div v-show="detailTab === 'measure'">
|
||||||
<div class="pk-detail-field">
|
<div class="pk-detail-toolbar">
|
||||||
<span>{{ t('order.pickupOrder.table.actualWeightKg') }}</span>
|
<template v-if="canEditMeasure && !measureEditing">
|
||||||
<b>{{ fmtNum(pick(detail, 'actualWeightKg', 'ActualWeightKg')) }}</b>
|
<a-button type="primary" size="mini" @click="startMeasureEdit">
|
||||||
|
{{ t('order.pickupOrder.actions.editMeasure') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="mini"
|
||||||
|
class="app-btn-primary-soft"
|
||||||
|
:loading="recalcLoading"
|
||||||
|
@click="handleRecalcFee"
|
||||||
|
>
|
||||||
|
{{ t('order.pickupOrder.actions.recalcFee') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="measureEditing">
|
||||||
|
<a-button size="mini" :disabled="measureSaving" @click="cancelMeasureEdit">
|
||||||
|
{{ t('order.pickupOrder.actions.cancelEdit') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="mini"
|
||||||
|
:loading="measureSaving"
|
||||||
|
@click="saveMeasureEdit"
|
||||||
|
>
|
||||||
|
{{ t('order.pickupOrder.actions.saveMeasure') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="pk-detail-field">
|
|
||||||
<span>{{ t('order.pickupOrder.table.chargeableWeight') }}</span>
|
<div v-if="!measureEditing" class="pk-detail-grid">
|
||||||
<b>{{ fmtNum(pick(detail, 'chargeableWeightKg', 'ChargeableWeightKg')) }}</b>
|
<div class="pk-detail-field">
|
||||||
|
<span>{{ t('order.pickupOrder.table.actualWeightKg') }}</span>
|
||||||
|
<b>{{ fmtNum(pick(detail, 'totalActualWeight', 'TotalActualWeight')) }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="pk-detail-field">
|
||||||
|
<span>{{ t('order.pickupOrder.table.chargeableWeight') }}</span>
|
||||||
|
<b>{{ fmtNum(pick(detail, 'chargeableWeight', 'ChargeableWeight')) }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="pk-detail-field">
|
||||||
|
<span>{{ t('order.pickupOrder.table.volumetricWeight') }}</span>
|
||||||
|
<b>{{ fmtNum(pick(detail, 'volumetricWeight', 'VolumetricWeight')) }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="pk-detail-field pk-detail-grid-span">
|
||||||
|
<span>{{ t('order.pickupOrder.detail.packageSizes') }}</span>
|
||||||
|
<b>{{ formatPackageSizes(pick(detail, 'packageSizes', 'PackageSizes')) }}</b>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pk-detail-field">
|
|
||||||
<span>{{ t('order.pickupOrder.table.volumetricWeight') }}</span>
|
<div v-else class="pk-edit-form">
|
||||||
<b>{{ fmtNum(pick(detail, 'volumetricWeightKg', 'VolumetricWeightKg')) }}</b>
|
<a-form layout="vertical">
|
||||||
</div>
|
<a-form-item :label="t('order.pickupOrder.table.actualWeightKg')" required>
|
||||||
<div class="pk-detail-field pk-detail-grid-span">
|
<a-input-number
|
||||||
<span>{{ t('order.pickupOrder.detail.packageSizes') }}</span>
|
v-model="measureForm.totalActualWeight"
|
||||||
<b>{{ formatPackageSizes(pick(detail, 'packageSizes', 'PackageSizes')) }}</b>
|
:min="0.001"
|
||||||
|
:precision="3"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="t('order.pickupOrder.detail.packageSizes')" required>
|
||||||
|
<div class="pk-size-list">
|
||||||
|
<div
|
||||||
|
v-for="(row, idx) in measureForm.packageSizes"
|
||||||
|
:key="idx"
|
||||||
|
class="pk-size-row"
|
||||||
|
>
|
||||||
|
<a-input-number
|
||||||
|
v-model="row.length"
|
||||||
|
:min="0.1"
|
||||||
|
:precision="1"
|
||||||
|
:placeholder="t('order.pickupOrder.form.sizeLength')"
|
||||||
|
/>
|
||||||
|
<span class="pk-size-sep">×</span>
|
||||||
|
<a-input-number
|
||||||
|
v-model="row.width"
|
||||||
|
:min="0.1"
|
||||||
|
:precision="1"
|
||||||
|
:placeholder="t('order.pickupOrder.form.sizeWidth')"
|
||||||
|
/>
|
||||||
|
<span class="pk-size-sep">×</span>
|
||||||
|
<a-input-number
|
||||||
|
v-model="row.height"
|
||||||
|
:min="0.1"
|
||||||
|
:precision="1"
|
||||||
|
:placeholder="t('order.pickupOrder.form.sizeHeight')"
|
||||||
|
/>
|
||||||
|
<span class="pk-size-sep">×</span>
|
||||||
|
<a-input-number
|
||||||
|
v-model="row.count"
|
||||||
|
:min="1"
|
||||||
|
:precision="0"
|
||||||
|
:placeholder="t('order.pickupOrder.form.sizeCount')"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
type="text"
|
||||||
|
size="mini"
|
||||||
|
status="danger"
|
||||||
|
:disabled="measureForm.packageSizes.length <= 1"
|
||||||
|
@click="removeSizeRow(idx)"
|
||||||
|
>
|
||||||
|
{{ t('order.pickupOrder.actions.removeSize') }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-button size="mini" @click="addSizeRow">
|
||||||
|
{{ t('order.pickupOrder.actions.addSize') }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -211,7 +684,7 @@ defineExpose({ open, close })
|
|||||||
</div>
|
</div>
|
||||||
<div class="pk-detail-field">
|
<div class="pk-detail-field">
|
||||||
<span>{{ t('order.pickupOrder.detail.omsPayable') }}</span>
|
<span>{{ t('order.pickupOrder.detail.omsPayable') }}</span>
|
||||||
<b>¥ {{ fmtMoney(pick(detail, 'payableAmount', 'PayableAmount')) }}</b>
|
<b>¥ {{ fmtMoney(pick(detail, 'payableAmount', 'PayableAmount') ?? pick(detail, 'carrierPayableAmount', 'CarrierPayableAmount')) }}</b>
|
||||||
</div>
|
</div>
|
||||||
<div class="pk-detail-field">
|
<div class="pk-detail-field">
|
||||||
<span>{{ t('order.pickupOrder.form.freight') }}</span>
|
<span>{{ t('order.pickupOrder.form.freight') }}</span>
|
||||||
@ -232,15 +705,15 @@ defineExpose({ open, close })
|
|||||||
<div class="pk-detail-grid">
|
<div class="pk-detail-grid">
|
||||||
<div class="pk-detail-field">
|
<div class="pk-detail-field">
|
||||||
<span>{{ t('order.pickupOrder.detail.carrierFreightWeight') }}</span>
|
<span>{{ t('order.pickupOrder.detail.carrierFreightWeight') }}</span>
|
||||||
<b>{{ fmtNum(pick(detail, 'carrierFreightWeightKg', 'CarrierFreightWeightKg')) }}</b>
|
<b>{{ fmtNum(pick(detail, 'carrierFreightWeight', 'CarrierFreightWeight')) }}</b>
|
||||||
</div>
|
</div>
|
||||||
<div class="pk-detail-field">
|
<div class="pk-detail-field">
|
||||||
<span>{{ t('order.pickupOrder.detail.carrierQuotedFreight') }}</span>
|
<span>{{ t('order.pickupOrder.detail.carrierQuotedFreight') }}</span>
|
||||||
<b>¥ {{ fmtMoney(pick(detail, 'carrierQuotedFreight', 'CarrierQuotedFreight')) }}</b>
|
<b>¥ {{ fmtMoney(pick(detail, 'carrierQuotedFreightAmount', 'CarrierQuotedFreightAmount')) }}</b>
|
||||||
</div>
|
</div>
|
||||||
<div class="pk-detail-field pk-detail-grid-span">
|
<div class="pk-detail-field pk-detail-grid-span">
|
||||||
<span>{{ t('order.pickupOrder.detail.carrierQuotedReceivable') }}</span>
|
<span>{{ t('order.pickupOrder.detail.carrierQuotedReceivable') }}</span>
|
||||||
<b>¥ {{ fmtMoney(pick(detail, 'carrierQuotedReceivable', 'CarrierQuotedReceivable')) }}</b>
|
<b>¥ {{ fmtMoney(pick(detail, 'carrierQuotedReceivableAmount', 'CarrierQuotedReceivableAmount')) }}</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -312,6 +785,13 @@ defineExpose({ open, close })
|
|||||||
min-height: 280px;
|
min-height: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pk-detail-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.pk-detail-header {
|
.pk-detail-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@ -443,6 +923,33 @@ defineExpose({ open, close })
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pk-edit-form {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-size-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-size-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
:deep(.arco-input-number) {
|
||||||
|
width: 88px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pk-size-sep {
|
||||||
|
color: var(--color-text-3);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.pk-detail-empty {
|
.pk-detail-empty {
|
||||||
padding: 24px 0;
|
padding: 24px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@ -153,12 +153,11 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item :label="t('order.myOrders.createPickup.carrierService')">
|
<a-form-item :label="t('order.myOrders.createPickup.carrierService')" required>
|
||||||
<a-select
|
<a-select
|
||||||
v-model="bindForm.carrierService"
|
v-model="bindForm.carrierService"
|
||||||
:options="carrierServiceOptions"
|
:options="carrierServiceOptions"
|
||||||
allow-search
|
allow-search
|
||||||
allow-clear
|
|
||||||
:loading="refLoading"
|
:loading="refLoading"
|
||||||
:placeholder="t('order.myOrders.createPickup.carrierServicePh')"
|
:placeholder="t('order.myOrders.createPickup.carrierServicePh')"
|
||||||
/>
|
/>
|
||||||
@ -622,7 +621,7 @@ async function loadBindableOrders() {
|
|||||||
function resetBindForm() {
|
function resetBindForm() {
|
||||||
bindForm.senderAddressId = senderOptions.value[0]?.value
|
bindForm.senderAddressId = senderOptions.value[0]?.value
|
||||||
bindForm.expectPickupTime = defaultPickupTime()
|
bindForm.expectPickupTime = defaultPickupTime()
|
||||||
bindForm.carrierService = ''
|
bindForm.carrierService = carrierServiceOptions.value[0]?.value || ''
|
||||||
bindForm.remark = ''
|
bindForm.remark = ''
|
||||||
orderKeyword.value = ''
|
orderKeyword.value = ''
|
||||||
orderRows.value = []
|
orderRows.value = []
|
||||||
@ -723,6 +722,10 @@ function validateBind() {
|
|||||||
$baseMessage(t('order.pickupOrder.message.pickupTimeRequired'), 'warning')
|
$baseMessage(t('order.pickupOrder.message.pickupTimeRequired'), 'warning')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (!bindForm.carrierService?.trim()) {
|
||||||
|
$baseMessage(t('order.pickupOrder.message.carrierServiceRequired'), 'warning')
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -767,7 +770,7 @@ async function handleCreate() {
|
|||||||
senderAddressId: bindForm.senderAddressId!,
|
senderAddressId: bindForm.senderAddressId!,
|
||||||
direction: createDirection.value,
|
direction: createDirection.value,
|
||||||
warehouseId: selectedWarehouseId.value!,
|
warehouseId: selectedWarehouseId.value!,
|
||||||
carrierService: bindForm.carrierService || undefined,
|
carrierService: bindForm.carrierService.trim(),
|
||||||
expectPickupTime: bindForm.expectPickupTime,
|
expectPickupTime: bindForm.expectPickupTime,
|
||||||
orderIds: selectedOrders.value.map((x) => x.id),
|
orderIds: selectedOrders.value.map((x) => x.id),
|
||||||
remark: bindForm.remark || undefined,
|
remark: bindForm.remark || undefined,
|
||||||
|
|||||||
@ -27,7 +27,6 @@ import {
|
|||||||
postPickupOrderTrackUpsertBigBag,
|
postPickupOrderTrackUpsertBigBag,
|
||||||
removePickupOrderOrders,
|
removePickupOrderOrders,
|
||||||
reschedulePickupOrder,
|
reschedulePickupOrder,
|
||||||
schedulePickupOrder,
|
|
||||||
type PickupBigBagTrackSyncResult,
|
type PickupBigBagTrackSyncResult,
|
||||||
type PickupBigBagTrackStepResult,
|
type PickupBigBagTrackStepResult,
|
||||||
} from '@/api/order/pickupOrder'
|
} from '@/api/order/pickupOrder'
|
||||||
@ -115,19 +114,12 @@ const detailLoading = ref(false)
|
|||||||
const detail = ref<any>(null)
|
const detail = ref<any>(null)
|
||||||
const detailDrawerRef = ref<{ open: () => void; close: () => void } | null>(null)
|
const detailDrawerRef = ref<{ open: () => void; close: () => void } | null>(null)
|
||||||
const [registerFreightModal, { openModal: openFreightModal, closeModal: closeFreightModal }] = useModal() as any
|
const [registerFreightModal, { openModal: openFreightModal, closeModal: closeFreightModal }] = useModal() as any
|
||||||
const [registerScheduleModal, { openModal: openScheduleModal, closeModal: closeScheduleModal }] = useModal() as any
|
|
||||||
const [registerRescheduleModal, { openModal: openRescheduleModal, closeModal: closeRescheduleModal }] = useModal() as any
|
const [registerRescheduleModal, { openModal: openRescheduleModal, closeModal: closeRescheduleModal }] = useModal() as any
|
||||||
const [registerRebindModal, { openModal: openRebindModal, closeModal: closeRebindModal }] = useModal() as any
|
const [registerRebindModal, { openModal: openRebindModal, closeModal: closeRebindModal }] = useModal() as any
|
||||||
const [registerBatchFreightModal, { openModal: openBatchFreightModal, closeModal: closeBatchFreightModal }] =
|
const [registerBatchFreightModal, { openModal: openBatchFreightModal, closeModal: closeBatchFreightModal }] =
|
||||||
useModal() as any
|
useModal() as any
|
||||||
|
|
||||||
const freightForm = reactive({ id: 0, freightAmount: undefined as number | undefined, otherFeeAmount: undefined as number | undefined })
|
const freightForm = reactive({ id: 0, freightAmount: undefined as number | undefined, otherFeeAmount: undefined as number | undefined })
|
||||||
const scheduleForm = reactive({
|
|
||||||
id: 0,
|
|
||||||
carrierService: '',
|
|
||||||
carrierWaybillNo: '',
|
|
||||||
expectPickupTime: '',
|
|
||||||
})
|
|
||||||
const rescheduleForm = reactive({
|
const rescheduleForm = reactive({
|
||||||
id: 0,
|
id: 0,
|
||||||
expectPickupTime: '',
|
expectPickupTime: '',
|
||||||
@ -148,15 +140,12 @@ const actionLoading = ref(false)
|
|||||||
const selectedRows = ref<any[]>([])
|
const selectedRows = ref<any[]>([])
|
||||||
const batchPaying = ref(false)
|
const batchPaying = ref(false)
|
||||||
const batchFreighting = ref(false)
|
const batchFreighting = ref(false)
|
||||||
const batchScheduling = ref(false)
|
|
||||||
const batchDownstreamForwarding = ref(false)
|
const batchDownstreamForwarding = ref(false)
|
||||||
const downstreamForwardModalVisible = ref(false)
|
const downstreamForwardModalVisible = ref(false)
|
||||||
const downstreamForwardResultText = ref('')
|
const downstreamForwardResultText = ref('')
|
||||||
const batchTrackUpserting = ref(false)
|
const batchTrackUpserting = ref(false)
|
||||||
const trackUpsertModalVisible = ref(false)
|
const trackUpsertModalVisible = ref(false)
|
||||||
const trackUpsertResultText = ref('')
|
const trackUpsertResultText = ref('')
|
||||||
/** batch schedule uses same schedule modal with multiple ids */
|
|
||||||
const batchScheduleIds = ref<number[]>([])
|
|
||||||
|
|
||||||
const canForwardDownstream = computed(
|
const canForwardDownstream = computed(
|
||||||
() =>
|
() =>
|
||||||
@ -187,9 +176,6 @@ const batchFreightCandidates = computed(() =>
|
|||||||
return payStatus !== 'paid' && status !== 'cancelled' && status !== 'failed'
|
return payStatus !== 'paid' && status !== 'cancelled' && status !== 'failed'
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const batchScheduleCandidates = computed(() =>
|
|
||||||
selectedRows.value.filter((row) => canShowSchedule(String(row?.status || ''))),
|
|
||||||
)
|
|
||||||
|
|
||||||
const statusTabs = [
|
const statusTabs = [
|
||||||
{ value: STATUS_TAB_ALL, labelKey: 'common.all' },
|
{ value: STATUS_TAB_ALL, labelKey: 'common.all' },
|
||||||
@ -249,10 +235,6 @@ function canShowPay(row: { status?: string; payStatus?: string; receivableAmount
|
|||||||
return Number.isFinite(amount) && amount > 0
|
return Number.isFinite(amount) && amount > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function canShowSchedule(status: string) {
|
|
||||||
return status === 'draft' || status === 'scheduled'
|
|
||||||
}
|
|
||||||
|
|
||||||
function canShowRebind(status: string) {
|
function canShowRebind(status: string) {
|
||||||
return status === 'draft' || status === 'scheduled'
|
return status === 'draft' || status === 'scheduled'
|
||||||
}
|
}
|
||||||
@ -362,9 +344,6 @@ function actionRenderer(rowData: any) {
|
|||||||
if (canShowPay(rowData)) {
|
if (canShowPay(rowData)) {
|
||||||
parts.push(renderWarehouseActionBtn('pay', t('order.pickupOrder.actions.pay'), 'pay'))
|
parts.push(renderWarehouseActionBtn('pay', t('order.pickupOrder.actions.pay'), 'pay'))
|
||||||
}
|
}
|
||||||
if (canShowSchedule(status)) {
|
|
||||||
parts.push(renderWarehouseActionBtn('schedule', t('order.pickupOrder.actions.draftForecast'), 'publish'))
|
|
||||||
}
|
|
||||||
if (canShowRebind(status)) {
|
if (canShowRebind(status)) {
|
||||||
parts.push(renderWarehouseActionBtn('rebind', t('order.pickupOrder.actions.rebind'), 'rebind'))
|
parts.push(renderWarehouseActionBtn('rebind', t('order.pickupOrder.actions.rebind'), 'rebind'))
|
||||||
}
|
}
|
||||||
@ -458,7 +437,6 @@ function showCreateNextSteps(payload: CreatePickupSuccessPayload) {
|
|||||||
const status = String(payload.status || 'draft')
|
const status = String(payload.status || 'draft')
|
||||||
const payStatus = String(payload.payStatus || '')
|
const payStatus = String(payload.payStatus || '')
|
||||||
const amount = Number(payload.receivableAmount)
|
const amount = Number(payload.receivableAmount)
|
||||||
const canSchedule = canShowSchedule(status)
|
|
||||||
const canPay =
|
const canPay =
|
||||||
payStatus !== 'paid' &&
|
payStatus !== 'paid' &&
|
||||||
status !== 'cancelled' &&
|
status !== 'cancelled' &&
|
||||||
@ -481,20 +459,6 @@ function showCreateNextSteps(payload: CreatePickupSuccessPayload) {
|
|||||||
'div',
|
'div',
|
||||||
{ style: 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end' },
|
{ style: 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end' },
|
||||||
[
|
[
|
||||||
canSchedule
|
|
||||||
? h(
|
|
||||||
'button',
|
|
||||||
{
|
|
||||||
class: 'arco-btn arco-btn-secondary arco-btn-size-medium',
|
|
||||||
style: 'height:32px',
|
|
||||||
onClick: () => {
|
|
||||||
Modal.destroyAll()
|
|
||||||
if (id) void openScheduleById(id)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
t('order.pickupOrder.createNext.goSchedule'),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
canPay
|
canPay
|
||||||
? h(
|
? h(
|
||||||
'button',
|
'button',
|
||||||
@ -836,26 +800,6 @@ async function submitBatchFreight() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBatchSchedule() {
|
|
||||||
if (!hasPermission('Order.Pickup.Create')) {
|
|
||||||
$baseMessage(t('order.pickupOrder.message.noPermission'), 'warning')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!batchScheduleCandidates.value.length) {
|
|
||||||
$baseMessage(t('order.pickupOrder.message.batchScheduleNoCandidates'), 'warning')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await ensureCarrierServiceOptions()
|
|
||||||
batchScheduleIds.value = batchScheduleCandidates.value
|
|
||||||
.map((r) => Number(r.id))
|
|
||||||
.filter((id) => id > 0)
|
|
||||||
scheduleForm.id = 0
|
|
||||||
scheduleForm.carrierService = ''
|
|
||||||
scheduleForm.carrierWaybillNo = ''
|
|
||||||
scheduleForm.expectPickupTime = dayjs().add(1, 'day').hour(10).minute(0).second(0).format('YYYY-MM-DD HH:mm:ss')
|
|
||||||
openScheduleModal(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePageChange(page: number) {
|
function handlePageChange(page: number) {
|
||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
void loadList()
|
void loadList()
|
||||||
@ -874,10 +818,6 @@ function handleRowAction(row: any, _rowIndex: number, action?: string) {
|
|||||||
void openDetail(row.id)
|
void openDetail(row.id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (key === 'schedule') {
|
|
||||||
void openScheduleById(row.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (key === 'rebind') {
|
if (key === 'rebind') {
|
||||||
void openRebind(row.id)
|
void openRebind(row.id)
|
||||||
return
|
return
|
||||||
@ -918,6 +858,13 @@ async function openDetail(id: number | string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDetailUpdated(res: any) {
|
||||||
|
if (res) {
|
||||||
|
detail.value = res
|
||||||
|
}
|
||||||
|
void loadList()
|
||||||
|
}
|
||||||
|
|
||||||
function openConfirmFreight() {
|
function openConfirmFreight() {
|
||||||
if (!detail.value) return
|
if (!detail.value) return
|
||||||
freightForm.id = Number(detail.value.id ?? detail.value.Id)
|
freightForm.id = Number(detail.value.id ?? detail.value.Id)
|
||||||
@ -1003,93 +950,6 @@ async function doPayById(id: number | string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillScheduleForm(raw: any) {
|
|
||||||
scheduleForm.id = Number(raw.id ?? raw.Id)
|
|
||||||
scheduleForm.carrierService = String(raw.carrierService ?? raw.CarrierService ?? '')
|
|
||||||
scheduleForm.carrierWaybillNo = String(raw.carrierWaybillNo ?? raw.CarrierWaybillNo ?? '')
|
|
||||||
const t0 = raw.expectPickupTime ?? raw.ExpectPickupTime
|
|
||||||
scheduleForm.expectPickupTime = t0
|
|
||||||
? dayjs(t0).format('YYYY-MM-DD HH:mm:ss')
|
|
||||||
: dayjs().add(1, 'day').hour(10).minute(0).second(0).format('YYYY-MM-DD HH:mm:ss')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openSchedule() {
|
|
||||||
if (!detail.value) return
|
|
||||||
batchScheduleIds.value = []
|
|
||||||
await ensureCarrierServiceOptions()
|
|
||||||
fillScheduleForm(detail.value)
|
|
||||||
openScheduleModal(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openScheduleById(id: number | string) {
|
|
||||||
actionLoading.value = true
|
|
||||||
batchScheduleIds.value = []
|
|
||||||
try {
|
|
||||||
await ensureCarrierServiceOptions()
|
|
||||||
const raw = await getPickupOrderDetail(id)
|
|
||||||
fillScheduleForm(raw)
|
|
||||||
openScheduleModal(true)
|
|
||||||
} catch (e: any) {
|
|
||||||
$baseMessage(e?.message || t('order.pickupOrder.message.detailFailed'), 'error')
|
|
||||||
} finally {
|
|
||||||
actionLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitSchedule() {
|
|
||||||
if (!scheduleForm.carrierService.trim()) {
|
|
||||||
$baseMessage(t('order.pickupOrder.message.carrierServiceRequired'), 'warning')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const ids =
|
|
||||||
batchScheduleIds.value.length > 0
|
|
||||||
? [...batchScheduleIds.value]
|
|
||||||
: scheduleForm.id > 0
|
|
||||||
? [scheduleForm.id]
|
|
||||||
: []
|
|
||||||
if (!ids.length) return
|
|
||||||
|
|
||||||
actionLoading.value = true
|
|
||||||
batchScheduling.value = batchScheduleIds.value.length > 0
|
|
||||||
let success = 0
|
|
||||||
let fail = 0
|
|
||||||
try {
|
|
||||||
for (const id of ids) {
|
|
||||||
try {
|
|
||||||
const res = await schedulePickupOrder({
|
|
||||||
id,
|
|
||||||
carrierService: scheduleForm.carrierService.trim(),
|
|
||||||
carrierWaybillNo: scheduleForm.carrierWaybillNo.trim() || undefined,
|
|
||||||
expectPickupTime: scheduleForm.expectPickupTime || undefined,
|
|
||||||
})
|
|
||||||
if (detail.value && Number(detail.value.id ?? detail.value.Id) === id) {
|
|
||||||
detail.value = res
|
|
||||||
}
|
|
||||||
success += 1
|
|
||||||
} catch {
|
|
||||||
fail += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
closeScheduleModal()
|
|
||||||
batchScheduleIds.value = []
|
|
||||||
if (ids.length > 1) {
|
|
||||||
$baseMessage(
|
|
||||||
t('order.pickupOrder.message.batchScheduleResult', { success, fail }),
|
|
||||||
fail > 0 ? 'warning' : 'success',
|
|
||||||
)
|
|
||||||
} else if (fail > 0) {
|
|
||||||
$baseMessage(t('order.pickupOrder.message.actionFailed'), 'error')
|
|
||||||
} else {
|
|
||||||
$baseMessage(t('order.pickupOrder.message.scheduleOk'), 'success')
|
|
||||||
}
|
|
||||||
selectedRows.value = []
|
|
||||||
void loadList()
|
|
||||||
} finally {
|
|
||||||
actionLoading.value = false
|
|
||||||
batchScheduling.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openReschedule() {
|
function openReschedule() {
|
||||||
if (!detail.value) return
|
if (!detail.value) return
|
||||||
rescheduleForm.id = Number(detail.value.id ?? detail.value.Id)
|
rescheduleForm.id = Number(detail.value.id ?? detail.value.Id)
|
||||||
@ -1360,12 +1220,6 @@ const detailSettlementText = computed(() => {
|
|||||||
return settlementStatusLabel(String(detail.value.settlementStatus || detail.value.SettlementStatus || 'none'))
|
return settlementStatusLabel(String(detail.value.settlementStatus || detail.value.SettlementStatus || 'none'))
|
||||||
})
|
})
|
||||||
|
|
||||||
const scheduleModalTitle = computed(() =>
|
|
||||||
batchScheduleIds.value.length > 1
|
|
||||||
? t('order.pickupOrder.actions.batchSchedule')
|
|
||||||
: t('order.pickupOrder.actions.draftForecast'),
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query.status,
|
() => route.query.status,
|
||||||
() => {
|
() => {
|
||||||
@ -1544,6 +1398,7 @@ onMounted(() => {
|
|||||||
:pay-status-text="payStatusLabel(detailPayStatus)"
|
:pay-status-text="payStatusLabel(detailPayStatus)"
|
||||||
@copy-service-nos="copyAllServiceNos"
|
@copy-service-nos="copyAllServiceNos"
|
||||||
@open-order="goLinkedOrder"
|
@open-order="goLinkedOrder"
|
||||||
|
@updated="onDetailUpdated"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ModalPro
|
<ModalPro
|
||||||
@ -1585,51 +1440,6 @@ onMounted(() => {
|
|||||||
</a-form>
|
</a-form>
|
||||||
</ModalPro>
|
</ModalPro>
|
||||||
|
|
||||||
<ModalPro
|
|
||||||
@register="registerScheduleModal"
|
|
||||||
:title="scheduleModalTitle"
|
|
||||||
:confirm-loading="actionLoading || batchScheduling"
|
|
||||||
@ok="submitSchedule"
|
|
||||||
>
|
|
||||||
<a-alert
|
|
||||||
v-if="batchScheduleIds.length > 1"
|
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
style="margin-bottom: 12px"
|
|
||||||
>
|
|
||||||
{{ t('order.pickupOrder.message.batchScheduleHint', { count: batchScheduleIds.length }) }}
|
|
||||||
</a-alert>
|
|
||||||
<a-form layout="vertical">
|
|
||||||
<a-form-item :label="t('order.myOrders.createPickup.carrierService')" required>
|
|
||||||
<a-select
|
|
||||||
v-model="scheduleForm.carrierService"
|
|
||||||
allow-search
|
|
||||||
allow-clear
|
|
||||||
:placeholder="t('order.myOrders.createPickup.carrierServicePh')"
|
|
||||||
>
|
|
||||||
<a-option v-for="opt in carrierServiceOptions" :key="opt.value" :value="opt.value">
|
|
||||||
{{ opt.label }}
|
|
||||||
</a-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item :label="t('order.pickupOrder.table.carrierWaybillNo')">
|
|
||||||
<a-input
|
|
||||||
v-model="scheduleForm.carrierWaybillNo"
|
|
||||||
:placeholder="t('order.pickupOrder.form.carrierWaybillNoPh')"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item :label="t('order.myOrders.createPickup.expectTime')">
|
|
||||||
<a-date-picker
|
|
||||||
v-model="scheduleForm.expectPickupTime"
|
|
||||||
show-time
|
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</ModalPro>
|
|
||||||
|
|
||||||
<ModalPro
|
<ModalPro
|
||||||
@register="registerRescheduleModal"
|
@register="registerRescheduleModal"
|
||||||
:title="t('order.pickupOrder.actions.reschedule')"
|
:title="t('order.pickupOrder.actions.reschedule')"
|
||||||
|
|||||||
@ -4081,6 +4081,27 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<template v-if="singleForm.inboundMethod === 'doorPickup'">
|
<template v-if="singleForm.inboundMethod === 'doorPickup'">
|
||||||
|
<a-form-item
|
||||||
|
label="仓库地址"
|
||||||
|
required
|
||||||
|
:validate-status="fieldValidateStatus('warehouseId')"
|
||||||
|
:help="fieldHelp('warehouseId')"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model="singleForm.warehouseId"
|
||||||
|
allow-search
|
||||||
|
placeholder="请选择仓库"
|
||||||
|
:loading="singleReferenceLoading"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="opt in singleWarehouseOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
<a-form-item label="揽收地址" required>
|
<a-form-item label="揽收地址" required>
|
||||||
<a-select
|
<a-select
|
||||||
v-model="singleForm.senderAddressId"
|
v-model="singleForm.senderAddressId"
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="activeTab === 'basic' || activeTab === 'weight' || activeTab === 'fee' || activeTab === 'track' || activeTab === 'stockBind'"
|
v-if="activeTab === 'basic' || activeTab === 'weight' || activeTab === 'fee' || activeTab === 'track' || activeTab === 'stockBind' || activeTab === 'operationLog'"
|
||||||
class="drawer-tab-toolbar"
|
class="drawer-tab-toolbar"
|
||||||
>
|
>
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
@ -103,6 +103,17 @@
|
|||||||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="activeTab === 'operationLog'">
|
||||||
|
<a-tag v-if="operationLogList.length" size="small" color="arcoblue">
|
||||||
|
{{ $t('order.myOrders.drawer.operationLog.count', { count: operationLogList.length }) }}
|
||||||
|
</a-tag>
|
||||||
|
<a-button
|
||||||
|
class="app-btn-search"
|
||||||
|
:loading="operationLogLoading"
|
||||||
|
@click="loadOperationLogs"
|
||||||
|
>{{ $t('order.myOrders.drawer.operationLog.refresh') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -747,6 +758,63 @@
|
|||||||
@changed="handleIssueChanged"
|
@changed="handleIssueChanged"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作日志 -->
|
||||||
|
<div v-if="activeTab === 'operationLog'" class="tab-panel operation-log-info">
|
||||||
|
<a-spin :loading="operationLogLoading" class="operation-log-spin">
|
||||||
|
<div v-if="operationLogList.length" class="operation-log-list">
|
||||||
|
<div
|
||||||
|
v-for="item in operationLogList"
|
||||||
|
:key="item.id"
|
||||||
|
class="operation-log-item"
|
||||||
|
>
|
||||||
|
<div class="operation-log-head" @click="toggleOperationLogExpand(item.id)">
|
||||||
|
<div class="operation-log-head-main">
|
||||||
|
<span class="operation-log-action">{{ item.actionName || item.action }}</span>
|
||||||
|
<a-tag v-if="item.changes?.length" size="small" color="orangered">
|
||||||
|
{{ item.changes.length }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
<div class="operation-log-head-meta">
|
||||||
|
<span>{{ item.operatorName || $t('order.myOrders.drawer.operationLog.operator') }}</span>
|
||||||
|
<span>{{ formatOperationLogTime(item.createTime) }}</span>
|
||||||
|
<IconDown v-if="!isOperationLogExpanded(item.id)" />
|
||||||
|
<IconUp v-else />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isOperationLogExpanded(item.id)" class="operation-log-body">
|
||||||
|
<p v-if="item.remark" class="operation-log-remark">{{ item.remark }}</p>
|
||||||
|
<div v-if="item.changes?.length" class="operation-log-changes">
|
||||||
|
<div class="operation-log-changes-header">
|
||||||
|
<span>{{ $t('order.myOrders.drawer.operationLog.field') }}</span>
|
||||||
|
<span>{{ $t('order.myOrders.drawer.operationLog.before') }}</span>
|
||||||
|
<span>{{ $t('order.myOrders.drawer.operationLog.after') }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(change, idx) in item.changes"
|
||||||
|
:key="`${item.id}-${change.path}-${idx}`"
|
||||||
|
class="operation-log-changes-row"
|
||||||
|
>
|
||||||
|
<span>{{ change.label || change.path }}</span>
|
||||||
|
<span>{{ change.before ?? '--' }}</span>
|
||||||
|
<span class="operation-log-after">{{ change.after ?? '--' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-empty
|
||||||
|
v-else
|
||||||
|
:description="$t('order.myOrders.drawer.operationLog.noChanges')"
|
||||||
|
:image-style="{ height: '40px' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-empty
|
||||||
|
v-else-if="!operationLogLoading"
|
||||||
|
:description="$t('order.myOrders.drawer.operationLog.empty')"
|
||||||
|
:image-style="{ height: '60px' }"
|
||||||
|
/>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DrawerPro>
|
</DrawerPro>
|
||||||
@ -813,7 +881,7 @@ import { ref, computed, watch, reactive } from 'vue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { IconRefresh, IconRobot, IconEye } from '@arco-design/web-vue/es/icon';
|
import { IconRefresh, IconRobot, IconEye, IconDown, IconUp } from '@arco-design/web-vue/es/icon';
|
||||||
import useDrawer from '@/components/DrawerPro/useDrawer';
|
import useDrawer from '@/components/DrawerPro/useDrawer';
|
||||||
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
||||||
import {
|
import {
|
||||||
@ -822,6 +890,8 @@ import {
|
|||||||
postOrderRecalculateReceivable,
|
postOrderRecalculateReceivable,
|
||||||
postOrderRecalculatePayable,
|
postOrderRecalculatePayable,
|
||||||
postOrderAdditionalServicesUpdate,
|
postOrderAdditionalServicesUpdate,
|
||||||
|
getOrderOperationLogList,
|
||||||
|
type OrderOperationLogItem,
|
||||||
} from '@/api/order';
|
} from '@/api/order';
|
||||||
import { getSalesProductDetail, getSalesProductList } from '@/api/channel/salesProduct';
|
import { getSalesProductDetail, getSalesProductList } from '@/api/channel/salesProduct';
|
||||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||||
@ -1197,8 +1267,13 @@ const tabList = computed(() => [
|
|||||||
{ key: 'attachments', label: t('order.myOrders.drawer.tabs.attachments') },
|
{ key: 'attachments', label: t('order.myOrders.drawer.tabs.attachments') },
|
||||||
{ key: 'stockBind', label: t('order.myOrders.drawer.tabs.stockBind') },
|
{ key: 'stockBind', label: t('order.myOrders.drawer.tabs.stockBind') },
|
||||||
{ key: 'issueHandling', label: t('order.myOrders.drawer.tabs.issueHandling') },
|
{ key: 'issueHandling', label: t('order.myOrders.drawer.tabs.issueHandling') },
|
||||||
|
{ key: 'operationLog', label: t('order.myOrders.drawer.tabs.operationLog') },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const operationLogList = ref<OrderOperationLogItem[]>([]);
|
||||||
|
const operationLogLoading = ref(false);
|
||||||
|
const operationLogExpandedIds = ref<Set<number>>(new Set());
|
||||||
|
|
||||||
const slidingTabItems = computed(() =>
|
const slidingTabItems = computed(() =>
|
||||||
tabList.value.map((tab) => ({ key: tab.key, label: tab.label })),
|
tabList.value.map((tab) => ({ key: tab.key, label: tab.label })),
|
||||||
);
|
);
|
||||||
@ -2362,6 +2437,45 @@ async function handleRecalculatePayable() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadOperationLogs() {
|
||||||
|
const id = orderId.value;
|
||||||
|
if (!id) {
|
||||||
|
operationLogList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
operationLogLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getOrderOperationLogList(id);
|
||||||
|
operationLogList.value = res.items || [];
|
||||||
|
operationLogExpandedIds.value = new Set(
|
||||||
|
operationLogList.value.slice(0, 3).map((x) => x.id),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
operationLogList.value = [];
|
||||||
|
} finally {
|
||||||
|
operationLogLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOperationLogExpanded(id: number) {
|
||||||
|
return operationLogExpandedIds.value.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOperationLogExpand(id: number) {
|
||||||
|
const next = new Set(operationLogExpandedIds.value);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
operationLogExpandedIds.value = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatOperationLogTime(value?: string) {
|
||||||
|
if (!value) return '--';
|
||||||
|
const d = new Date(value);
|
||||||
|
if (Number.isNaN(d.getTime())) return String(value);
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
function handleTabChange(tabKey: string) {
|
function handleTabChange(tabKey: string) {
|
||||||
resetEditState();
|
resetEditState();
|
||||||
if (tabKey === 'basic') {
|
if (tabKey === 'basic') {
|
||||||
@ -2378,6 +2492,9 @@ function handleTabChange(tabKey: string) {
|
|||||||
if (tabKey === 'track') {
|
if (tabKey === 'track') {
|
||||||
loadTrackTimeline();
|
loadTrackTimeline();
|
||||||
}
|
}
|
||||||
|
if (tabKey === 'operationLog') {
|
||||||
|
void loadOperationLogs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
@ -2400,13 +2517,16 @@ watch(
|
|||||||
if (activeTab.value === 'track') {
|
if (activeTab.value === 'track') {
|
||||||
loadTrackTimeline();
|
loadTrackTimeline();
|
||||||
}
|
}
|
||||||
|
if (activeTab.value === 'operationLog') {
|
||||||
|
void loadOperationLogs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true, deep: true }
|
{ immediate: true, deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openDrawer: (opts?: { tab?: 'basic' | 'weight' | 'fee' | 'track' | 'attachments' | 'stockBind' | 'issueHandling' }) => {
|
openDrawer: (opts?: { tab?: 'basic' | 'weight' | 'fee' | 'track' | 'attachments' | 'stockBind' | 'issueHandling' | 'operationLog' }) => {
|
||||||
activeTab.value = opts?.tab || 'basic';
|
activeTab.value = opts?.tab || 'basic';
|
||||||
basicViewMode.value = 'all';
|
basicViewMode.value = 'all';
|
||||||
feeViewMode.value = 'all';
|
feeViewMode.value = 'all';
|
||||||
@ -3515,4 +3635,102 @@ defineExpose({
|
|||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.operation-log-spin {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-item {
|
||||||
|
border: 1px solid var(--color-border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-fill-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-head-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-action {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-head-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-body {
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
border-top: 1px solid var(--color-border-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-remark {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-changes {
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 1px solid var(--color-border-2);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-changes-header,
|
||||||
|
.operation-log-changes-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.1fr 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-changes-header {
|
||||||
|
background: var(--color-fill-2);
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-changes-row {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
border-top: 1px solid var(--color-border-2);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-log-after {
|
||||||
|
color: var(--app-primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -423,7 +423,7 @@
|
|||||||
isActiveTab('all', 'pendingOnline', 'shipping', 'deliverySuccess', 'deliveryFailed'),
|
isActiveTab('all', 'pendingOnline', 'shipping', 'deliverySuccess', 'deliveryFailed'),
|
||||||
);
|
);
|
||||||
const showReallocateAction = computed(() =>
|
const showReallocateAction = computed(() =>
|
||||||
isActiveTab('all', 'pendingAllocation', 'pendingPayment'),
|
isActiveTab('all', 'predeclared', 'pendingAllocation', 'pendingPayment', 'pendingShip'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const problematicModalVisible = ref(false);
|
const problematicModalVisible = ref(false);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user