front/src/views/track/trackOrder/components/TrackOrderDetailDrawer.vue
2026-03-16 19:38:49 +08:00

752 lines
21 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 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"
type="primary"
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>
<!-- Handsontable 表格 -->
<div class="handsontable-container">
<div ref="detailHotContainer" class="handsontable-wrapper ht-theme-classic"></div>
</div>
</div>
<!-- 推送记录 -->
<div v-if="activeTab === 'pushRecord'" class="push-record-content">
<!-- Handsontable 表格 -->
<div class="handsontable-container">
<div ref="pushRecordHotContainer" class="handsontable-wrapper ht-theme-classic"></div>
</div>
</div>
<!-- 日志 -->
<div v-if="activeTab === 'log'" class="log-content">
<!-- Handsontable 表格 -->
<div class="handsontable-container">
<div ref="logHotContainer" class="handsontable-wrapper ht-theme-classic"></div>
</div>
</div>
<!-- 轨迹抓取配置 -->
<div v-if="activeTab === 'pullConfig'" class="pull-config-content">
<a-spin :loading="pullConfigLoading">
<pre v-if="pullConfigJson" class="pull-config-json">{{ pullConfigJson }}</pre>
<a-empty v-else-if="!pullConfigLoading" :description="$t('track.order.pullConfig.noData')" />
</a-spin>
</div>
</div>
</div>
</DrawerPro>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, watch, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import Handsontable from 'handsontable';
import useDrawer from '@/components/DrawerPro/useDrawer';
import { $baseMessage } from '@/utils';
import { getGetTrackPushRecords, getGetOrderOperationsById } from '@/api/TrackOrder/trackOrder';
import { postOrderFindOrderPullConfigs } from '@/api/InterfaceMappingProvider/order';
const { t } = useI18n();
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') }
]);
// Handsontable 容器和实例
const detailHotContainer = ref<HTMLElement | null>(null);
const pushRecordHotContainer = ref<HTMLElement | null>(null);
const logHotContainer = ref<HTMLElement | null>(null);
let detailHotInstance: Handsontable | null = null;
let pushRecordHotInstance: Handsontable | null = null;
let logHotInstance: Handsontable | null = null;
// 数据
const sourceTrajectoryData = ref<API.TrackOrderDetailDto[]>([]);
const pushRecordData = ref<any[]>([]);
const logData = ref<any[]>([]);
const detailSelectAll = ref(false);
const selectedDetailRows = ref<any[]>([]);
const pullConfigData = ref<any>(null);
const pullConfigLoading = ref(false);
const hasSelectedRows = computed(() => selectedDetailRows.value.length > 0);
// 轨迹抓取配置:报文 JSON 展示
const pullConfigJson = computed(() => {
const data = pullConfigData.value;
if (!data) return '';
try {
return JSON.stringify(data, null, 2);
} catch {
return String(data);
}
});
// 监听详情数据变化
watch(() => props.detailData, (newData) => {
if (newData && newData.length > 0) {
sourceTrajectoryData.value = newData.map((item: any) => ({
...item,
checked: false
}));
if (activeTab.value === 'detail') {
nextTick(() => {
initDetailHandsontable();
});
}
}
}, { immediate: true, deep: true });
// 处理服务商轨迹开关变化
function handleSourceTrajectoryChange(checked: boolean) {
emit('toggleSourceTrajectory', checked);
}
// 创建只带省略号、不带气泡(tooltip)的单元格渲染器
const createEllipsisRenderer = () => {
return function(instance: any, td: any, row: number, col: number, prop: any, value: any) {
const text = value || '--';
const textStr = String(text);
td.style.textAlign = 'left';
td.style.verticalAlign = 'middle';
td.style.overflow = 'hidden';
td.style.textOverflow = 'ellipsis';
td.style.whiteSpace = 'nowrap';
td.style.padding = '7px 16px';
td.textContent = textStr;
};
};
// 初始化明细列表 Handsontable
const initDetailHandsontable = () => {
if (!detailHotContainer.value) {
return;
}
if (detailHotInstance) {
detailHotInstance.destroy();
detailHotInstance = null;
}
const ellipsisRenderer = createEllipsisRenderer();
const columns = [
{
data: 0,
type: 'checkbox',
width: 50,
className: 'htLeft',
},
{
data: 1,
type: 'text',
readOnly: true,
width: 60,
className: 'htLeft',
renderer: ellipsisRenderer,
},
{
data: 2,
type: 'text',
readOnly: true,
width: 120,
className: 'htLeft',
renderer: ellipsisRenderer,
},
{
data: 3,
type: 'text',
readOnly: true,
width: 120,
className: 'htLeft',
renderer: ellipsisRenderer,
},
{
data: 4,
type: 'text',
readOnly: true,
width: 200,
className: 'htLeft',
renderer: ellipsisRenderer,
},
{
data: 5,
type: 'text',
readOnly: true,
width: 180,
className: 'htLeft',
renderer: ellipsisRenderer,
},
{
data: 6,
type: 'text',
readOnly: true,
width: 250,
className: 'htLeft',
renderer: ellipsisRenderer,
},
{
data: 7,
type: 'text',
readOnly: true,
width: 250,
className: 'htLeft',
renderer: ellipsisRenderer,
},
{
data: 8,
type: 'text',
readOnly: true,
width: 120,
className: 'htLeft',
renderer: ellipsisRenderer,
},
{
data: 9,
type: 'text',
readOnly: true,
width: 120,
className: 'htCenter',
renderer: function(instance: any, td: any, row: number, col: number, prop: any, value: any) {
td.style.textAlign = 'center';
td.style.verticalAlign = 'middle';
const isVisibility = value;
td.textContent = isVisibility ? t('track.order.common.yes') : t('track.order.common.no');
},
},
{
data: 10,
type: 'text',
readOnly: true,
width: 150,
className: 'htLeft',
renderer: ellipsisRenderer,
},
{
data: 11,
type: 'text',
readOnly: true,
width: 120,
className: 'htLeft',
renderer: ellipsisRenderer,
},
{
data: 12,
type: 'text',
readOnly: true,
width: 120,
className: 'htLeft',
renderer: function(instance: any, td: any, row: number, col: number, prop: any, value: any) {
const sourceText = value === 1 ? t('track.order.detail.trajectoryType.external')
: value === 2 ? t('track.order.detail.trajectoryType.internal')
: value === 3 ? t('track.order.detail.trajectoryType.manual')
: '--';
td.textContent = sourceText;
td.style.textAlign = 'left';
td.style.verticalAlign = 'middle';
},
},
];
const colHeaders = [
'',
t('track.order.table.serialNo'),
t('track.order.detail.title.trackTime'),
t('track.order.detail.title.trackTimeZone'),
t('track.order.detail.title.trackPlace'),
t('track.order.detail.title.trajectoryContent'),
t('track.order.detail.title.trajectoryContentEn'),
t('track.order.detail.title.originTrackContent'),
t('track.order.detail.title.pushDetail'),
t('track.order.detail.title.isVisibility'),
t('track.order.table.createTime'),
t('track.order.detail.title.createUser'),
t('track.order.detail.title.trajectoryType'),
];
const data = sourceTrajectoryData.value.map((row, index) => [
(row as any).checked || false,
index + 1,
row.trackTime || '--',
row.trackTimeZoneValue !== undefined && row.trackTimeZoneValue !== null ? String(row.trackTimeZoneValue) : '--',
row.trackPlace || '--',
row.content || '--',
row.enContent || '--',
row.originTrackContent || '--',
(row as any).pushContent || '--',
row.isVisibility !== undefined && row.isVisibility !== null ? row.isVisibility : false,
row.createTime || '--',
row.createUser || '--',
row.source ?? '--',
]);
try {
detailHotInstance = new Handsontable(detailHotContainer.value, {
data: data,
columns: columns,
colHeaders: colHeaders,
rowHeaders: false,
width: '100%',
height: 1000,
licenseKey: 'non-commercial-and-evaluation',
className: 'ht-theme-classic',
language: 'zh-CN',
readOnly: false,
contextMenu: false,
manualColumnResize: true,
stretchH: 'all',
afterChange: function(changes: any, source: string) {
if (source === 'edit' && changes) {
changes.forEach(([row, col, oldValue, newValue]: any[]) => {
if (col === 0) {
const dataRow = sourceTrajectoryData.value[row];
if (dataRow) {
(dataRow as any).checked = newValue || false;
handleDetailRowSelect(dataRow);
}
}
});
}
},
});
} catch (error) {
console.error('初始化明细列表 Handsontable 失败:', error);
}
};
// 更新明细 Handsontable
const updateDetailHandsontableData = () => {
if (!detailHotInstance || !sourceTrajectoryData.value.length) {
return;
}
const data = sourceTrajectoryData.value.map((row, index) => [
(row as any).checked || false,
index + 1,
row.trackTime || '--',
row.trackTimeZoneValue !== undefined && row.trackTimeZoneValue !== null ? String(row.trackTimeZoneValue) : '--',
row.trackPlace || '--',
row.content || '--',
row.enContent || '--',
row.originTrackContent || '--',
(row as any).pushContent || '--',
row.isVisibility !== undefined && row.isVisibility !== null ? row.isVisibility : false,
row.createTime || '--',
row.createUser || '--',
row.source ?? '--',
]);
detailHotInstance.loadData(data);
detailHotInstance.render();
};
// 初始化推送记录列表 Handsontable
const initPushRecordHandsontable = () => {
if (!pushRecordHotContainer.value) {
return;
}
if (pushRecordHotInstance) {
pushRecordHotInstance.destroy();
pushRecordHotInstance = null;
}
const ellipsisRenderer = createEllipsisRenderer();
const columns = [
{ data: 0, type: 'text', readOnly: true, width: 80, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 1, type: 'text', readOnly: true, width: 150, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 2, type: 'text', readOnly: true, width: 100, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 3, type: 'text', readOnly: true, width: 200, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 4, type: 'text', readOnly: true, width: 120, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 5, type: 'text', readOnly: true, width: 120, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 6, type: 'text', readOnly: true, width: 120, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 7, type: 'text', readOnly: true, width: 200, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 8, type: 'text', readOnly: true, width: 150, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 9, type: 'text', readOnly: true, width: 120, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 10, type: 'text', readOnly: true, width: 300, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 11, type: 'text', readOnly: true, width: 180, className: 'htLeft', renderer: ellipsisRenderer },
];
const colHeaders = [
t('track.order.table.serialNo'),
t('track.order.pushRecord.table.requestId'),
t('track.order.pushRecord.table.isSuccess'),
t('track.order.pushRecord.table.message'),
t('track.order.pushRecord.table.optionType'),
t('track.order.pushRecord.table.trackStatus'),
t('track.order.pushRecord.table.trackCode'),
t('track.order.pushRecord.table.trackContent'),
t('track.order.pushRecord.table.trackPlace'),
t('track.order.pushRecord.table.trackGTMTimeZone'),
t('track.order.pushRecord.table.pushContent'),
t('track.order.pushRecord.table.createTime'),
];
const data = pushRecordData.value.map((row, index) => [
index + 1,
row.requestId || '--',
row.isSuccess ? t('track.order.common.yes') : t('track.order.common.no'),
row.message || '--',
row.optionType || '--',
row.trackStatus || '--',
row.trackCode || '--',
row.trackContent || '--',
row.trackPlace || '--',
row.trackGTMTimeZone || '--',
row.pushContent || '--',
row.createTime || '--',
]);
try {
pushRecordHotInstance = new Handsontable(pushRecordHotContainer.value, {
data: data,
columns: columns,
colHeaders: colHeaders,
rowHeaders: false,
width: '100%',
height: 1000,
licenseKey: 'non-commercial-and-evaluation',
className: 'ht-theme-classic',
language: 'zh-CN',
readOnly: true,
contextMenu: false,
manualColumnResize: true,
stretchH: 'all',
});
} catch (error) {
console.error('初始化推送记录 Handsontable 失败:', error);
}
};
// 初始化日志列表 Handsontable
const initLogHandsontable = () => {
if (!logHotContainer.value) {
return;
}
if (logHotInstance) {
logHotInstance.destroy();
logHotInstance = null;
}
const ellipsisRenderer = createEllipsisRenderer();
const columns = [
{ data: 0, type: 'text', readOnly: true, width: 80, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 1, type: 'text', readOnly: true, width: 180, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 2, type: 'text', readOnly: true, width: 400, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 3, type: 'text', readOnly: true, width: 150, className: 'htLeft', renderer: ellipsisRenderer },
{ data: 4, type: 'text', readOnly: true, width: 150, className: 'htLeft', renderer: ellipsisRenderer },
];
const colHeaders = [
t('track.order.table.serialNo'),
t('track.order.log.table.createTime'),
t('track.order.log.table.content'),
t('track.order.log.table.optionType'),
t('track.order.log.table.createUserName'),
];
const data = logData.value.map((row, index) => [
index + 1,
row.createTime || '--',
row.content || '--',
row.optionType || '--',
row.createUserName || '--',
]);
try {
logHotInstance = new Handsontable(logHotContainer.value, {
data: data,
columns: columns,
colHeaders: colHeaders,
rowHeaders: false,
width: '100%',
height: 400,
licenseKey: 'non-commercial-and-evaluation',
className: 'ht-theme-classic',
language: 'zh-CN',
readOnly: true,
contextMenu: false,
manualColumnResize: true,
stretchH: 'all',
});
} catch (error) {
console.error('初始化日志 Handsontable 失败:', error);
}
};
// 加载推送记录数据
async function loadPushRecordData(orderId: number) {
try {
const response: any = await getGetTrackPushRecords({ orderId });
pushRecordData.value = response?.data || response || [];
nextTick(() => {
initPushRecordHandsontable();
});
} catch (error) {
$baseMessage('加载推送记录失败', 'error');
pushRecordData.value = [];
}
}
// 加载日志数据
async function loadLogData(orderId: number) {
try {
const response: any = await getGetOrderOperationsById({ orderId });
logData.value = response?.data || response || [];
nextTick(() => {
initLogHandsontable();
});
} catch (error) {
$baseMessage('加载日志数据失败', 'error');
logData.value = [];
}
}
// 加载轨迹抓取配置
async function loadPullConfigData() {
if (!props.appKey || !props.orderNumber) {
pullConfigData.value = null;
return;
}
pullConfigLoading.value = true;
pullConfigData.value = null;
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
});
pullConfigData.value = response ?? null;
} catch (error) {
$baseMessage('加载轨迹抓取配置失败', 'error');
pullConfigData.value = null;
} finally {
pullConfigLoading.value = false;
}
}
// 处理 Tab 切换
function handleTabChange(tabKey: string) {
if (!props.orderId && tabKey !== 'pullConfig') return;
switch (tabKey) {
case 'detail':
nextTick(() => {
initDetailHandsontable();
});
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 handleDetailRowSelect(row: any) {
selectedDetailRows.value = sourceTrajectoryData.value.filter((r: any) => r.checked);
emit('rowSelect', row);
}
// 处理取消
function handleCancel() {
emit('cancel');
}
// 暴露方法
defineExpose({
openDrawer: () => {
// 每次打开详情时,默认切回“明细”Tab
activeTab.value = 'detail';
innerOpenDrawer();
},
setDrawerProps,
closeDrawer,
updateDetailData: () => {
if (activeTab.value === 'detail') {
updateDetailHandsontableData();
}
},
getSelectedRows: () => selectedDetailRows.value
});
// 清理
onUnmounted(() => {
if (detailHotInstance) {
detailHotInstance.destroy();
detailHotInstance = null;
}
if (pushRecordHotInstance) {
pushRecordHotInstance.destroy();
pushRecordHotInstance = null;
}
if (logHotInstance) {
logHotInstance.destroy();
logHotInstance = null;
}
});
</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 {
height: 100%;
display: flex;
flex-direction: column;
}
.pull-config-content {
padding: 16px 0;
min-height: 200px;
.pull-config-json {
margin: 0;
padding: 12px;
background: #f7f8fa;
border-radius: 4px;
font-size: 12px;
line-height: 1.5;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
}
}
}
.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%;
}
}
}
</style>