fix: polish order detail drawer track/fee UX and drawer flush layout

Remove redundant basic summary, fix empty a-alert content, refine track timeline with status and bilingual copy, and stretch drawers across scrollbar-gutter.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-09-03 18:56:29 +08:00
parent f71d2f1088
commit f193929351
13 changed files with 362 additions and 449 deletions

View File

@ -21,6 +21,10 @@ declare namespace API {
type GetDetailTimelineOutput = { type GetDetailTimelineOutput = {
/** 轨迹编码 */ /** 轨迹编码 */
trackCode?: string; trackCode?: string;
/** 轨迹状态 */
trackStatus?: string;
/** 轨迹状态 code */
trackStatusCode?: string;
/** 轨迹内容 */ /** 轨迹内容 */
content?: string; content?: string;
/** 轨迹英文内容 */ /** 轨迹英文内容 */

View File

@ -20,6 +20,10 @@ declare namespace API {
type GetDetailTimelineOutput = { type GetDetailTimelineOutput = {
/** 轨迹编码 */ /** 轨迹编码 */
trackCode?: string; trackCode?: string;
/** 轨迹状态 */
trackStatus?: string;
/** 轨迹状态 code */
trackStatusCode?: string;
/** 轨迹内容 */ /** 轨迹内容 */
content?: string; content?: string;
/** 轨迹英文内容 */ /** 轨迹英文内容 */

View File

@ -159,6 +159,17 @@ body .arco-drawer {
border-radius: 0 !important; border-radius: 0 !important;
} }
/*
* index.html 为防 boot/居中抖动给 html 加了 scrollbar-gutter: stable,
* Chromium 下 fixed 容器会被收窄一截 gutter(约 15px),抽屉 right:0 仍贴容器右缘,
* 视觉上就在屏幕右边界留出缝。用 100vw 铺满视觉视口。
*/
body .arco-drawer-container {
width: 100vw;
left: 0;
right: auto;
}
/* vxe table wrapper */ /* vxe table wrapper */
body .vxe-table--border, body .vxe-table--border,
body .vxe-grid { body .vxe-grid {

View File

@ -1895,6 +1895,7 @@ export default {
'order.myOrders.drawer.track.remark': 'Remark', 'order.myOrders.drawer.track.remark': 'Remark',
'order.myOrders.drawer.track.code': 'Track code', 'order.myOrders.drawer.track.code': 'Track code',
'order.myOrders.drawer.track.timezone': 'TZ {zone}', 'order.myOrders.drawer.track.timezone': 'TZ {zone}',
'order.myOrders.drawer.track.statusUnknown': 'Update',
'order.myOrders.drawer.weightLength': 'Length(cm)', 'order.myOrders.drawer.weightLength': 'Length(cm)',
'order.myOrders.drawer.weightWidth': 'Width(cm)', 'order.myOrders.drawer.weightWidth': 'Width(cm)',
'order.myOrders.drawer.weightHeight': 'Height(cm)', 'order.myOrders.drawer.weightHeight': 'Height(cm)',
@ -2192,7 +2193,7 @@ export default {
'order.placeOrder.batch.stepTemplate': 'Select template', 'order.placeOrder.batch.stepTemplate': 'Select template',
'order.placeOrder.batch.stepUpload': 'Upload Excel', 'order.placeOrder.batch.stepUpload': 'Upload Excel',
'order.placeOrder.batch.stepSubmit': 'Submit', 'order.placeOrder.batch.stepSubmit': 'Submit',
'order.placeOrder.batch.guideHint': 'Click a step to jump · pick template on the right, then upload & submit', 'order.placeOrder.batch.guideHint': 'Click a step to jump · pick template & sender, then upload & submit',
'order.placeOrder.batch.pickTemplateHint': 'Select a template', 'order.placeOrder.batch.pickTemplateHint': 'Select a template',
'order.placeOrder.batch.block.noTemplate': 'Please select an order template on the right first', 'order.placeOrder.batch.block.noTemplate': 'Please select an order template on the right first',
'order.placeOrder.batch.block.templateDraft': 'Finish mapping this custom template before submitting', 'order.placeOrder.batch.block.templateDraft': 'Finish mapping this custom template before submitting',
@ -2257,6 +2258,11 @@ export default {
'order.placeOrder.single.hideExtraCols': 'Less', 'order.placeOrder.single.hideExtraCols': 'Less',
'order.placeOrder.single.showRecipientOptional': 'More', 'order.placeOrder.single.showRecipientOptional': 'More',
'order.placeOrder.single.hideRecipientOptional': 'Less', 'order.placeOrder.single.hideRecipientOptional': 'Less',
'order.placeOrder.single.saveReceiver': 'Save',
'order.placeOrder.single.savingReceiver': 'Saving…',
'order.placeOrder.single.saveReceiverRequired': 'Enter recipient name and address line 1 first',
'order.placeOrder.single.saveReceiverSuccess': 'Recipient address saved',
'order.placeOrder.single.saveReceiverFailed': 'Failed to save recipient address',
'order.placeOrder.single.declarationResizeHint': 'Scroll sideways for more columns; drag bottom to resize height', 'order.placeOrder.single.declarationResizeHint': 'Scroll sideways for more columns; drag bottom to resize height',
'order.placeOrder.single.successTitle': 'Order created', 'order.placeOrder.single.successTitle': 'Order created',
'order.placeOrder.single.successContent': 'You can view details and status in My Orders.', 'order.placeOrder.single.successContent': 'You can view details and status in My Orders.',

View File

@ -1814,6 +1814,7 @@ export default {
'order.myOrders.drawer.track.remark': '备注', 'order.myOrders.drawer.track.remark': '备注',
'order.myOrders.drawer.track.code': '轨迹代码', 'order.myOrders.drawer.track.code': '轨迹代码',
'order.myOrders.drawer.track.timezone': '时区 {zone}', 'order.myOrders.drawer.track.timezone': '时区 {zone}',
'order.myOrders.drawer.track.statusUnknown': '轨迹更新',
'order.myOrders.drawer.weightLength': '长(cm)', 'order.myOrders.drawer.weightLength': '长(cm)',
'order.myOrders.drawer.weightWidth': '宽(cm)', 'order.myOrders.drawer.weightWidth': '宽(cm)',
'order.myOrders.drawer.weightHeight': '高(cm)', 'order.myOrders.drawer.weightHeight': '高(cm)',
@ -2111,7 +2112,7 @@ export default {
'order.placeOrder.batch.stepTemplate': '选择模板', 'order.placeOrder.batch.stepTemplate': '选择模板',
'order.placeOrder.batch.stepUpload': '上传 Excel', 'order.placeOrder.batch.stepUpload': '上传 Excel',
'order.placeOrder.batch.stepSubmit': '提交下单', 'order.placeOrder.batch.stepSubmit': '提交下单',
'order.placeOrder.batch.guideHint': '点步骤可定位 · 右侧选模板,左侧上传后提交', 'order.placeOrder.batch.guideHint': '点步骤可定位 · 先选模板与发件人,再上传提交',
'order.placeOrder.batch.pickTemplateHint': '请选择模板', 'order.placeOrder.batch.pickTemplateHint': '请选择模板',
'order.placeOrder.batch.block.noTemplate': '请先在右侧选择下单模板', 'order.placeOrder.batch.block.noTemplate': '请先在右侧选择下单模板',
'order.placeOrder.batch.block.templateDraft': '当前自定义模板待编辑完成映射后才能提交', 'order.placeOrder.batch.block.templateDraft': '当前自定义模板待编辑完成映射后才能提交',
@ -2182,6 +2183,11 @@ export default {
'order.placeOrder.single.declarationSkuFilled': '已填入申报信息,请补充单价等必填项', 'order.placeOrder.single.declarationSkuFilled': '已填入申报信息,请补充单价等必填项',
'order.placeOrder.single.showRecipientOptional': '更多', 'order.placeOrder.single.showRecipientOptional': '更多',
'order.placeOrder.single.hideRecipientOptional': '收起', 'order.placeOrder.single.hideRecipientOptional': '收起',
'order.placeOrder.single.saveReceiver': '保存',
'order.placeOrder.single.savingReceiver': '保存中…',
'order.placeOrder.single.saveReceiverRequired': '请先填写收件人名称和地址1',
'order.placeOrder.single.saveReceiverSuccess': '收件地址已保存',
'order.placeOrder.single.saveReceiverFailed': '保存收件地址失败',
'order.placeOrder.single.declarationResizeHint': '左右滑动看更多列,底部拖拽调节高度', 'order.placeOrder.single.declarationResizeHint': '左右滑动看更多列,底部拖拽调节高度',
'order.placeOrder.single.successTitle': '单票下单成功', 'order.placeOrder.single.successTitle': '单票下单成功',
'order.placeOrder.single.successContent': '订单已创建,可前往「我的订单」查看详情与后续状态。', 'order.placeOrder.single.successContent': '订单已创建,可前往「我的订单」查看详情与后续状态。',

View File

@ -160,9 +160,10 @@
v-for="(w, i) in warningList" v-for="(w, i) in warningList"
:key="i" :key="i"
type="warning" type="warning"
:content="w"
class="warn-item" class="warn-item"
/> >
{{ w }}
</a-alert>
<div class="section-title">{{ t('channel.salesProducts.billingSimulate.receivable') }}</div> <div class="section-title">{{ t('channel.salesProducts.billingSimulate.receivable') }}</div>
<a-table <a-table

View File

@ -290,14 +290,16 @@ function handleCancel() {
v-if="showInsuranceTip" v-if="showInsuranceTip"
type="info" type="info"
show-icon show-icon
:content="t('order.myOrders.claimed.tipInsured', { amount: declaredSumText })" >
/> {{ t('order.myOrders.claimed.tipInsured', { amount: declaredSumText }) }}
</a-alert>
<a-alert <a-alert
v-else-if="showUninsuredTip" v-else-if="showUninsuredTip"
type="warning" type="warning"
show-icon show-icon
:content="t('order.myOrders.claimed.tipUninsured')" >
/> {{ t('order.myOrders.claimed.tipUninsured') }}
</a-alert>
<div v-if="declaredSumNumber != null" class="claim-max-hint"> <div v-if="declaredSumNumber != null" class="claim-max-hint">
{{ t('order.myOrders.claimed.maxHint', { amount: declaredSumText }) }} {{ t('order.myOrders.claimed.maxHint', { amount: declaredSumText }) }}
</div> </div>

View File

@ -36,12 +36,22 @@
<a-button v-else class="app-btn-search" @click="startBasicEdit"> <a-button v-else class="app-btn-search" @click="startBasicEdit">
{{ $t('order.myOrders.drawer.actions.edit') }} {{ $t('order.myOrders.drawer.actions.edit') }}
</a-button> </a-button>
<a-button class="app-btn-search" @click="handleAskAdvisor">
{{ $t('nanaAssistant.askAboutWaybill') }}
</a-button>
</template> </template>
<template v-else-if="activeTab === 'track'"> <template v-else-if="activeTab === 'track'">
<span class="toolbar-meta"> <span class="toolbar-meta">
<span class="toolbar-meta-label">{{ $t('order.myOrders.drawer.trackNo') }}</span> <span class="toolbar-meta-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
<span class="toolbar-meta-value">{{ trackOrderNumber || '--' }}</span> <span class="toolbar-meta-value">{{ trackOrderNumber || '--' }}</span>
</span> </span>
<a-tag
v-if="currentOrderStatusMeta"
size="small"
:style="getOrderStatusTagStyle(currentOrderStatusMeta)"
>
{{ $t(currentOrderStatusMeta.labelKey) }}
</a-tag>
<a-tag v-if="trackTimeline.length" size="small" color="arcoblue"> <a-tag v-if="trackTimeline.length" size="small" color="arcoblue">
{{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }} {{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }}
</a-tag> </a-tag>
@ -66,40 +76,6 @@
<div class="drawer-tab-content"> <div class="drawer-tab-content">
<!-- 基本信息 --> <!-- 基本信息 -->
<div v-if="activeTab === 'basic'" class="tab-panel basic-info"> <div v-if="activeTab === 'basic'" class="tab-panel basic-info">
<div class="basic-summary">
<div class="basic-summary-top">
<div class="basic-summary-main">
<span class="basic-summary-label">{{ $t('order.myOrders.drawer.serviceNo') }}</span>
<span class="basic-summary-no">{{ getOrderValue('serviceNo') }}</span>
</div>
<a-tag
v-if="currentOrderStatusMeta"
size="small"
:style="getOrderStatusTagStyle(currentOrderStatusMeta)"
>
{{ $t(currentOrderStatusMeta.labelKey) }}
</a-tag>
</div>
<div class="basic-summary-chips">
<span class="basic-chip">
<span class="basic-chip-label">{{ $t('order.myOrders.drawer.clientNo') }}</span>
<span class="basic-chip-value">{{ getOrderValue('clientNo') }}</span>
</span>
<span class="basic-chip">
<span class="basic-chip-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
<span class="basic-chip-value">{{ getOrderValue('trackNo') }}</span>
</span>
<span class="basic-chip">
<span class="basic-chip-label">{{ $t('order.myOrders.drawer.consigneeCountryName') }}</span>
<span class="basic-chip-value">{{ getOrderValue('consigneeCountryName') }}</span>
</span>
</div>
<div class="basic-summary-actions">
<a-button class="app-btn-search" @click="handleAskAdvisor">{{ $t('nanaAssistant.askAboutWaybill') }}
</a-button>
</div>
</div>
<div v-if="predeclareFailGuide" class="predeclare-fail-guide"> <div v-if="predeclareFailGuide" class="predeclare-fail-guide">
<p class="predeclare-fail-guide__reason">{{ predeclareFailGuide.reason }}</p> <p class="predeclare-fail-guide__reason">{{ predeclareFailGuide.reason }}</p>
<p v-if="predeclareFailGuide.tips?.[0]" class="predeclare-fail-guide__tip"> <p v-if="predeclareFailGuide.tips?.[0]" class="predeclare-fail-guide__tip">
@ -116,9 +92,6 @@
<p class="predeclare-fail-guide__raw">{{ predeclareFailGuide.rawReason }}</p> <p class="predeclare-fail-guide__raw">{{ predeclareFailGuide.rawReason }}</p>
</details> </details>
<div class="predeclare-fail-guide__actions"> <div class="predeclare-fail-guide__actions">
<a-button class="app-btn-search" @click="handleAskAdvisor">
{{ $t('order.myOrders.drawer.failGuideAskSupport') }}
</a-button>
<a-button class="app-btn-search" @click="startBasicEdit"> <a-button class="app-btn-search" @click="startBasicEdit">
{{ $t('order.myOrders.drawer.failGuideEdit') }} {{ $t('order.myOrders.drawer.failGuideEdit') }}
</a-button> </a-button>
@ -580,10 +553,7 @@
:class="{ active: isTrackNodeActive(item, i) }" :class="{ active: isTrackNodeActive(item, i) }"
> >
<div class="track-left"> <div class="track-left">
<span v-if="isTrackNodeActive(item, i)" class="track-icon active"> <span class="track-dot" :class="{ active: isTrackNodeActive(item, i) }" />
<icon-check-circle-fill />
</span>
<span v-else class="track-icon">{{ displayTrackTimeline.length - i }}</span>
<span <span
v-if="i < displayTrackTimeline.length - 1" v-if="i < displayTrackTimeline.length - 1"
class="track-line" class="track-line"
@ -591,29 +561,32 @@
/> />
</div> </div>
<div class="track-right"> <div class="track-right">
<div class="track-card"> <div class="track-row-head">
<div class="track-card-head"> <span class="track-status">{{ getTrackStatusLabel(item) }}</span>
<div class="track-title">{{ item.content || '--' }}</div> <a-tag v-if="isTrackNodeActive(item, i)" size="small" color="green">
<a-tag v-if="isTrackNodeActive(item, i)" size="small" color="green"> {{ $t('order.myOrders.drawer.track.latest') }}
{{ $t('order.myOrders.drawer.track.latest') }} </a-tag>
</a-tag> </div>
</div> <div v-if="getTrackZhDetail(item)" class="track-content track-content--zh">
<div v-if="formatTrackPlace(item)" class="track-meta track-place"> {{ getTrackZhDetail(item) }}
<icon-location class="track-meta-icon" /> </div>
<span>{{ formatTrackPlace(item) }}</span> <div v-if="getTrackEnDetail(item)" class="track-content track-content--en">
</div> {{ getTrackEnDetail(item) }}
<div v-if="formatTrackTimeDisplay(item)" class="track-meta track-time"> </div>
<span>{{ item.trackTimePrefix || '' }}{{ formatTrackTimeDisplay(item) }}</span> <div class="track-meta-row">
<span v-if="item.trackTimeZone" class="track-timezone"> <span v-if="formatTrackTimeDisplay(item)" class="track-meta-time">
{{ $t('order.myOrders.drawer.track.timezone', { zone: item.trackTimeZone }) }} {{ item.trackTimePrefix || '' }}{{ formatTrackTimeDisplay(item) }}
</span> </span>
</div> <span v-if="formatTrackPlace(item)" class="track-meta-place">
<div v-if="item.remark" class="track-meta track-remark"> {{ formatTrackPlace(item) }}
{{ $t('order.myOrders.drawer.track.remark') }}:{{ item.remark }} </span>
</div> <span v-if="item.trackTimeZone" class="track-meta-zone">
<div v-if="item.trackCode" class="track-meta track-code"> {{ item.trackTimeZone }}
{{ $t('order.myOrders.drawer.track.code') }}:{{ item.trackCode }} </span>
</div> <span v-if="item.trackCode" class="track-meta-code">{{ item.trackCode }}</span>
</div>
<div v-if="item.remark" class="track-remark">
{{ $t('order.myOrders.drawer.track.remark') }}:{{ item.remark }}
</div> </div>
</div> </div>
</div> </div>
@ -663,7 +636,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, IconCheckCircleFill, IconLocation, IconRobot, IconDown, IconUp } from '@arco-design/web-vue/es/icon'; import { IconRefresh, IconRobot, 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 {
@ -1444,15 +1417,50 @@ function formatTrackTimeDisplay(item: any) {
return formatTrackTime(item?.trackTime); return formatTrackTime(item?.trackTime);
} }
function normalizeTrackText(value: unknown) {
const text = value == null ? '' : String(value).trim();
return !text || text === '--' ? '' : text;
}
function formatTrackPlace(item: any) { function formatTrackPlace(item: any) {
return item?.trackPlace == null ? '' : String(item.trackPlace).trim(); const structured = [item?.trackPlaceNation, item?.trackPlaceProvince, item?.trackPlaceCity]
.map(normalizeTrackText)
.filter(Boolean);
if (structured.length) return structured.join(' · ');
return normalizeTrackText(item?.trackPlace);
}
function getTrackStatusLabel(item: any) {
return (
normalizeTrackText(item?.trackStatus) ||
normalizeTrackText(item?.content) ||
t('order.myOrders.drawer.track.statusUnknown')
);
}
/** 有独立状态时,中文内容作为详情;否则中文已作标题 */
function getTrackZhDetail(item: any) {
const status = normalizeTrackText(item?.trackStatus);
const zh = normalizeTrackText(item?.content);
if (!zh || !status || zh === status) return '';
return zh;
}
/** 英文内容;与标题/中文相同则不重复展示 */
function getTrackEnDetail(item: any) {
const en = normalizeTrackText(item?.enContent);
if (!en) return '';
const zh = normalizeTrackText(item?.content);
const status = normalizeTrackText(item?.trackStatus);
if (en === zh || en === status) return '';
return en;
} }
const displayTrackTimeline = computed(() => { const displayTrackTimeline = computed(() => {
const list = trackTimeline.value; const list = [...trackTimeline.value];
if (!list.length) return []; if (!list.length) return [];
if (list.some((item) => item?.isHighlight)) return [...list]; // 与顺丰/京东一致:最新在上
return [...list].reverse(); return list.sort((a, b) => Number(b?.trackTime || 0) - Number(a?.trackTime || 0));
}); });
function isTrackNodeActive(item: any, index: number) { function isTrackNodeActive(item: any, index: number) {
@ -2124,54 +2132,6 @@ defineExpose({
gap: 12px; gap: 12px;
} }
.basic-summary {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 16px;
border: 1px solid var(--color-border-2);
border-radius: 6px;
background: var(--app-card-bg);
}
.basic-summary-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.basic-summary-main {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.basic-summary-label {
font-size: 12px;
color: var(--color-text-3);
line-height: 20px;
}
.basic-summary-no {
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
line-height: 26px;
word-break: break-all;
}
.basic-summary-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.basic-summary-actions {
margin-top: 10px;
}
.predeclare-fail-guide { .predeclare-fail-guide {
margin-top: 12px; margin-top: 12px;
padding: 12px 14px; padding: 12px 14px;
@ -2229,29 +2189,6 @@ defineExpose({
} }
} }
.basic-chip {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 100%;
padding: 4px 10px;
border-radius: 4px;
background: var(--color-fill-2);
font-size: 12px;
line-height: 20px;
}
.basic-chip-label {
color: var(--color-text-3);
flex-shrink: 0;
}
.basic-chip-value {
color: var(--color-text-1);
font-weight: 500;
word-break: break-all;
}
.basic-view-switch { .basic-view-switch {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@ -3018,19 +2955,18 @@ defineExpose({
} }
.track-timeline { .track-timeline {
padding: 4px 0; padding: 4px 0 8px;
.track-item { .track-item {
display: flex; display: flex;
margin-bottom: 0; margin-bottom: 0;
&.active { &.active {
.track-card { .track-status {
border-color: #b9f0c2; color: var(--app-primary-color, #2d8c8c);
background: #f6ffed;
} }
.track-title { .track-content {
color: var(--color-text-1); color: var(--color-text-1);
} }
} }
@ -3039,41 +2975,38 @@ defineExpose({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 28px; width: 16px;
margin-right: 12px; margin-right: 14px;
flex-shrink: 0; flex-shrink: 0;
padding-top: 5px;
.track-icon { .track-dot {
width: 24px; width: 10px;
height: 24px; height: 10px;
line-height: 24px;
text-align: center;
background: var(--color-fill-2);
color: var(--color-text-2);
border-radius: 50%; border-radius: 50%;
font-size: 12px; background: var(--color-fill-3);
border: 2px solid var(--color-border-2);
box-sizing: border-box;
flex-shrink: 0; flex-shrink: 0;
&.active { &.active {
display: flex; width: 12px;
align-items: center; height: 12px;
justify-content: center; background: var(--app-primary-color, #2d8c8c);
background: transparent; border-color: color-mix(in srgb, var(--app-primary-color, #2d8c8c) 35%, #fff);
color: var(--app-primary-color, #2d8c8c); box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-primary-color, #2d8c8c) 18%, transparent);
font-size: 22px;
line-height: 1;
} }
} }
.track-line { .track-line {
width: 2px; width: 2px;
flex: 1; flex: 1;
min-height: 20px; min-height: 18px;
background: var(--color-fill-3); background: var(--color-fill-3);
margin: 4px 0; margin: 4px 0 0;
&.active { &.active {
background: var(--app-primary-color, #2d8c8c); background: color-mix(in srgb, var(--app-primary-color, #2d8c8c) 45%, var(--color-fill-3));
} }
} }
} }
@ -3081,61 +3014,75 @@ defineExpose({
.track-right { .track-right {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
padding-bottom: 16px; padding-bottom: 18px;
} }
.track-card { .track-row-head {
padding: 10px 12px;
border: 1px solid var(--color-border-2);
border-radius: 6px;
background: var(--app-card-bg);
}
.track-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
}
.track-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-1);
line-height: 22px;
word-break: break-all;
}
.track-meta {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px 8px; gap: 8px;
font-size: 12px;
color: var(--color-text-3);
margin-top: 6px;
line-height: 20px;
word-break: break-all;
} }
.track-place { .track-status {
font-size: 14px;
font-weight: 600;
color: var(--color-text-2);
line-height: 22px;
word-break: break-word;
}
.track-content {
margin-top: 4px;
font-size: 13px;
line-height: 20px;
color: var(--color-text-2);
word-break: break-word;
&--en {
margin-top: 2px;
font-size: 12px;
line-height: 18px;
color: var(--color-text-3);
}
}
.track-meta-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px 10px;
margin-top: 6px;
font-size: 12px;
line-height: 18px;
color: var(--color-text-3);
}
.track-meta-place {
color: var(--color-text-2); color: var(--color-text-2);
} }
.track-meta-icon { .track-meta-code {
font-size: 14px; padding: 0 6px;
border-radius: 3px;
background: var(--color-fill-2);
color: var(--color-text-3); color: var(--color-text-3);
flex-shrink: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 11px;
line-height: 18px;
} }
.track-timezone { .track-remark {
margin-top: 4px;
font-size: 12px;
line-height: 18px;
color: var(--color-text-3); color: var(--color-text-3);
word-break: break-word;
} }
}
.track-remark, .track-item:last-child .track-right {
.track-code { padding-bottom: 0;
color: var(--color-text-3);
}
} }
} }
</style> </style>

View File

@ -261,10 +261,11 @@ watch(
<a-alert <a-alert
v-if="preview.error" v-if="preview.error"
type="warning" type="warning"
:content="preview.error"
show-icon show-icon
class="pay-order-alert" class="pay-order-alert"
/> >
{{ preview.error }}
</a-alert>
<a-table <a-table
v-else v-else

View File

@ -142,9 +142,10 @@ function toggleExpand(id: number) {
<a-alert <a-alert
v-if="trace.failReason" v-if="trace.failReason"
type="warning" type="warning"
:content="trace.failReason"
style="margin-bottom: 12px" style="margin-bottom: 12px"
/> >
{{ trace.failReason }}
</a-alert>
<div v-else class="alloc-trace__conclusion"> <div v-else class="alloc-trace__conclusion">
{{ {{

View File

@ -290,14 +290,16 @@ function handleCancel() {
v-if="showInsuranceTip" v-if="showInsuranceTip"
type="info" type="info"
show-icon show-icon
:content="t('order.myOrders.claimed.tipInsured', { amount: declaredSumText })" >
/> {{ t('order.myOrders.claimed.tipInsured', { amount: declaredSumText }) }}
</a-alert>
<a-alert <a-alert
v-else-if="showUninsuredTip" v-else-if="showUninsuredTip"
type="warning" type="warning"
show-icon show-icon
:content="t('order.myOrders.claimed.tipUninsured')" >
/> {{ t('order.myOrders.claimed.tipUninsured') }}
</a-alert>
<div v-if="declaredSumNumber != null" class="claim-max-hint"> <div v-if="declaredSumNumber != null" class="claim-max-hint">
{{ t('order.myOrders.claimed.maxHint', { amount: declaredSumText }) }} {{ t('order.myOrders.claimed.maxHint', { amount: declaredSumText }) }}
</div> </div>

View File

@ -36,6 +36,9 @@
<a-button v-else class="app-btn-search" @click="startBasicEdit"> <a-button v-else class="app-btn-search" @click="startBasicEdit">
{{ $t('order.myOrders.drawer.actions.edit') }} {{ $t('order.myOrders.drawer.actions.edit') }}
</a-button> </a-button>
<a-button class="app-btn-search" @click="handleAskAdvisor">
{{ $t('nanaAssistant.askAboutWaybill') }}
</a-button>
</template> </template>
<template v-else-if="activeTab === 'weight'"> <template v-else-if="activeTab === 'weight'">
<template v-if="weightEditing"> <template v-if="weightEditing">
@ -80,6 +83,13 @@
<span class="toolbar-meta-label">{{ $t('order.myOrders.drawer.trackNo') }}</span> <span class="toolbar-meta-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
<span class="toolbar-meta-value">{{ trackOrderNumber || '--' }}</span> <span class="toolbar-meta-value">{{ trackOrderNumber || '--' }}</span>
</span> </span>
<a-tag
v-if="currentOrderStatusMeta"
size="small"
:style="getOrderStatusTagStyle(currentOrderStatusMeta)"
>
{{ $t(currentOrderStatusMeta.labelKey) }}
</a-tag>
<a-tag v-if="trackTimeline.length" size="small" color="arcoblue"> <a-tag v-if="trackTimeline.length" size="small" color="arcoblue">
{{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }} {{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }}
</a-tag> </a-tag>
@ -104,60 +114,6 @@
<div class="drawer-tab-content"> <div class="drawer-tab-content">
<!-- 基本信息 --> <!-- 基本信息 -->
<div v-if="activeTab === 'basic'" class="tab-panel basic-info"> <div v-if="activeTab === 'basic'" class="tab-panel basic-info">
<div class="basic-summary">
<div class="basic-summary-top">
<div class="basic-summary-main">
<span class="basic-summary-label">{{ $t('order.myOrders.drawer.serviceNo') }}</span>
<span class="basic-summary-no">{{ getOrderValue('serviceNo') }}</span>
</div>
<a-tag
v-if="currentOrderStatusMeta"
size="small"
:style="getOrderStatusTagStyle(currentOrderStatusMeta)"
>
{{ $t(currentOrderStatusMeta.labelKey) }}
</a-tag>
</div>
<div class="basic-summary-chips">
<span class="basic-chip">
<span class="basic-chip-label">{{ $t('order.myOrders.drawer.clientNo') }}</span>
<span class="basic-chip-value">{{ getOrderValue('clientNo') }}</span>
</span>
<span class="basic-chip">
<span class="basic-chip-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
<span class="basic-chip-value">{{ getOrderValue('trackNo') }}</span>
</span>
<span class="basic-chip">
<span class="basic-chip-label">{{ $t('order.myOrders.drawer.downstreamServiceNo') }}</span>
<span class="basic-chip-value">{{ getOrderValue('downstreamServiceNo') }}</span>
</span>
<span class="basic-chip">
<span class="basic-chip-label">{{ $t('order.myOrders.drawer.channelName') }}</span>
<span class="basic-chip-value basic-chip-value--with-action">
{{ getOrderValue('channelName') }}
<a-button
v-if="canViewAllocationTrace"
type="text"
size="mini"
class="alloc-eye-btn"
:title="$t('order.myOrders.allocationTrace.view')"
@click.stop="openAllocationTrace"
>
<template #icon><icon-eye /></template>
</a-button>
</span>
</span>
<span class="basic-chip">
<span class="basic-chip-label">{{ $t('order.myOrders.drawer.consigneeCountryName') }}</span>
<span class="basic-chip-value">{{ getOrderValue('consigneeCountryName') }}</span>
</span>
</div>
<div class="basic-summary-actions">
<a-button class="app-btn-search" @click="handleAskAdvisor">{{ $t('nanaAssistant.askAboutWaybill') }}
</a-button>
</div>
</div>
<div v-if="predeclareFailGuide" class="predeclare-fail-guide"> <div v-if="predeclareFailGuide" class="predeclare-fail-guide">
<p class="predeclare-fail-guide__reason">{{ predeclareFailGuide.reason }}</p> <p class="predeclare-fail-guide__reason">{{ predeclareFailGuide.reason }}</p>
<p v-if="predeclareFailGuide.tips?.[0]" class="predeclare-fail-guide__tip"> <p v-if="predeclareFailGuide.tips?.[0]" class="predeclare-fail-guide__tip">
@ -174,9 +130,6 @@
<p class="predeclare-fail-guide__raw">{{ predeclareFailGuide.rawReason }}</p> <p class="predeclare-fail-guide__raw">{{ predeclareFailGuide.rawReason }}</p>
</details> </details>
<div class="predeclare-fail-guide__actions"> <div class="predeclare-fail-guide__actions">
<a-button class="app-btn-search" @click="handleAskAdvisor">
{{ $t('order.myOrders.drawer.failGuideAskSupport') }}
</a-button>
<a-button class="app-btn-search" @click="startBasicEdit"> <a-button class="app-btn-search" @click="startBasicEdit">
{{ $t('order.myOrders.drawer.failGuideEdit') }} {{ $t('order.myOrders.drawer.failGuideEdit') }}
</a-button> </a-button>
@ -682,8 +635,9 @@
v-if="hasNegativeGrossProfit" v-if="hasNegativeGrossProfit"
type="warning" type="warning"
show-icon show-icon
:content="$t('order.myOrders.drawer.fee.negativeGrossProfitWarn')" >
/> {{ $t('order.myOrders.drawer.fee.negativeGrossProfitWarn') }}
</a-alert>
<div class="fee-view-switch"> <div class="fee-view-switch">
<a-radio-group <a-radio-group
@ -773,10 +727,7 @@
:class="{ active: isTrackNodeActive(item, i) }" :class="{ active: isTrackNodeActive(item, i) }"
> >
<div class="track-left"> <div class="track-left">
<span v-if="isTrackNodeActive(item, i)" class="track-icon active"> <span class="track-dot" :class="{ active: isTrackNodeActive(item, i) }" />
<icon-check-circle-fill />
</span>
<span v-else class="track-icon">{{ displayTrackTimeline.length - i }}</span>
<span <span
v-if="i < displayTrackTimeline.length - 1" v-if="i < displayTrackTimeline.length - 1"
class="track-line" class="track-line"
@ -784,29 +735,32 @@
/> />
</div> </div>
<div class="track-right"> <div class="track-right">
<div class="track-card"> <div class="track-row-head">
<div class="track-card-head"> <span class="track-status">{{ getTrackStatusLabel(item) }}</span>
<div class="track-title">{{ item.content || '--' }}</div> <a-tag v-if="isTrackNodeActive(item, i)" size="small" color="green">
<a-tag v-if="isTrackNodeActive(item, i)" size="small" color="green"> {{ $t('order.myOrders.drawer.track.latest') }}
{{ $t('order.myOrders.drawer.track.latest') }} </a-tag>
</a-tag> </div>
</div> <div v-if="getTrackZhDetail(item)" class="track-content track-content--zh">
<div v-if="formatTrackPlace(item)" class="track-meta track-place"> {{ getTrackZhDetail(item) }}
<icon-location class="track-meta-icon" /> </div>
<span>{{ formatTrackPlace(item) }}</span> <div v-if="getTrackEnDetail(item)" class="track-content track-content--en">
</div> {{ getTrackEnDetail(item) }}
<div v-if="formatTrackTimeDisplay(item)" class="track-meta track-time"> </div>
<span>{{ item.trackTimePrefix || '' }}{{ formatTrackTimeDisplay(item) }}</span> <div class="track-meta-row">
<span v-if="item.trackTimeZone" class="track-timezone"> <span v-if="formatTrackTimeDisplay(item)" class="track-meta-time">
{{ $t('order.myOrders.drawer.track.timezone', { zone: item.trackTimeZone }) }} {{ item.trackTimePrefix || '' }}{{ formatTrackTimeDisplay(item) }}
</span> </span>
</div> <span v-if="formatTrackPlace(item)" class="track-meta-place">
<div v-if="item.remark" class="track-meta track-remark"> {{ formatTrackPlace(item) }}
{{ $t('order.myOrders.drawer.track.remark') }}:{{ item.remark }} </span>
</div> <span v-if="item.trackTimeZone" class="track-meta-zone">
<div v-if="item.trackCode" class="track-meta track-code"> {{ item.trackTimeZone }}
{{ $t('order.myOrders.drawer.track.code') }}:{{ item.trackCode }} </span>
</div> <span v-if="item.trackCode" class="track-meta-code">{{ item.trackCode }}</span>
</div>
<div v-if="item.remark" class="track-remark">
{{ $t('order.myOrders.drawer.track.remark') }}:{{ item.remark }}
</div> </div>
</div> </div>
</div> </div>
@ -895,7 +849,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, IconCheckCircleFill, IconLocation, IconRobot, IconEye } from '@arco-design/web-vue/es/icon'; import { IconRefresh, IconRobot, IconEye } 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 {
@ -1605,15 +1559,50 @@ function formatTrackTimeDisplay(item: any) {
return formatTrackTime(item?.trackTime); return formatTrackTime(item?.trackTime);
} }
function normalizeTrackText(value: unknown) {
const text = value == null ? '' : String(value).trim();
return !text || text === '--' ? '' : text;
}
function formatTrackPlace(item: any) { function formatTrackPlace(item: any) {
return item?.trackPlace == null ? '' : String(item.trackPlace).trim(); const structured = [item?.trackPlaceNation, item?.trackPlaceProvince, item?.trackPlaceCity]
.map(normalizeTrackText)
.filter(Boolean);
if (structured.length) return structured.join(' · ');
return normalizeTrackText(item?.trackPlace);
}
function getTrackStatusLabel(item: any) {
return (
normalizeTrackText(item?.trackStatus) ||
normalizeTrackText(item?.content) ||
t('order.myOrders.drawer.track.statusUnknown')
);
}
/** 有独立状态时,中文内容作为详情;否则中文已作标题 */
function getTrackZhDetail(item: any) {
const status = normalizeTrackText(item?.trackStatus);
const zh = normalizeTrackText(item?.content);
if (!zh || !status || zh === status) return '';
return zh;
}
/** 英文内容;与标题/中文相同则不重复展示 */
function getTrackEnDetail(item: any) {
const en = normalizeTrackText(item?.enContent);
if (!en) return '';
const zh = normalizeTrackText(item?.content);
const status = normalizeTrackText(item?.trackStatus);
if (en === zh || en === status) return '';
return en;
} }
const displayTrackTimeline = computed(() => { const displayTrackTimeline = computed(() => {
const list = trackTimeline.value; const list = [...trackTimeline.value];
if (!list.length) return []; if (!list.length) return [];
if (list.some((item) => item?.isHighlight)) return [...list]; // 与顺丰/京东一致:最新在上
return [...list].reverse(); return list.sort((a, b) => Number(b?.trackTime || 0) - Number(a?.trackTime || 0));
}); });
function isTrackNodeActive(item: any, index: number) { function isTrackNodeActive(item: any, index: number) {
@ -2522,54 +2511,6 @@ defineExpose({
gap: 12px; gap: 12px;
} }
.basic-summary {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 16px;
border: 1px solid var(--color-border-2);
border-radius: 6px;
background: var(--app-card-bg);
}
.basic-summary-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.basic-summary-main {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.basic-summary-label {
font-size: 12px;
color: var(--color-text-3);
line-height: 20px;
}
.basic-summary-no {
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
line-height: 26px;
word-break: break-all;
}
.basic-summary-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.basic-summary-actions {
margin-top: 12px;
}
.predeclare-fail-guide { .predeclare-fail-guide {
margin-top: 12px; margin-top: 12px;
padding: 12px 14px; padding: 12px 14px;
@ -2627,30 +2568,6 @@ defineExpose({
} }
} }
.basic-chip {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 100%;
padding: 4px 10px;
border-radius: 4px;
background: var(--color-fill-2);
font-size: 12px;
line-height: 20px;
}
.basic-chip-label {
color: var(--color-text-3);
flex-shrink: 0;
}
.basic-chip-value {
color: var(--color-text-1);
font-weight: 500;
word-break: break-all;
}
.basic-chip-value--with-action,
.detail-field-text--with-action { .detail-field-text--with-action {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -3436,19 +3353,18 @@ defineExpose({
} }
.track-timeline { .track-timeline {
padding: 4px 0; padding: 4px 0 8px;
.track-item { .track-item {
display: flex; display: flex;
margin-bottom: 0; margin-bottom: 0;
&.active { &.active {
.track-card { .track-status {
border-color: #b9f0c2; color: var(--app-primary-color, #2d8c8c);
background: #f6ffed;
} }
.track-title { .track-content {
color: var(--color-text-1); color: var(--color-text-1);
} }
} }
@ -3457,41 +3373,38 @@ defineExpose({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 28px; width: 16px;
margin-right: 12px; margin-right: 14px;
flex-shrink: 0; flex-shrink: 0;
padding-top: 5px;
.track-icon { .track-dot {
width: 24px; width: 10px;
height: 24px; height: 10px;
line-height: 24px;
text-align: center;
background: var(--color-fill-2);
color: var(--color-text-2);
border-radius: 50%; border-radius: 50%;
font-size: 12px; background: var(--color-fill-3);
border: 2px solid var(--color-border-2);
box-sizing: border-box;
flex-shrink: 0; flex-shrink: 0;
&.active { &.active {
display: flex; width: 12px;
align-items: center; height: 12px;
justify-content: center; background: var(--app-primary-color, #2d8c8c);
background: transparent; border-color: color-mix(in srgb, var(--app-primary-color, #2d8c8c) 35%, #fff);
color: var(--app-primary-color, #2d8c8c); box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-primary-color, #2d8c8c) 18%, transparent);
font-size: 22px;
line-height: 1;
} }
} }
.track-line { .track-line {
width: 2px; width: 2px;
flex: 1; flex: 1;
min-height: 20px; min-height: 18px;
background: var(--color-fill-3); background: var(--color-fill-3);
margin: 4px 0; margin: 4px 0 0;
&.active { &.active {
background: var(--app-primary-color, #2d8c8c); background: color-mix(in srgb, var(--app-primary-color, #2d8c8c) 45%, var(--color-fill-3));
} }
} }
} }
@ -3499,61 +3412,75 @@ defineExpose({
.track-right { .track-right {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
padding-bottom: 16px; padding-bottom: 18px;
} }
.track-card { .track-row-head {
padding: 10px 12px;
border: 1px solid var(--color-border-2);
border-radius: 6px;
background: var(--app-card-bg);
}
.track-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
}
.track-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-1);
line-height: 22px;
word-break: break-all;
}
.track-meta {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px 8px; gap: 8px;
font-size: 12px;
color: var(--color-text-3);
margin-top: 6px;
line-height: 20px;
word-break: break-all;
} }
.track-place { .track-status {
font-size: 14px;
font-weight: 600;
color: var(--color-text-2);
line-height: 22px;
word-break: break-word;
}
.track-content {
margin-top: 4px;
font-size: 13px;
line-height: 20px;
color: var(--color-text-2);
word-break: break-word;
&--en {
margin-top: 2px;
font-size: 12px;
line-height: 18px;
color: var(--color-text-3);
}
}
.track-meta-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px 10px;
margin-top: 6px;
font-size: 12px;
line-height: 18px;
color: var(--color-text-3);
}
.track-meta-place {
color: var(--color-text-2); color: var(--color-text-2);
} }
.track-meta-icon { .track-meta-code {
font-size: 14px; padding: 0 6px;
border-radius: 3px;
background: var(--color-fill-2);
color: var(--color-text-3); color: var(--color-text-3);
flex-shrink: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 11px;
line-height: 18px;
} }
.track-timezone { .track-remark {
margin-top: 4px;
font-size: 12px;
line-height: 18px;
color: var(--color-text-3); color: var(--color-text-3);
word-break: break-word;
} }
}
.track-remark, .track-item:last-child .track-right {
.track-code { padding-bottom: 0;
color: var(--color-text-3);
}
} }
} }
</style> </style>

View File

@ -259,10 +259,11 @@ watch(
<a-alert <a-alert
v-if="preview.error" v-if="preview.error"
type="warning" type="warning"
:content="preview.error"
show-icon show-icon
class="pay-order-alert" class="pay-order-alert"
/> >
{{ preview.error }}
</a-alert>
<a-table <a-table
v-else v-else