front/src/views/track/trackOrder/components/TrackOrderDetailDrawer.vue
zzh 407a0f7be6 feat: Handsontable 混合表头与请求日志改造,并完善 H5 下单/登录体验
统一 HandsontablePro 液态筛选表头、批量输入与自动列增强;请求日志改用公共列表组件;同步收件地址解析与 H5/PC 下单相关改动。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 20:09:42 +08:00

549 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<DrawerPro :footer="false" padding-bottom="16px" @register="register" @cancel="handleCancel">
<div class="detail-drawer-container">
<!-- Tab 切换 -->
<div class="detail-tabs">
<a-tabs
v-model:active-key="activeTab"
type="line"
size="large"
@change="handleTabChange"
>
<a-tab-pane
v-for="tab in tabList"
:key="tab.key"
:title="tab.label"
/>
</a-tabs>
</div>
<!-- Tab 内容 -->
<div class="detail-tab-content">
<!-- 明细 -->
<div v-if="activeTab === 'detail'" class="detail-content">
<!-- 操作按钮和开关 -->
<div class="detail-toolbar">
<div class="custom-buttons">
<a-button
class="btn-add"
size="small"
@click="handleAction('add')"
>
{{ $t('track.order.actions.addTrajectory') }}
</a-button>
<a-button
class="btn-update"
size="small"
@click="handleAction('update')"
>
{{ $t('track.order.actions.updateTrajectory') }}
</a-button>
<a-button
class="btn-delete"
size="small"
:disabled="!hasSelectedRows"
@click="handleAction('delete')"
>
{{ $t('track.order.actions.deleteTrajectory') }}
</a-button>
<a-button
class="btn-push"
size="small"
@click="handleAction('push')"
>
{{ $t('track.order.actions.pushTrajectory') }}
</a-button>
</div>
<div class="toolbar-right">
<a-checkbox :checked="isSourceTrajectory" @change="handleSourceTrajectoryChange">
{{ $t('track.order.detail.showAllSourceTrajectory') }}
</a-checkbox>
</div>
</div>
<!-- 明细表格 -->
<div ref="tableContainerRef" class="handsontable-container">
<HandsontablePro
:columns="detailColumns"
:data="detailTableData"
:height="tableHeight"
:enable-header-search="false"
:auto-hybrid-header="false"
:enable-header-filter="false"
:enable-header-sort="false"
:show-active-filter-bar="false"
@selection-change="handleDetailSelectionChange"
/>
</div>
</div>
<!-- 推送记录 -->
<div v-if="activeTab === 'pushRecord'" class="push-record-content">
<!-- 推送记录表格 -->
<div ref="tableContainerRef" class="handsontable-container">
<HandsontablePro
:columns="pushRecordColumns"
:data="pushRecordTableData"
:height="tableHeight"
:enable-header-search="false"
:auto-hybrid-header="false"
:enable-header-filter="false"
:enable-header-sort="false"
:show-active-filter-bar="false"
/>
</div>
</div>
<!-- 日志 -->
<div v-if="activeTab === 'log'" class="log-content">
<!-- 操作日志表格 -->
<div ref="tableContainerRef" class="handsontable-container">
<HandsontablePro
:columns="logColumns"
:data="logTableData"
:height="tableHeight"
:stretch-columns="true"
:enable-header-search="false"
:auto-hybrid-header="false"
:enable-header-filter="false"
:enable-header-sort="false"
:show-active-filter-bar="false"
/>
</div>
</div>
<!-- 轨迹抓取配置:ref 固定在始终存在的容器上,避免加载/空数据时高度为 0;a-spin 需参与 flex 撑满 -->
<div v-if="activeTab === 'pullConfig'" class="pull-config-content">
<a-spin :loading="pullConfigLoading" class="pull-config-spin">
<div ref="tableContainerRef" class="handsontable-container pull-config-table-host">
<HandsontablePro
v-if="pullConfigTableData.length"
:columns="pullConfigColumns"
:data="pullConfigTableData"
:height="pullConfigHandsontableHeight"
:enable-header-search="false"
:auto-hybrid-header="false"
:enable-header-filter="false"
:enable-header-sort="false"
:show-active-filter-bar="false"
/>
<a-empty
v-else-if="!pullConfigLoading"
:description="$t('track.order.pullConfig.noData')"
/>
</div>
</a-spin>
</div>
</div>
</div>
</DrawerPro>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import HandsontablePro from '@/components/HandsontablePro/index.vue';
import useDrawer from '@/components/DrawerPro/useDrawer';
import { $baseMessage } from '@/utils';
import { getGetTrackPushRecords, getGetOrderOperationsById } from '@/api/TrackOrder/trackOrder';
import { postOrderFindOrderPullConfigs } from '@/api/InterfaceMappingProvider/order';
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
const { t } = useI18n();
const { tableContainerRef, tableHeight } = useTableContainerHeight();
interface Props {
orderId?: number;
orderNumber?: string;
orderType?: number;
appKey?: string;
detailData?: API.TrackOrderDetailDto[];
isSourceTrajectory?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
orderId: undefined,
orderNumber: '',
orderType: 1,
appKey: '',
detailData: () => [],
isSourceTrajectory: false
});
const emit = defineEmits<{
action: [type: 'add' | 'update' | 'delete' | 'push', orderNumber?: string];
cancel: [];
rowSelect: [row: any];
toggleSourceTrajectory: [show: boolean];
}>();
const [register, { openDrawer: innerOpenDrawer, setDrawerProps, closeDrawer }] = useDrawer();
// Tab 相关
const activeTab = ref('detail');
const tabList = computed(() => [
{ key: 'detail', label: t('track.order.drawer.tabs.detail') },
{ key: 'pushRecord', label: t('track.order.drawer.tabs.pushRecord') },
{ key: 'log', label: t('track.order.drawer.tabs.log') },
{ key: 'pullConfig', label: t('track.order.drawer.tabs.pullConfig') }
]);
// 数据
const sourceTrajectoryData = ref<API.TrackOrderDetailDto[]>([]);
const pushRecordData = ref<any[]>([]);
const logData = ref<any[]>([]);
const selectedDetailRows = ref<any[]>([]);
const pullConfigData = ref<any[]>([]);
const pullConfigLoading = ref(false);
const hasSelectedRows = computed(() => selectedDetailRows.value.length > 0);
const pullConfigColumns = computed(() => [
{ key: 'pullOrderNo', label: t('track.order.pullConfig.table.pullOrderNo'), width: 180 },
{ key: 'serviceCode', label: t('track.order.pullConfig.table.serviceCode'), width: 120 },
{ key: 'startCode', label: t('track.order.pullConfig.table.startCode'), width: 150 },
{ key: 'endCode', label: t('track.order.pullConfig.table.endCode'), width: 150 },
{ key: 'silenceTime', label: t('track.order.pullConfig.table.silenceTime'), width: 170, className: 'htCenter' },
{ key: 'stopTime', label: t('track.order.pullConfig.table.stopTime'), width: 170, className: 'htCenter' },
{ key: 'pullStatus', label: t('track.order.pullConfig.table.pullStatus'), width: 120, className: 'htCenter' },
{ key: 'startTime', label: t('track.order.pullConfig.table.startTime'), width: 180 },
{ key: 'endTime', label: t('track.order.pullConfig.table.endTime'), width: 180 },
]);
/** 抽屉内轨迹配置表:避免容器高度未算出时为 0 */
const pullConfigHandsontableHeight = computed(() =>
Math.max(tableHeight.value, 280)
);
const pullConfigTableData = computed(() =>
(pullConfigData.value || []).map((row: any) => ({
pullOrderNo: row?.pullOrderNo ?? '--',
serviceCode: row?.serviceCode ?? '--',
startCode: row?.startCode ?? '--',
endCode: row?.endCode ?? '--',
silenceTime: row?.silenceTime ?? '--',
stopTime: row?.stopTime ?? '--',
pullStatus:
row?.pullStatus === 0
? t('track.order.pullConfig.pullStatus.initial')
: row?.pullStatus === 1
? t('track.order.pullConfig.pullStatus.silenceBeforeStart')
: row?.pullStatus === 2
? t('track.order.pullConfig.pullStatus.pulling')
: row?.pullStatus === 3
? t('track.order.pullConfig.pullStatus.silenceAfterEnd')
: row?.pullStatus === 4
? t('track.order.pullConfig.pullStatus.completed')
: row?.pullStatus ?? '--',
startTime: row?.startTime ?? '--',
endTime: row?.endTime ?? '--',
}))
);
// 监听详情数据变化
watch(() => props.detailData, (newData) => {
sourceTrajectoryData.value = (newData || []).map((item: any) => ({ ...item }));
}, { immediate: true, deep: true });
// 处理服务商轨迹开关变化
function handleSourceTrajectoryChange(checked: boolean) {
emit('toggleSourceTrajectory', checked);
}
const detailColumns = computed(() => [
{ key: 'trackTime', label: t('track.order.detail.title.trackTime'), width: 180 },
{ key: 'trackTimeZoneDisplay', label: t('track.order.detail.title.trackTimeZone'), width: 120 },
{ key: 'trackPlace', label: t('track.order.detail.title.trackPlace'), width: 200 },
{ key: 'content', label: t('track.order.detail.title.trajectoryContent'), width: 180 },
{ key: 'enContent', label: t('track.order.detail.title.trajectoryContentEn'), width: 250 },
{ key: 'originTrackContent', label: t('track.order.detail.title.originTrackContent'), width: 250 },
{ key: 'pushContent', label: t('track.order.detail.title.pushDetail'), width: 120 },
{ key: 'isVisibilityText', label: t('track.order.detail.title.isVisibility'), width: 120, className: 'htCenter' },
{ key: 'createTime', label: t('track.order.table.createTime'), width: 150 },
{ key: 'createUser', label: t('track.order.detail.title.createUser'), width: 120 },
{ key: 'sourceText', label: t('track.order.detail.title.trajectoryType'), width: 120 },
]);
const detailTableData = computed(() =>
sourceTrajectoryData.value.map((row: any) => ({
...row,
trackTime: row.trackTime || '--',
trackTimeZoneDisplay: row.trackTimeZoneValue !== undefined && row.trackTimeZoneValue !== null
? String(row.trackTimeZoneValue)
: '--',
trackPlace: row.trackPlace || '--',
content: row.content || '--',
enContent: row.enContent || '--',
originTrackContent: row.originTrackContent || '--',
pushContent: row.pushContent || '--',
isVisibilityText: row.isVisibility ? t('track.order.common.yes') : t('track.order.common.no'),
createTime: row.createTime || '--',
createUser: row.createUser || '--',
sourceText: row.source === 1
? t('track.order.detail.trajectoryType.external')
: row.source === 2
? t('track.order.detail.trajectoryType.internal')
: row.source === 3
? t('track.order.detail.trajectoryType.manual')
: '--',
}))
);
const pushRecordColumns = computed(() => [
{ key: 'requestId', label: t('track.order.pushRecord.table.requestId'), width: 150 },
{ key: 'isSuccessText', label: t('track.order.pushRecord.table.isSuccess'), width: 100 },
{ key: 'message', label: t('track.order.pushRecord.table.message'), width: 200 },
{ key: 'optionType', label: t('track.order.pushRecord.table.optionType'), width: 120 },
{ key: 'trackStatus', label: t('track.order.pushRecord.table.trackStatus'), width: 120 },
{ key: 'trackCode', label: t('track.order.pushRecord.table.trackCode'), width: 120 },
{ key: 'trackContent', label: t('track.order.pushRecord.table.trackContent'), width: 200 },
{ key: 'trackPlace', label: t('track.order.pushRecord.table.trackPlace'), width: 150 },
{ key: 'trackGTMTimeZone', label: t('track.order.pushRecord.table.trackGTMTimeZone'), width: 120 },
{ key: 'pushContent', label: t('track.order.pushRecord.table.pushContent'), width: 300 },
{ key: 'createTime', label: t('track.order.pushRecord.table.createTime'), width: 180 },
]);
const pushRecordTableData = computed(() =>
pushRecordData.value.map((row: any) => ({
requestId: row.requestId || '--',
isSuccessText: row.isSuccess ? t('track.order.common.yes') : t('track.order.common.no'),
message: row.message || '--',
optionType: row.optionType || '--',
trackStatus: row.trackStatus || '--',
trackCode: row.trackCode || '--',
trackContent: row.trackContent || '--',
trackPlace: row.trackPlace || '--',
trackGTMTimeZone: row.trackGTMTimeZone || '--',
pushContent: row.pushContent || '--',
createTime: row.createTime || '--',
}))
);
const logColumns = computed(() => [
{ key: 'createTime', label: t('track.order.log.table.createTime'), width: 180 },
{ key: 'content', label: t('track.order.log.table.content'), width: 400 },
{ key: 'optionType', label: t('track.order.log.table.optionType'), width: 150 },
{ key: 'createUserName', label: t('track.order.log.table.createUserName'), width: 150 },
]);
const logTableData = computed(() =>
logData.value.map((row: any) => ({
createTime: row.createTime || '--',
content: row.content || '--',
optionType: row.optionType || '--',
createUserName: row.createUserName || '--',
}))
);
// 加载推送记录数据
async function loadPushRecordData(orderId: number) {
try {
const response: any = await getGetTrackPushRecords({ orderId });
pushRecordData.value = response?.data || response || [];
} catch (error) {
$baseMessage('加载推送记录失败', 'error');
pushRecordData.value = [];
}
}
// 加载日志数据
async function loadLogData(orderId: number) {
try {
const response: any = await getGetOrderOperationsById({ orderId });
logData.value = response?.data || response || [];
} catch (error) {
$baseMessage('加载日志数据失败', 'error');
logData.value = [];
}
}
// 加载轨迹抓取配置
async function loadPullConfigData() {
if (!props.appKey || !props.orderNumber) {
pullConfigData.value = [];
return;
}
pullConfigLoading.value = true;
pullConfigData.value = [];
try {
const orderNos = props.orderNumber.split(',').map((s) => s.trim()).filter(Boolean);
const response: any = await postOrderFindOrderPullConfigs({
appKey: props.appKey,
orderType: props.orderType ?? 1,
orderNos
});
// request 拦截器可能已解包为 data 本体;兼容 items / data 数组等多种返回
let items: any[] = [];
if (Array.isArray(response)) {
items = response;
} else if (response && typeof response === 'object') {
if (Array.isArray(response.items)) items = response.items;
else if (Array.isArray(response.data?.items)) items = response.data.items;
else if (Array.isArray(response.data)) items = response.data;
}
pullConfigData.value = items;
} catch (error) {
$baseMessage('加载轨迹抓取配置失败', 'error');
pullConfigData.value = [];
} finally {
pullConfigLoading.value = false;
}
}
// 处理 Tab 切换
function handleTabChange(tabKey: string) {
if (!props.orderId && tabKey !== 'pullConfig') return;
switch (tabKey) {
case 'detail':
break;
case 'pushRecord':
loadPushRecordData(props.orderId!);
break;
case 'log':
loadLogData(props.orderId!);
break;
case 'pullConfig':
loadPullConfigData();
break;
}
}
// 处理操作按钮点击
function handleAction(type: 'add' | 'update' | 'delete' | 'push') {
emit('action', type, props.orderNumber);
}
function handleDetailSelectionChange(rows: any[]) {
selectedDetailRows.value = rows || [];
if (selectedDetailRows.value.length > 0) {
emit('rowSelect', selectedDetailRows.value[0]);
}
}
// 处理取消
function handleCancel() {
emit('cancel');
}
// 暴露方法
defineExpose({
openDrawer: () => {
// 每次打开详情时,默认切回“明细”Tab
activeTab.value = 'detail';
innerOpenDrawer();
},
setDrawerProps,
closeDrawer,
updateDetailData: () => undefined,
getSelectedRows: () => selectedDetailRows.value
});
</script>
<style lang="less" scoped>
.detail-drawer-container {
display: flex;
flex-direction: column;
height: 100%;
.detail-tabs {
margin-bottom: 16px;
}
.detail-tab-content {
flex: 1;
overflow: hidden;
.detail-content,
.push-record-content,
.log-content,
.pull-config-content {
height: 100%;
display: flex;
flex-direction: column;
}
.pull-config-content {
flex: 1;
min-height: 0;
padding: 16px 0;
// Spin 默认 inline-block,在 flex 列里不撑高,会导致内部 Handsontable 高度为 0
.pull-config-spin {
flex: 1;
min-height: 0;
display: flex !important;
flex-direction: column;
width: 100%;
align-self: stretch;
}
.pull-config-table-host {
flex: 1;
min-height: 280px;
}
}
.detail-toolbar {
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
.custom-buttons {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
// 符合设计规范:border-radius 4px, height 32px
:deep(.custom-buttons .arco-btn) {
background: var(--app-primary-color);
border-color: var(--app-primary-color);
color: #ffffff;
border-radius: 4px;
font-weight: 500;
transition: all 0.2s ease;
border: 1px solid;
padding: 0 12px;
height: 32px;
&:hover:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
&:active:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
&:disabled {
opacity: 0.6;
}
}
}
}
.handsontable-container {
width: 100%;
border: 0.5px solid #f0f0f0;
border-radius: 4px;
overflow: hidden;
flex: 1;
min-height: 0;
.handsontable-wrapper {
width: 100%;
height: 100%;
}
}
}
:deep(.arco-drawer-footer) {
display: none !important;
}
</style>