fix: 抽屉去顶栏/圆角,订单操作按钮统一到 Tab 下

DrawerPro 默认隐藏空 header;抽屉无圆角;修复全部轨迹开关;详情操作从 footer 挪到 Tab 工具栏。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-09-03 18:05:14 +08:00
parent a703d1efdd
commit f71d2f1088
7 changed files with 257 additions and 139 deletions

View File

@ -135,13 +135,13 @@ body .arco-modal-simple .arco-modal-title {
text-align: left; text-align: left;
} }
/* Rounded corners to align with SimpleAdmin (Element Plus-like 4px) */ /* Rounded corners to align with SimpleAdmin (Element Plus-like 4px)
抽屉贴边展开,不做圆角 */
body .arco-card, body .arco-card,
body .arco-card-bordered, body .arco-card-bordered,
body .arco-modal, body .arco-modal,
body .arco-popover-content, body .arco-popover-content,
body .arco-dropdown, body .arco-dropdown,
body .arco-drawer,
body .arco-input-wrapper, body .arco-input-wrapper,
body .arco-textarea-wrapper, body .arco-textarea-wrapper,
body .arco-select, body .arco-select,
@ -155,6 +155,10 @@ body .arco-notification {
border-radius: 8px !important; border-radius: 8px !important;
} }
body .arco-drawer {
border-radius: 0 !important;
}
/* vxe table wrapper */ /* vxe table wrapper */
body .vxe-table--border, body .vxe-table--border,
body .vxe-grid { body .vxe-grid {

View File

@ -10,11 +10,18 @@
visible: false, visible: false,
width: 400, width: 400,
placement: 'right', placement: 'right',
// 默认不展示顶栏(可点遮罩关闭);需要标题时 setDrawerProps({ title }) 会自动打开
header: false,
...unref(Attrs), ...unref(Attrs),
}); });
const setDrawerProps = (props: DrawerProps) => { const setDrawerProps = (props: DrawerProps) => {
Object.assign(defaultProps, props); const next: DrawerProps = { ...props };
// 传入 title 且未显式指定 header 时,自动显示顶栏
if (next.title && next.header === undefined) {
next.header = true;
}
Object.assign(defaultProps, next);
}; };
const instance = getCurrentInstance(); const instance = getCurrentInstance();

View File

