front/src/views/track/trackWarningOrder/TimeWaring.vue
2026-02-06 12:01:39 +08:00

674 lines
20 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.

<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { useRoute } from 'vue-router';
import dayjs from 'dayjs';
import { $baseMessage } from '@/utils';
import { useRequest } from 'vue-hooks-plus';
import useModal from '@/components/ModalPro/useModal';
import { trackOrderGetOrderTypeDictGet } from '@/api/interface-trackOrder/trackOrder';
import { postGetAvailableContent } from '@/api/TrackContent/trackContent';
import { postGetDetails } from '@/api/TrackOrder/trackOrder';
import { postWarningTransportGetPageTransportWarningDetails } from '@/api/WarningServices/transportWarning';
import { postTrackDelete } from '@/api/InterfaceMappingProvider/track';
import { postTrackRefresh } from '@/api/InterfaceMappingProvider/trackPull';
import { useUserStore } from '@/store';
import TimeAxis from '../trackOrder/components/TimeAxis.vue';
import TimeZoneConversion from '../trackOrder/components/TimeZoneConversion.vue';
import { useI18n } from 'vue-i18n';
import { eventBus } from '@/utils/pub';
import HandsontablePro from '@/components/HandsontablePro/index.vue';
import TablePro from '@/components/TablePro/index.vue';
import FormPro from '@/components/FormPro/index.vue';
import ModalPro from '@/components/ModalPro/index.vue';
import TableButtons from '@/components/TableButtons/index.vue';
import { useContentHeight } from '@/composables/table/useContentHeight';
const { t } = useI18n();
const userStore = useUserStore();
const handsontableRef = ref<InstanceType<typeof HandsontablePro>>();
// 处理标签页刷新事件
const handlePageRefresh = (data: any) => {
if (data?.routeName === 'TrackWarningOrder') {
// 调用页面数据加载方法重新从后端获取数据
onFormSubmit();
}
};
// 在组件初始化时立即监听事件,确保即使组件被重新挂载也能收到事件
eventBus.on('page:refresh', handlePageRefresh);
const createTimeSelectStart = ref();
const optionFormRef = ref();
const route = useRoute();
const formRef = ref();
const tableRef = ref();
const isSourceTrajectory = ref(false);
const rowSourceTrajectory = ref<any>({});
const sourceTrajectoryData = ref<any[]>([]);
const sourceTrajectoryDataTemporaryStorage = ref<any[]>([]);
const notSourceTrajectoryDataTemporaryStorage = ref<any[]>([]);
const isRefresh = ref(false);
const optionModalType = ref('');
/** 定义时间轴组件 */
const timeAxisRef = ref<typeof TimeAxis>();
// 预警状态 (根据key展示对应名称,即将超时需要标红) 1:即将超时 2:已超时 3:已完成 4:超时完成 5:已关闭,初始为【即将超时】
const statusData = [
{ enumkey: 1, disp: '即将超时' },
{ enumkey: 2, disp: '已超时' },
{ enumkey: 3, disp: '已完成' },
{ enumkey: 4, disp: '超时完成' },
{ enumkey: 5, disp: '已关闭' },
];
const { contentHeight, calcContentHeight } = useContentHeight(
tableRef,
true,
24
);
const formTopData = reactive<any>({});
const formModelOption = reactive<any>({});
const [registerOption, optionMethods] = useModal() as any;
// 表单配置(临时空配置,如果需要可以后续补充)
const optionFormPro = computed(() => ({
itemList: [],
layout: 'vertical',
}));
type OrderType = {
name: number;
label: string;
};
const orderTypesData = ref<OrderType[]>([]);
// 获取订单类型数据
async function getOrderTypes() {
const orderTypeResponse = await trackOrderGetOrderTypeDictGet();
if (orderTypeResponse?.data) {
orderTypesData.value = Object.entries(orderTypeResponse.data).map(
([key, value]) => ({
name: Number(key),
label: value as string,
})
);
}
}
const orderDetails = useRequest(async (params: any) => {
const response = await postGetDetails(params);
return response?.data || response || [];
}, {
manual: true,
onSuccess(res: any[]) {
sourceTrajectoryDataTemporaryStorage.value = res.filter((item: any) => {
return item.code && item.code !== '--';
});
notSourceTrajectoryDataTemporaryStorage.value = res;
sourceTrajectoryData.value = sourceTrajectoryDataTemporaryStorage.value;
},
});
type FormData = {
subSystemId?: number;
orderNos?: string;
notifyUserId?: number;
createTimeStart?: string;
createTimeEnd?: string;
customId?: number;
warningStatus?: number;
warningName?: string;
createTime?: [string, string];
};
// 自定义表格相关
const tableData = ref<any[]>([]);
const totalCount = ref(0);
const currentPage = ref(1);
const pageSize = ref(300);
const selectedRows = ref<any[]>([]);
// 搜索参数
const searchParams = reactive({
orderNos: '',
orderType: '',
status: '',
customer: '',
latestTrackContent: '',
latestTrackTime: '',
latestTrackTimeZone: '',
latestTrackPlace: '',
notifyUserNames: '',
startTrackStatus: '',
startTrackContent: '',
startTrackTime: '',
startTrackTimeZone: '',
endTrackStatus: '',
endTrackContent: '',
endTrackTime: '',
endTrackTimeZone: '',
warningName: '',
cumulativeTimeSpan: '',
createTime: '',
});
// 加载表格数据
async function loadTableData() {
try {
const params: any = {
...searchParams,
orderNos: searchParams.orderNos ? searchParams.orderNos.split(',').filter(Boolean) : [],
orderType: searchParams.orderType ? Number(searchParams.orderType) : undefined,
status: searchParams.status ? Number(searchParams.status) : undefined,
pageIndex: currentPage.value,
pageSize: pageSize.value,
appKey: userStore.appKey,
};
const response: any = await postWarningTransportGetPageTransportWarningDetails(params);
tableData.value = response.data?.items || response.data || [];
totalCount.value = response.data?.totalCount || response.data?.length || 0;
} catch (error) {
$baseMessage('加载数据失败', 'error');
}
}
// 表头搜索
function handleHeaderSearch() {
currentPage.value = 1;
loadTableData();
}
// 表头失焦
function handleHeaderBlur() {
handleHeaderSearch();
}
// 分页改变
function handlePageChange(page: number) {
currentPage.value = page;
loadTableData();
}
// 每页数量改变
function handlePageSizeChange(size: number) {
pageSize.value = size;
currentPage.value = 1;
loadTableData();
}
// 跳转页码
const jumpPage = ref(1);
function handleJumpPage() {
const page = Number(jumpPage.value);
if (page > 0 && page <= Math.ceil(totalCount.value / pageSize.value)) {
currentPage.value = page;
loadTableData();
} else {
$baseMessage('请输入有效页码', 'warning');
}
}
// 行选择
function handleRowSelect(row: any) {
row.checked = !row.checked;
if (row.checked) {
selectedRows.value.push(row);
} else {
selectedRows.value = selectedRows.value.filter((item) => item.esId !== row.esId);
}
}
// 全选
const selectAll = ref(false);
function handleSelectAll() {
selectAll.value = !selectAll.value;
tableData.value.forEach((row) => {
row.checked = selectAll.value;
});
selectedRows.value = selectAll.value ? [...tableData.value] : [];
}
// 表格操作按钮点击
function onTableOptionClick(type: string) {
if (type === 'add') {
optionModalType.value = 'add';
(optionMethods as any).openModal();
} else if (type === 'refresh') {
isRefresh.value = true;
// 刷新逻辑
setTimeout(() => {
isRefresh.value = false;
loadTableData();
}, 1000);
}
}
// 表单提交
function onFormSubmit() {
currentPage.value = 1;
loadTableData();
}
// 弹窗确认
function onOptionSubmit() {
// 处理弹窗确认逻辑
(optionMethods as any).closeModal();
loadTableData();
}
// 弹窗取消
function onOptionCancel() {
(optionMethods as any).closeModal();
}
// 表格配置
const tableProps = computed(() => {
return {
useForm: false,
autoHeight: false,
columns: [
{
title: '订单号',
dataIndex: 'orderNo',
width: 200,
},
{
title: '状态',
dataIndex: 'status',
width: 100,
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
},
],
data: tableData.value,
pagination: {
current: currentPage.value,
pageSize: pageSize.value,
total: totalCount.value,
showTotal: true,
showSizeChanger: true,
showQuickJumper: true,
onChange: handlePageChange,
onShowSizeChange: handlePageSizeChange,
},
};
});
// 列定义
const columns = computed(() => [
{ key: 'orderNo', label: '订单号', width: 200, searchable: true, searchField: 'orderNos', multiValue: true },
{ key: 'orderTypeName', label: '订单类型', width: 120, searchable: true, searchField: 'orderType' },
{ key: 'status', label: '预警状态', width: 100, searchable: true, searchField: 'status' },
{ key: 'customer', label: '客户简称', width: 150, searchable: true, searchField: 'customer' },
{ key: 'latestTrackContent', label: '最新轨迹内容', width: 200, searchable: true, searchField: 'latestTrackContent' },
{ key: 'latestTrackTime', label: '最新轨迹时间', width: 180, searchable: true, searchField: 'latestTrackTime' },
{ key: 'latestTrackTimeZone', label: '最新轨迹时区', width: 150, searchable: true, searchField: 'latestTrackTimeZone' },
{ key: 'latestTrackPlace', label: '最新轨迹发生地', width: 180, searchable: true, searchField: 'latestTrackPlace' },
{ key: 'notifyUserNames', label: '通知对象', width: 150, searchable: true, searchField: 'notifyUserNames' },
{ key: 'startTrackStatus', label: '起始轨迹状态', width: 150, searchable: true, searchField: 'startTrackStatus' },
{ key: 'startTrackContent', label: '起始轨迹内容', width: 200, searchable: true, searchField: 'startTrackContent' },
{ key: 'startTrackTime', label: '起始轨迹时间', width: 180, searchable: true, searchField: 'startTrackTime' },
{ key: 'startTrackTimeZone', label: '起始轨迹时区', width: 150, searchable: true, searchField: 'startTrackTimeZone' },
{ key: 'endTrackStatus', label: '截止轨迹状态', width: 150, searchable: true, searchField: 'endTrackStatus' },
{ key: 'endTrackContent', label: '截止轨迹内容', width: 200, searchable: true, searchField: 'endTrackContent' },
{ key: 'endTrackTime', label: '截止轨迹时间', width: 180, searchable: true, searchField: 'endTrackTime' },
{ key: 'endTrackTimeZone', label: '截止轨迹时区', width: 150, searchable: true, searchField: 'endTrackTimeZone' },
{ key: 'warningName', label: '预警名称', width: 150, searchable: true, searchField: 'warningName' },
{ key: 'cumulativeTimeSpan', label: '累计运输时间', width: 150, searchable: true, searchField: 'cumulativeTimeSpan' },
{ key: 'createTime', label: '预警创建时间', width: 180, searchable: true, searchField: 'createTime' },
]);
// 处理数据映射(不需要转换状态,因为 customCellRenderer 会处理)
const processedTableData = computed(() => {
return tableData.value;
});
// 自定义单元格渲染器
function customCellRenderer(row: number, col: number, value: any, rowData: any) {
// 预警状态列(第3个数据列,加上操作列和序号列,所以是 col === 4)
// 注意:rowData 是原始数据,status 还是数字
if (col === 4) {
const status = (tableData.value[row] as any)?.status;
const statusText = statusData.find((item) => item.enumkey === status)?.disp || '--';
return (status === 1 || status === 2) ? `<span style="color: #f53f3f;">${statusText}</span>` : statusText;
}
return null;
}
// 行选择变化处理
function handleSelectionChange(selectedRowsList: any[]) {
selectedRows.value = selectedRowsList;
}
onMounted(() => {
getOrderTypes();
formTopData.createTime = [
dayjs().startOf('day').subtract(6, 'day').format('YYYY-MM-DD HH:mm:ss'),
dayjs()
.startOf('day')
.add(1, 'day')
.subtract(1, 'second')
.format('YYYY-MM-DD HH:mm:ss'),
];
onFormSubmit();
});
onUnmounted(() => {
// 移除事件监听
eventBus.off('page:refresh', handlePageRefresh);
});
</script>
<template>
<div class="xxx-container" style="background-color: #fff;">
<!-- 表格操作按钮和分页控制 -->
<div class="table-toolbar">
<div class="toolbar-left">
<div class="custom-buttons">
<a-button
class="btn-add"
:disabled="!selectedRows.length"
@click="onTableOptionClick('add')"
>
新增轨迹
</a-button>
<a-button
class="btn-refresh"
:disabled="isRefresh || !selectedRows.length"
@click="onTableOptionClick('refresh')"
>
刷新轨迹
</a-button>
</div>
</div>
<div class="toolbar-right">
<div class="pagination-controls">
<span class="total-records">共 {{ totalCount }} 条记录</span>
<a-button
type="text"
size="small"
:disabled="currentPage === 1"
@click="handlePageChange(currentPage - 1)"
>
上一页
</a-button>
<span class="current-page">{{ currentPage }}</span>
<a-button
type="text"
size="small"
:disabled="currentPage * pageSize >= totalCount"
@click="handlePageChange(currentPage + 1)"
>
下一页
</a-button>
<a-select
v-model="pageSize"
size="small"
:style="{ width: '110px' }"
@change="handlePageSizeChange"
>
<a-option :value="300">300</a-option>
<a-option :value="500">500</a-option>
<a-option :value="1000">1000</a-option>
<a-option :value="2000">2000</a-option>
</a-select>
<span class="page-size-text">条/页</span>
<span class="go-to-text">前往</span>
<a-input-number
v-model="jumpPage"
size="small"
:style="{ width: '60px' }"
@keyup.enter="handleJumpPage"
/>
<span class="page-text">页</span>
</div>
</div>
</div>
<!-- 列表区域:占满剩余高度,滚动条高度正确 -->
<div class="list-wrap">
<HandsontablePro
ref="handsontableRef"
:columns="columns"
:data="processedTableData"
:total="totalCount"
:current-page="currentPage"
:page-size="pageSize"
:search-params="searchParams"
:show-action-column="true"
:multi-value-fields="['orderNos']"
:custom-cell-renderer="customCellRenderer"
@action-click="() => {}"
@search="handleHeaderSearch"
@selection-change="handleSelectionChange"
/>
</div>
<!-- 隐藏原始表格 -->
<TablePro ref="tableRef" v-bind="tableProps" style="display: none;">
<template #status="{ row }">
<span v-if="row.status === 1 || row.status === 2" class="text-red">
{{ statusData.find((item) => item.enumkey === row.status)?.disp }}
</span>
<span v-else>
{{ statusData.find((item) => item.enumkey === row.status)?.disp }}
</span>
</template>
<template #warningName="{ row }">
<a-tooltip
class="box-item"
effect="dark"
placement="bottom"
enterable
>
<template #content>
<span>
{{ row.warningRuleDescription }}
</span>
</template>
<span class="filesbox" style="color: #009ffa">
{{ row.warningName || '--' }}
</span>
</a-tooltip>
</template>
<template #toolbar_buttons>
<TableButtons
:actions="[
{
label: '新增轨迹',
onClick: onTableOptionClick.bind(null, 'add'),
},
{
label: '刷新轨迹',
disabled: isRefresh || !selectedRows.length,
onClick: onTableOptionClick.bind(null, 'refresh'),
},
]"
/>
</template>
</TablePro>
<ModalPro
@register="registerOption"
@ok="onOptionSubmit"
@cancel="onOptionCancel"
>
<div
v-if="optionModalType === 'delete'"
class="mb-5px rd-4px bg-#ec46461a px-12px px-17px py-10px text-12px c-#EC4646"
>
批量无法做到精准删除,如果存在多个轨迹的代码相同,但是时间,地点不一致,会误删,请注意!
</div>
<FormPro v-bind="optionFormPro" ref="optionFormRef">
<!-- 表单内容 -->
</FormPro>
</ModalPro>
<TimeAxis ref="timeAxisRef" />
</div>
</template>
<style scoped lang="less">
/* 与左侧 tab 容器高度一致:填满 content-area(和 vertical-tabs 同高) */
.xxx-container {
padding: 16px;
background-color: #fff;
height: 100%;
min-height: 0;
box-sizing: border-box;
width: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 列表区域:占满工具栏下方剩余高度,滚动条高度与容器一致 */
.list-wrap {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.list-wrap :deep(.handsontable-pro-container) {
flex: 1;
min-height: 0;
height: 100%;
}
.list-wrap :deep(.handsontable-wrapper) {
height: 100%;
}
.table-toolbar {
flex-shrink: 0;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
.toolbar-left {
flex: 1;
.custom-buttons {
display: flex;
gap: 12px;
align-items: center;
.arco-btn {
border-radius: 20px;
font-weight: 500;
transition: all 0.3s ease;
border: 2px solid;
position: relative;
overflow: hidden;
padding: 8px 16px;
height: auto;
// 所有按钮 - 深青色
&.btn-add, &.btn-refresh {
background: var(--app-primary-color);
border-color: var(--app-primary-color);
color: #ffffff;
&:hover:not(:disabled) {
background: #2A7F7F;
border-color: #2A7F7F;
}
&:active:not(:disabled) {
background: #257272;
border-color: #257272;
}
}
// 禁用状态
&:disabled {
opacity: 1;
cursor: not-allowed;
background: rgba(169, 169, 169, 0.8) !important;
border-color: rgba(169, 169, 169, 0.8) !important;
color: #ffffff !important;
transform: none !important;
box-shadow: none !important;
}
}
}
}
.toolbar-right {
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
flex-wrap: nowrap;
.total-records,
.go-to-text,
.page-size-text,
.page-text {
font-size: 12px;
color: #4e5969;
white-space: nowrap;
}
.current-page {
font-size: 12px;
font-weight: 500;
color: #1d2129;
white-space: nowrap;
}
.arco-btn-size-small {
padding: 0 8px;
height: 24px;
min-width: 24px;
border-radius: 4px;
color: #4e5969;
border: 1px solid #e5e6eb;
background-color: #fff;
&:hover {
background-color: #f2f3f5;
}
&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
.arco-select-size-small {
.arco-select-view {
height: 24px;
line-height: 24px;
font-size: 12px;
}
}
.arco-input-number-size-small {
.arco-input-wrapper {
height: 24px;
line-height: 24px;
font-size: 12px;
}
}
}
}
}
.text-red {
color: #f53f3f;
}
</style>