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 */
|
||||
export async function getOrderExceptionEventList(orderId: number | string): Promise<OrderExceptionEventListResult> {
|
||||
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 }) =>
|
||||
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) =>
|
||||
postOk('/order/pickup-order/mark-picked', { id })
|
||||
|
||||
|
||||
@ -896,6 +896,26 @@ export default {
|
||||
'order.pickupOrder.settlementStatus.pending': 'Pending',
|
||||
'order.pickupOrder.settlementStatus.settled': 'Settled',
|
||||
'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.batchSchedule': 'Batch Draft Forecast',
|
||||
'order.pickupOrder.actions.copyServiceNos': 'Copy all service Nos.',
|
||||
@ -1799,7 +1819,7 @@ export default {
|
||||
'order.myOrders.actions.markProcessed': 'Mark processed',
|
||||
'order.myOrders.reallocate.modalTitle': 'Reallocate ({count} selected)',
|
||||
'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.channel': 'Designated channel',
|
||||
'order.myOrders.reallocate.schemeLabel': 'Allocation scheme',
|
||||
@ -2110,6 +2130,15 @@ export default {
|
||||
'order.myOrders.drawer.tabs.track': 'Tracking',
|
||||
'order.myOrders.drawer.tabs.attachments': 'Attachments',
|
||||
'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.issueHandling.pendingCount': 'Pending {count}',
|
||||
'order.myOrders.drawer.issueHandling.processedCount': 'Processed {count}',
|
||||
|
||||
@ -885,6 +885,26 @@ export default {
|
||||
'order.pickupOrder.settlementStatus.pending': '待结算',
|
||||
'order.pickupOrder.settlementStatus.settled': '已结算',
|
||||
'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.batchSchedule': '批量草稿预报',
|
||||
'order.pickupOrder.actions.copyServiceNos': '复制全部运单号',
|
||||
@ -1626,7 +1646,7 @@ export default {
|
||||
'order.waybillManage.importDownstream.failRow': '第 {row} 行({waybill}):{reason}',
|
||||
'order.myOrders.reallocate.modalTitle': '重新排货(已选 {count} 单)',
|
||||
'order.myOrders.reallocate.hint':
|
||||
'仅重算应付并更新服务渠道,不改应收与件重尺;已付款订单会自动跳过。换渠道会清空服务商单号/跟踪号,需自行重新预报下游。',
|
||||
'仅重算应付并更新服务渠道,不改应收与件重尺;已预报/待排货/待支付/待发货可用(含已付款)。换渠道会清空服务商单号/跟踪号,需自行重新预报下游。',
|
||||
'order.myOrders.reallocate.section.scheme': '排货方案',
|
||||
'order.myOrders.reallocate.section.channel': '指定渠道',
|
||||
'order.myOrders.reallocate.schemeLabel': '排货方案',
|
||||
@ -1971,6 +1991,15 @@ export default {
|
||||
'order.myOrders.drawer.tabs.track': '轨迹',
|
||||
'order.myOrders.drawer.tabs.attachments': '附件',
|
||||
'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.issueHandling.pendingCount': '待处理 {count}',
|
||||
'order.myOrders.drawer.issueHandling.processedCount': '已处理 {count}',
|
||||
|
||||
@ -38,11 +38,10 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('order.myOrders.createPickup.carrierService')">
|
||||
<a-form-item :label="t('order.myOrders.createPickup.carrierService')" required>
|
||||
<a-select
|
||||
v-model="form.carrierService"
|
||||
allow-search
|
||||
allow-clear
|
||||
:loading="loadingRefs"
|
||||
: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.expectPickupTime = defaultPickupTime()
|
||||
form.carrierService = ''
|
||||
form.carrierService = carrierServiceOptions.value[0]?.value || ''
|
||||
form.remark = ''
|
||||
form.senderAddressId = undefined
|
||||
|
||||
@ -200,6 +199,10 @@ async function handleOk() {
|
||||
$baseMessage(t('order.myOrders.createPickup.needTime'), 'warning')
|
||||
return
|
||||
}
|
||||
if (!form.carrierService?.trim()) {
|
||||
$baseMessage(t('order.pickupOrder.message.carrierServiceRequired'), 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
@ -216,7 +219,7 @@ async function handleOk() {
|
||||
senderAddressId: form.senderAddressId,
|
||||
direction: 'forward',
|
||||
warehouseId: form.warehouseId,
|
||||
carrierService: form.carrierService || undefined,
|
||||
carrierService: form.carrierService.trim(),
|
||||
expectPickupTime: form.expectPickupTime,
|
||||
orderIds: orderIds.value,
|
||||
remark: form.remark || undefined,
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="activeTab === 'basic' || activeTab === 'track' || activeTab === 'stockBind'"
|
||||
v-if="activeTab === 'basic' || activeTab === 'track' || activeTab === 'stockBind' || activeTab === 'operationLog'"
|
||||
class="drawer-tab-toolbar"
|
||||
>
|
||||
<div class="toolbar-left">
|
||||
@ -65,6 +65,17 @@
|
||||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||||
</a-button>
|
||||
</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>
|
||||
|
||||
@ -566,6 +577,63 @@
|
||||
hide-refresh
|
||||
/>
|
||||
</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>
|
||||
</DrawerPro>
|
||||
@ -608,6 +676,8 @@ import {
|
||||
postOrderWeightUpdate,
|
||||
postOrderRecalculateReceivable,
|
||||
postOrderRecalculatePayable,
|
||||
getOrderOperationLogList,
|
||||
type OrderOperationLogItem,
|
||||
} from '@/api/order';
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
||||
@ -983,8 +1053,13 @@ const tabList = computed(() => [
|
||||
{ key: 'track', label: t('order.myOrders.drawer.tabs.track') },
|
||||
{ key: 'attachments', label: t('order.myOrders.drawer.tabs.attachments') },
|
||||
{ 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(() =>
|
||||
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) {
|
||||
const next = resolveDrawerTab(tabKey);
|
||||
if (next !== tabKey) {
|
||||
@ -2003,6 +2117,9 @@ function handleTabChange(tabKey: string) {
|
||||
weightViewMode.value = 'all';
|
||||
void ensureFeeItemDictLoaded();
|
||||
}
|
||||
if (next === 'operationLog') {
|
||||
void loadOperationLogs();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
@ -2024,13 +2141,16 @@ watch(
|
||||
if (activeTab.value === 'weightFee') {
|
||||
void ensureFeeItemDictLoaded();
|
||||
}
|
||||
if (activeTab.value === 'operationLog') {
|
||||
void loadOperationLogs();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
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);
|
||||
activeTab.value = tab;
|
||||
basicViewMode.value = 'all';
|
||||
@ -3117,4 +3237,102 @@ defineExpose({
|
||||
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>
|
||||
|
||||
@ -1,13 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import DrawerPro from '@/components/DrawerPro/index.vue'
|
||||
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||
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' })
|
||||
|
||||
type SelectOpt = { value: number | string; label: string }
|
||||
type SizeRow = { length?: number; width?: number; height?: number; count?: number }
|
||||
|
||||
const props = defineProps<{
|
||||
loading: boolean
|
||||
detail: any
|
||||
@ -25,17 +38,58 @@ const emit = defineEmits<{
|
||||
close: []
|
||||
copyServiceNos: []
|
||||
openOrder: [orderId: number]
|
||||
updated: [detail: any]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const [register, { openDrawer, closeDrawer, setDrawerProps }] = useDrawer() as any
|
||||
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(() =>
|
||||
String(props.detail?.payStatus ?? props.detail?.PayStatus ?? ''),
|
||||
)
|
||||
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(() => [
|
||||
{
|
||||
title: t('order.pickupOrder.table.serviceNo'),
|
||||
@ -69,6 +123,7 @@ watch(
|
||||
() => props.detail?.id ?? props.detail?.Id,
|
||||
() => {
|
||||
detailTab.value = 'info'
|
||||
resetEditState()
|
||||
},
|
||||
)
|
||||
|
||||
@ -78,6 +133,10 @@ watch(tabItems, (items) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(detailTab, () => {
|
||||
resetEditState()
|
||||
})
|
||||
|
||||
function pick(d: any, a: string, b: string) {
|
||||
return d?.[a] ?? d?.[b]
|
||||
}
|
||||
@ -94,18 +153,257 @@ function fmtMoney(v: unknown) {
|
||||
return Number.isFinite(n) ? n.toFixed(2) : String(v)
|
||||
}
|
||||
|
||||
function formatPackageSizes(raw: unknown) {
|
||||
const text = String(raw ?? '').trim()
|
||||
if (!text) return '-'
|
||||
return text
|
||||
.split(';')
|
||||
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)
|
||||
.join(';')
|
||||
.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) {
|
||||
const rows = normalizePackageSizes(raw)
|
||||
if (!rows.length) {
|
||||
const text = typeof raw === 'string' ? raw.trim() : ''
|
||||
return text || '-'
|
||||
}
|
||||
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)
|
||||
.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() {
|
||||
detailTab.value = 'info'
|
||||
resetEditState()
|
||||
setDrawerProps({
|
||||
title: t('order.pickupOrder.detail.title'),
|
||||
width: 720,
|
||||
@ -117,6 +415,7 @@ function open() {
|
||||
}
|
||||
|
||||
function close() {
|
||||
resetEditState()
|
||||
closeDrawer()
|
||||
}
|
||||
|
||||
@ -157,7 +456,24 @@ defineExpose({ open, close })
|
||||
<SlidingTabs v-model="detailTab" :items="tabItems" size="mini" class="pk-detail-tabs" />
|
||||
|
||||
<div class="pk-detail-body">
|
||||
<div v-show="detailTab === 'info'" class="pk-detail-grid">
|
||||
<div v-show="detailTab === 'info'">
|
||||
<div class="pk-detail-toolbar">
|
||||
<template v-if="canEditInfo && !infoEditing">
|
||||
<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 v-if="!infoEditing" class="pk-detail-grid">
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.table.carrierWaybillNo') }}</span>
|
||||
<b>{{ detail.carrierWaybillNo || detail.CarrierWaybillNo || '-' }}</b>
|
||||
@ -184,18 +500,112 @@ defineExpose({ open, close })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="detailTab === 'measure'" class="pk-detail-grid">
|
||||
<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 v-show="detailTab === 'measure'">
|
||||
<div class="pk-detail-toolbar">
|
||||
<template v-if="canEditMeasure && !measureEditing">
|
||||
<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 v-if="!measureEditing" class="pk-detail-grid">
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.table.actualWeightKg') }}</span>
|
||||
<b>{{ fmtNum(pick(detail, 'actualWeightKg', 'ActualWeightKg')) }}</b>
|
||||
<b>{{ fmtNum(pick(detail, 'totalActualWeight', 'TotalActualWeight')) }}</b>
|
||||
</div>
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.table.chargeableWeight') }}</span>
|
||||
<b>{{ fmtNum(pick(detail, 'chargeableWeightKg', 'ChargeableWeightKg')) }}</b>
|
||||
<b>{{ fmtNum(pick(detail, 'chargeableWeight', 'ChargeableWeight')) }}</b>
|
||||
</div>
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.table.volumetricWeight') }}</span>
|
||||
<b>{{ fmtNum(pick(detail, 'volumetricWeightKg', 'VolumetricWeightKg')) }}</b>
|
||||
<b>{{ fmtNum(pick(detail, 'volumetricWeight', 'VolumetricWeight')) }}</b>
|
||||
</div>
|
||||
<div class="pk-detail-field pk-detail-grid-span">
|
||||
<span>{{ t('order.pickupOrder.detail.packageSizes') }}</span>
|
||||
@ -203,6 +613,69 @@ defineExpose({ open, close })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="pk-edit-form">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item :label="t('order.pickupOrder.table.actualWeightKg')" required>
|
||||
<a-input-number
|
||||
v-model="measureForm.totalActualWeight"
|
||||
: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 v-show="detailTab === 'fee' && isAdmin">
|
||||
<div class="pk-detail-grid">
|
||||
<div class="pk-detail-field">
|
||||
@ -211,7 +684,7 @@ defineExpose({ open, close })
|
||||
</div>
|
||||
<div class="pk-detail-field">
|
||||
<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 class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.form.freight') }}</span>
|
||||
@ -232,15 +705,15 @@ defineExpose({ open, close })
|
||||
<div class="pk-detail-grid">
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.detail.carrierFreightWeight') }}</span>
|
||||
<b>{{ fmtNum(pick(detail, 'carrierFreightWeightKg', 'CarrierFreightWeightKg')) }}</b>
|
||||
<b>{{ fmtNum(pick(detail, 'carrierFreightWeight', 'CarrierFreightWeight')) }}</b>
|
||||
</div>
|
||||
<div class="pk-detail-field">
|
||||
<span>{{ t('order.pickupOrder.detail.carrierQuotedFreight') }}</span>
|
||||
<b>¥ {{ fmtMoney(pick(detail, 'carrierQuotedFreight', 'CarrierQuotedFreight')) }}</b>
|
||||
<b>¥ {{ fmtMoney(pick(detail, 'carrierQuotedFreightAmount', 'CarrierQuotedFreightAmount')) }}</b>
|
||||
</div>
|
||||
<div class="pk-detail-field pk-detail-grid-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>
|
||||
@ -312,6 +785,13 @@ defineExpose({ open, close })
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.pk-detail-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pk-detail-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@ -443,6 +923,33 @@ defineExpose({ open, close })
|
||||
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 {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
|
||||
@ -153,12 +153,11 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<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
|
||||
v-model="bindForm.carrierService"
|
||||
:options="carrierServiceOptions"
|
||||
allow-search
|
||||
allow-clear
|
||||
:loading="refLoading"
|
||||
:placeholder="t('order.myOrders.createPickup.carrierServicePh')"
|
||||
/>
|
||||
@ -622,7 +621,7 @@ async function loadBindableOrders() {
|
||||
function resetBindForm() {
|
||||
bindForm.senderAddressId = senderOptions.value[0]?.value
|
||||
bindForm.expectPickupTime = defaultPickupTime()
|
||||
bindForm.carrierService = ''
|
||||
bindForm.carrierService = carrierServiceOptions.value[0]?.value || ''
|
||||
bindForm.remark = ''
|
||||
orderKeyword.value = ''
|
||||
orderRows.value = []
|
||||
@ -723,6 +722,10 @@ function validateBind() {
|
||||
$baseMessage(t('order.pickupOrder.message.pickupTimeRequired'), 'warning')
|
||||
return false
|
||||
}
|
||||
if (!bindForm.carrierService?.trim()) {
|
||||
$baseMessage(t('order.pickupOrder.message.carrierServiceRequired'), 'warning')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -767,7 +770,7 @@ async function handleCreate() {
|
||||
senderAddressId: bindForm.senderAddressId!,
|
||||
direction: createDirection.value,
|
||||
warehouseId: selectedWarehouseId.value!,
|
||||
carrierService: bindForm.carrierService || undefined,
|
||||
carrierService: bindForm.carrierService.trim(),
|
||||
expectPickupTime: bindForm.expectPickupTime,
|
||||
orderIds: selectedOrders.value.map((x) => x.id),
|
||||
remark: bindForm.remark || undefined,
|
||||
|
||||
@ -27,7 +27,6 @@ import {
|
||||
postPickupOrderTrackUpsertBigBag,
|
||||
removePickupOrderOrders,
|
||||
reschedulePickupOrder,
|
||||
schedulePickupOrder,
|
||||
type PickupBigBagTrackSyncResult,
|
||||
type PickupBigBagTrackStepResult,
|
||||
} from '@/api/order/pickupOrder'
|
||||
@ -115,19 +114,12 @@ const detailLoading = ref(false)
|
||||
const detail = ref<any>(null)
|
||||
const detailDrawerRef = ref<{ open: () => void; close: () => void } | null>(null)
|
||||
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 [registerRebindModal, { openModal: openRebindModal, closeModal: closeRebindModal }] = useModal() as any
|
||||
const [registerBatchFreightModal, { openModal: openBatchFreightModal, closeModal: closeBatchFreightModal }] =
|
||||
useModal() as any
|
||||
|
||||
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({
|
||||
id: 0,
|
||||
expectPickupTime: '',
|
||||
@ -148,15 +140,12 @@ const actionLoading = ref(false)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const batchPaying = ref(false)
|
||||
const batchFreighting = ref(false)
|
||||
const batchScheduling = ref(false)
|
||||
const batchDownstreamForwarding = ref(false)
|
||||
const downstreamForwardModalVisible = ref(false)
|
||||
const downstreamForwardResultText = ref('')
|
||||
const batchTrackUpserting = ref(false)
|
||||
const trackUpsertModalVisible = ref(false)
|
||||
const trackUpsertResultText = ref('')
|
||||
/** batch schedule uses same schedule modal with multiple ids */
|
||||
const batchScheduleIds = ref<number[]>([])
|
||||
|
||||
const canForwardDownstream = computed(
|
||||
() =>
|
||||
@ -187,9 +176,6 @@ const batchFreightCandidates = computed(() =>
|
||||
return payStatus !== 'paid' && status !== 'cancelled' && status !== 'failed'
|
||||
}),
|
||||
)
|
||||
const batchScheduleCandidates = computed(() =>
|
||||
selectedRows.value.filter((row) => canShowSchedule(String(row?.status || ''))),
|
||||
)
|
||||
|
||||
const statusTabs = [
|
||||
{ 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
|
||||
}
|
||||
|
||||
function canShowSchedule(status: string) {
|
||||
return status === 'draft' || status === 'scheduled'
|
||||
}
|
||||
|
||||
function canShowRebind(status: string) {
|
||||
return status === 'draft' || status === 'scheduled'
|
||||
}
|
||||
@ -362,9 +344,6 @@ function actionRenderer(rowData: any) {
|
||||
if (canShowPay(rowData)) {
|
||||
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)) {
|
||||
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 payStatus = String(payload.payStatus || '')
|
||||
const amount = Number(payload.receivableAmount)
|
||||
const canSchedule = canShowSchedule(status)
|
||||
const canPay =
|
||||
payStatus !== 'paid' &&
|
||||
status !== 'cancelled' &&
|
||||
@ -481,20 +459,6 @@ function showCreateNextSteps(payload: CreatePickupSuccessPayload) {
|
||||
'div',
|
||||
{ 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
|
||||
? h(
|
||||
'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) {
|
||||
currentPage.value = page
|
||||
void loadList()
|
||||
@ -874,10 +818,6 @@ function handleRowAction(row: any, _rowIndex: number, action?: string) {
|
||||
void openDetail(row.id)
|
||||
return
|
||||
}
|
||||
if (key === 'schedule') {
|
||||
void openScheduleById(row.id)
|
||||
return
|
||||
}
|
||||
if (key === 'rebind') {
|
||||
void openRebind(row.id)
|
||||
return
|
||||
@ -918,6 +858,13 @@ async function openDetail(id: number | string) {
|
||||
}
|
||||
}
|
||||
|
||||
function onDetailUpdated(res: any) {
|
||||
if (res) {
|
||||
detail.value = res
|
||||
}
|
||||
void loadList()
|
||||
}
|
||||
|
||||
function openConfirmFreight() {
|
||||
if (!detail.value) return
|
||||
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() {
|
||||
if (!detail.value) return
|
||||
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'))
|
||||
})
|
||||
|
||||
const scheduleModalTitle = computed(() =>
|
||||
batchScheduleIds.value.length > 1
|
||||
? t('order.pickupOrder.actions.batchSchedule')
|
||||
: t('order.pickupOrder.actions.draftForecast'),
|
||||
)
|
||||
|
||||
watch(
|
||||
() => route.query.status,
|
||||
() => {
|
||||
@ -1544,6 +1398,7 @@ onMounted(() => {
|
||||
:pay-status-text="payStatusLabel(detailPayStatus)"
|
||||
@copy-service-nos="copyAllServiceNos"
|
||||
@open-order="goLinkedOrder"
|
||||
@updated="onDetailUpdated"
|
||||
/>
|
||||
|
||||
<ModalPro
|
||||
@ -1585,51 +1440,6 @@ onMounted(() => {
|
||||
</a-form>
|
||||
</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
|
||||
@register="registerRescheduleModal"
|
||||
:title="t('order.pickupOrder.actions.reschedule')"
|
||||
|
||||
@ -4081,6 +4081,27 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<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-select
|
||||
v-model="singleForm.senderAddressId"
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
</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"
|
||||
>
|
||||
<div class="toolbar-left">
|
||||
@ -103,6 +103,17 @@
|
||||
>{{ $t('order.myOrders.drawer.track.refresh') }}
|
||||
</a-button>
|
||||
</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>
|
||||
|
||||
@ -747,6 +758,63 @@
|
||||
@changed="handleIssueChanged"
|
||||
/>
|
||||
</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>
|
||||
</DrawerPro>
|
||||
@ -813,7 +881,7 @@ import { ref, computed, watch, reactive } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
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 { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
||||
import {
|
||||
@ -822,6 +890,8 @@ import {
|
||||
postOrderRecalculateReceivable,
|
||||
postOrderRecalculatePayable,
|
||||
postOrderAdditionalServicesUpdate,
|
||||
getOrderOperationLogList,
|
||||
type OrderOperationLogItem,
|
||||
} from '@/api/order';
|
||||
import { getSalesProductDetail, getSalesProductList } from '@/api/channel/salesProduct';
|
||||
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: 'stockBind', label: t('order.myOrders.drawer.tabs.stockBind') },
|
||||
{ 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(() =>
|
||||
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) {
|
||||
resetEditState();
|
||||
if (tabKey === 'basic') {
|
||||
@ -2378,6 +2492,9 @@ function handleTabChange(tabKey: string) {
|
||||
if (tabKey === 'track') {
|
||||
loadTrackTimeline();
|
||||
}
|
||||
if (tabKey === 'operationLog') {
|
||||
void loadOperationLogs();
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
@ -2400,13 +2517,16 @@ watch(
|
||||
if (activeTab.value === 'track') {
|
||||
loadTrackTimeline();
|
||||
}
|
||||
if (activeTab.value === 'operationLog') {
|
||||
void loadOperationLogs();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
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';
|
||||
basicViewMode.value = 'all';
|
||||
feeViewMode.value = 'all';
|
||||
@ -3515,4 +3635,102 @@ defineExpose({
|
||||
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>
|
||||
|
||||
@ -423,7 +423,7 @@
|
||||
isActiveTab('all', 'pendingOnline', 'shipping', 'deliverySuccess', 'deliveryFailed'),
|
||||
);
|
||||
const showReallocateAction = computed(() =>
|
||||
isActiveTab('all', 'pendingAllocation', 'pendingPayment'),
|
||||
isActiveTab('all', 'predeclared', 'pendingAllocation', 'pendingPayment', 'pendingShip'),
|
||||
);
|
||||
|
||||
const problematicModalVisible = ref(false);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user