@ -1,25 +1,10 @@
<template> <template>
<DrawerPro <DrawerPro
:footer="false"
padding-bottom="16px" padding-bottom="16px"
@register="register" @register="register"
@cancel="handleCancel" @cancel="handleCancel"
> >
<template #title />
<template #footer>
<div class="drawer-footer-banner order-detail-drawer-shell">
<template v-if="activeTab === 'basic'">
<template v-if="basicEditing">
<a-button class="app-btn-search" @click="cancelBasicEdit">{{ $t('order.myOrders.drawer.actions.cancel') }}</a-button>
<a-button class="app-btn-search" :loading="basicSaving" @click="handleBasicEditSave">
{{ $t('order.myOrders.drawer.actions.save') }}
</a-button>
</template>
<a-button v-else class="app-btn-search" @click="startBasicEdit">
{{ $t('order.myOrders.drawer.actions.edit') }}
</a-button>
</template>
</div>
</template>
<div class="order-detail-drawer order-detail-drawer-shell"> <div class="order-detail-drawer order-detail-drawer-shell">
<div class="drawer-tabs"> <div class="drawer-tabs">
<a-tabs <a-tabs
@ -36,6 +21,48 @@
</a-tabs> </a-tabs>
</div> </div>
<div
v-if="activeTab === 'basic' || activeTab === 'track' || activeTab === 'stockBind'"
class="drawer-tab-toolbar"
>
<div class="toolbar-left">
<template v-if="activeTab === 'basic'">
<template v-if="basicEditing">
<a-button class="app-btn-search" @click="cancelBasicEdit">{{ $t('order.myOrders.drawer.actions.cancel') }}</a-button>
<a-button class="app-btn-search" :loading="basicSaving" @click="handleBasicEditSave">
{{ $t('order.myOrders.drawer.actions.save') }}
</a-button>
</template>
<a-button v-else class="app-btn-search" @click="startBasicEdit">
{{ $t('order.myOrders.drawer.actions.edit') }}
</a-button>
</template>
<template v-else-if="activeTab === 'track'">
<span class="toolbar-meta">
<span class="toolbar-meta-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
<span class="toolbar-meta-value">{{ trackOrderNumber || '--' }}</span>
</span>
<a-tag v-if="trackTimeline.length" size="small" color="arcoblue">
{{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }}
</a-tag>
<a-button
class="app-btn-search"
:loading="trackLoading"
@click="loadTrackTimeline"
>{{ $t('order.myOrders.drawer.track.refresh') }}
</a-button>
</template>
<template v-else-if="activeTab === 'stockBind'">
<a-button
class="app-btn-search"
:loading="stockBindLoading"
@click="reloadStockBind"
>{{ $t('order.myOrders.drawer.track.refresh') }}
</a-button>
</template>
</div>
</div>
<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">
@ -544,24 +571,6 @@
<!-- 轨迹 --> <!-- 轨迹 -->
<div v-if="activeTab === 'track'" class="tab-panel track-info"> <div v-if="activeTab === 'track'" class="tab-panel track-info">
<div class="track-toolbar">
<div class="track-toolbar-left">
<span class="track-toolbar-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
<span class="track-toolbar-value">{{ trackOrderNumber || '--' }}</span>
</div>
<div class="track-toolbar-right">
<a-tag v-if="trackTimeline.length" size="small" color="arcoblue">
{{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }}
</a-tag>
<a-button
class="app-btn-search"
:loading="trackLoading"
@click="loadTrackTimeline"
>{{ $t('order.myOrders.drawer.track.refresh') }}
</a-button>
</div>
</div>
<a-spin :loading="trackLoading" class="track-spin"> <a-spin :loading="trackLoading" class="track-spin">
<div v-if="displayTrackTimeline.length" class="track-timeline"> <div v-if="displayTrackTimeline.length" class="track-timeline">
<div <div
@ -631,8 +640,10 @@
<!-- 绑定包裹/库存 --> <!-- 绑定包裹/库存 -->
<div v-if="activeTab === 'stockBind'" class="tab-panel stock-bind-info"> <div v-if="activeTab === 'stockBind'" class="tab-panel stock-bind-info">
<OrderStockBindTab <OrderStockBindTab
ref="stockBindTabRef"
:order-id="orderId" :order-id="orderId"
:active="activeTab === 'stockBind'" :active="activeTab === 'stockBind'"
hide-refresh
/> />
</div> </div>
</div> </div>
@ -723,6 +734,17 @@ const recalcReceivableLoading = ref(false);
const recalcPayableLoading = ref(false); const recalcPayableLoading = ref(false);
const addFeeModalVisible = ref(false); const addFeeModalVisible = ref(false);
const addFeeDefaultType = ref<'receivable' | 'payable'>('receivable'); const addFeeDefaultType = ref<'receivable' | 'payable'>('receivable');
const stockBindTabRef = ref<{ reload?: () => Promise<void> | void } | null>(null);
const stockBindLoading = ref(false);
async function reloadStockBind() {
stockBindLoading.value = true;
try {
await stockBindTabRef.value?.reload?.();
} finally {
stockBindLoading.value = false;
}
}
type BasicViewMode = 'all' | 'basic' | 'sender' | 'recipient' | 'declaration'; type BasicViewMode = 'all' | 'basic' | 'sender' | 'recipient' | 'declaration';
const basicViewMode = ref<BasicViewMode>('all'); const basicViewMode = ref<BasicViewMode>('all');
@ -1982,10 +2004,44 @@ defineExpose({
} }
.drawer-tabs { .drawer-tabs {
margin-bottom: 16px; margin-bottom: 12px;
flex-shrink: 0; flex-shrink: 0;
} }
.drawer-tab-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 12px;
flex-shrink: 0;
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.toolbar-meta {
display: inline-flex;
align-items: baseline;
gap: 6px;
margin-right: 4px;
}
.toolbar-meta-label {
font-size: 13px;
color: var(--color-text-3);
}
.toolbar-meta-value {
font-size: 13px;
font-weight: 500;
color: var(--color-text-1);
}
}
.drawer-tab-content { .drawer-tab-content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
@ -1997,14 +2053,6 @@ defineExpose({
padding: 0 4px; padding: 0 4px;
} }
.drawer-footer-banner {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
width: 100%;
}
.inline-edit-form { .inline-edit-form {
:deep(.arco-form-item) { :deep(.arco-form-item) {
margin-bottom: 12px; margin-bottom: 12px;

View File

@ -8,6 +8,8 @@ import { resolveOmsAssetUrl } from '@/api/oms/common'
const props = defineProps<{ const props = defineProps<{
orderId: number | null | undefined orderId: number | null | undefined
active: boolean active: boolean
/** 刷新按钮已上移到抽屉 Tab 工具栏时隐藏内部刷新 */
hideRefresh?: boolean
}>() }>()
const { t } = useI18n() const { t } = useI18n()
@ -113,7 +115,13 @@ defineExpose({ reload: loadDetail })
{{ t('order.myOrders.stockBind.cartSummary', { lines: items.length, qty: totalBindQty }) }} {{ t('order.myOrders.stockBind.cartSummary', { lines: items.length, qty: totalBindQty }) }}
</span> </span>
</div> </div>
<a-button type="text" size="small" :loading="loading" @click="loadDetail">{{ t('order.myOrders.drawer.track.refresh') }} <a-button
v-if="!props.hideRefresh"
type="text"
size="small"
:loading="loading"
@click="loadDetail"
>{{ t('order.myOrders.drawer.track.refresh') }}
</a-button> </a-button>
</div> </div>

View File

@ -1,12 +1,31 @@
<template> <template>
<DrawerPro <DrawerPro
:footer="false"
padding-bottom="16px" padding-bottom="16px"
@register="register" @register="register"
@cancel="handleCancel" @cancel="handleCancel"
> >
<template #title /> <div class="order-detail-drawer order-detail-drawer-shell">
<template #footer> <div class="drawer-tabs">
<div class="drawer-footer-banner order-detail-drawer-shell"> <a-tabs
v-model:active-key="activeTab"
type="line"
size="medium"
@change="handleTabChange"
>
<a-tab-pane
v-for="tab in tabList"
:key="tab.key"
:title="tab.label"
/>
</a-tabs>
</div>
<div
v-if="activeTab === 'basic' || activeTab === 'weight' || activeTab === 'fee' || activeTab === 'track' || activeTab === 'stockBind'"
class="drawer-tab-toolbar"
>
<div class="toolbar-left">
<template v-if="activeTab === 'basic'"> <template v-if="activeTab === 'basic'">
<template v-if="basicEditing"> <template v-if="basicEditing">
<a-button class="app-btn-search" @click="cancelBasicEdit">{{ $t('order.myOrders.drawer.actions.cancel') }}</a-button> <a-button class="app-btn-search" @click="cancelBasicEdit">{{ $t('order.myOrders.drawer.actions.cancel') }}</a-button>
@ -56,22 +75,30 @@
{{ $t('order.myOrders.drawer.actions.recalculatePayable') }} {{ $t('order.myOrders.drawer.actions.recalculatePayable') }}
</a-button> </a-button>
</template> </template>
</div> <template v-else-if="activeTab === 'track'">
<span class="toolbar-meta">
<span class="toolbar-meta-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
<span class="toolbar-meta-value">{{ trackOrderNumber || '--' }}</span>
</span>
<a-tag v-if="trackTimeline.length" size="small" color="arcoblue">
{{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }}
</a-tag>
<a-button
class="app-btn-search"
:loading="trackLoading"
@click="loadTrackTimeline"
>{{ $t('order.myOrders.drawer.track.refresh') }}
</a-button>
</template> </template>
<div class="order-detail-drawer order-detail-drawer-shell"> <template v-else-if="activeTab === 'stockBind'">
<div class="drawer-tabs"> <a-button
<a-tabs class="app-btn-search"
v-model:active-key="activeTab" :loading="stockBindLoading"
type="line" @click="reloadStockBind"
size="medium" >{{ $t('order.myOrders.drawer.track.refresh') }}
@change="handleTabChange" </a-button>
> </template>
<a-tab-pane </div>
v-for="tab in tabList"
:key="tab.key"
:title="tab.label"
/>
</a-tabs>
</div> </div>
<div class="drawer-tab-content"> <div class="drawer-tab-content">
@ -737,24 +764,6 @@
<!-- 轨迹 --> <!-- 轨迹 -->
<div v-if="activeTab === 'track'" class="tab-panel track-info"> <div v-if="activeTab === 'track'" class="tab-panel track-info">
<div class="track-toolbar">
<div class="track-toolbar-left">
<span class="track-toolbar-label">{{ $t('order.myOrders.drawer.trackNo') }}</span>
<span class="track-toolbar-value">{{ trackOrderNumber || '--' }}</span>
</div>
<div class="track-toolbar-right">
<a-tag v-if="trackTimeline.length" size="small" color="arcoblue">
{{ $t('order.myOrders.drawer.track.count', { count: trackTimeline.length }) }}
</a-tag>
<a-button
class="app-btn-search"
:loading="trackLoading"
@click="loadTrackTimeline"
>{{ $t('order.myOrders.drawer.track.refresh') }}
</a-button>
</div>
</div>
<a-spin :loading="trackLoading" class="track-spin"> <a-spin :loading="trackLoading" class="track-spin">
<div v-if="displayTrackTimeline.length" class="track-timeline"> <div v-if="displayTrackTimeline.length" class="track-timeline">
<div <div
@ -822,8 +831,10 @@
<!-- 绑定包裹/库存 --> <!-- 绑定包裹/库存 -->
<div v-if="activeTab === 'stockBind'" class="tab-panel stock-bind-info"> <div v-if="activeTab === 'stockBind'" class="tab-panel stock-bind-info">
<OrderStockBindTab <OrderStockBindTab
ref="stockBindTabRef"
:order-id="orderId" :order-id="orderId"
:active="activeTab === 'stockBind'" :active="activeTab === 'stockBind'"
hide-refresh
/> />
</div> </div>
@ -958,6 +969,17 @@ const selectedAdditionalServices = ref<string[]>([]);
const productHasAdditionalServices = ref(false); const productHasAdditionalServices = ref(false);
const additionalServiceDictRows = ref<DictDto[]>([]); const additionalServiceDictRows = ref<DictDto[]>([]);
const exportCustomsDictRows = ref<DictDto[]>([]); const exportCustomsDictRows = ref<DictDto[]>([]);
const stockBindTabRef = ref<{ reload?: () => Promise<void> | void } | null>(null);
const stockBindLoading = ref(false);
async function reloadStockBind() {
stockBindLoading.value = true;
try {
await stockBindTabRef.value?.reload?.();
} finally {
stockBindLoading.value = false;
}
}
type BasicViewMode = 'all' | 'basic' | 'sender' | 'recipient' | 'declaration'; type BasicViewMode = 'all' | 'basic' | 'sender' | 'recipient' | 'declaration';
const basicViewMode = ref<BasicViewMode>('all'); const basicViewMode = ref<BasicViewMode>('all');
@ -2380,10 +2402,44 @@ defineExpose({
} }
.drawer-tabs { .drawer-tabs {
margin-bottom: 16px; margin-bottom: 12px;
flex-shrink: 0; flex-shrink: 0;
} }
.drawer-tab-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 12px;
flex-shrink: 0;
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.toolbar-meta {
display: inline-flex;
align-items: baseline;
gap: 6px;
margin-right: 4px;
}
.toolbar-meta-label {
font-size: 13px;
color: var(--color-text-3);
}
.toolbar-meta-value {
font-size: 13px;
font-weight: 500;
color: var(--color-text-1);
}
}
.drawer-tab-content { .drawer-tab-content {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
@ -2395,14 +2451,6 @@ defineExpose({
padding: 0 4px; padding: 0 4px;
} }
.drawer-footer-banner {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
width: 100%;
}
.inline-edit-form { .inline-edit-form {
:deep(.arco-form-item) { :deep(.arco-form-item) {
margin-bottom: 12px; margin-bottom: 12px;

View File

@ -51,7 +51,10 @@
</a-button> </a-button>
</div> </div>
<div class="toolbar-right"> <div class="toolbar-right">
<a-checkbox :checked="isSourceTrajectory" @change="handleSourceTrajectoryChange"> <a-checkbox
:model-value="isSourceTrajectory"
@change="handleSourceTrajectoryChange"
>
{{ $t('track.order.detail.showAllSourceTrajectory') }} {{ $t('track.order.detail.showAllSourceTrajectory') }}
</a-checkbox> </a-checkbox>
</div> </div>
@ -241,9 +244,9 @@ watch(() => props.detailData, (newData) => {
sourceTrajectoryData.value = (newData || []).map((item: any) => ({ ...item })); sourceTrajectoryData.value = (newData || []).map((item: any) => ({ ...item }));
}, { immediate: true, deep: true }); }, { immediate: true, deep: true });
// 处理服务商轨迹开关变化 // 处理服务商轨迹开关变化(Arco Checkbox 使用 modelValue,无 checked prop)
function handleSourceTrajectoryChange(checked: boolean) { function handleSourceTrajectoryChange(checked: boolean | (string | number | boolean)[]) {
emit('toggleSourceTrajectory', checked); emit('toggleSourceTrajectory', !!checked);
} }
const detailColumns = computed(() => [ const detailColumns = computed(() => [

View File

@ -787,6 +787,7 @@ import { useI18n } from 'vue-i18n';
// 关闭详情抽屉 // 关闭详情抽屉
function closeDetailDrawerHandler() { function closeDetailDrawerHandler() {
currentDetailRow.value = null; currentDetailRow.value = null;
isSourceTrajectory.value = false;
} }
// 使用操作处理 composable // 使用操作处理 composable
@ -1537,13 +1538,12 @@ import { useI18n } from 'vue-i18n';
watch( watch(
() => isSourceTrajectory.value, () => isSourceTrajectory.value,
(val) => { (val) => {
// 勾选显示服务商全部源轨迹则把没有轨迹代码映射的服务商源轨迹展示到列表上,注意该列没有更新和删除的按钮。 // 勾选显示服务商全部源轨迹:含未映射轨迹代码的源轨迹;勿用当前列表 length===0 提前 return,
if (sourceTrajectoryData.value.length === 0) return; // 否则「过滤后为空、全量仍有数据」时勾选无效果。
if (val) const full = notSourceTrajectoryDataTemporaryStorage.value;
sourceTrajectoryData.value = const mapped = sourceTrajectoryDataTemporaryStorage.value;
notSourceTrajectoryDataTemporaryStorage.value; if (!full.length && !mapped.length) return;
else sourceTrajectoryData.value = val ? full : mapped;
sourceTrajectoryData.value = sourceTrajectoryDataTemporaryStorage.value;
} }
); );