统一 HandsontablePro 液态筛选表头、批量输入与自动列增强;请求日志改用公共列表组件;同步收件地址解析与 H5/PC 下单相关改动。 Co-authored-by: Cursor <cursoragent@cursor.com>
549 lines
18 KiB
Vue
549 lines
18 KiB
Vue
<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>
|