Files
cattleTransportation/pc-cattle-transportation/src/views/earlywarning/warningDetailDialog.vue
2025-12-08 15:24:43 +08:00

1528 lines
56 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>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="900px"
:close-on-click-modal="false"
@close="handleClose"
>
<!-- 轨迹定位按钮 -->
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>{{ dialogTitle }}</span>
<el-button
type="primary"
:icon="Location"
@click="handleTrackClick"
:disabled="!warningData.deliveryId"
>
轨迹定位
</el-button>
</div>
</template>
<!-- 温度预警 - 只显示设备信息不显示地图 -->
<div v-if="isTemperatureWarning" class="warning-content temperature-warning">
<!-- 预警基本信息 -->
<el-descriptions title="温度预警基本信息" :column="2" border>
<el-descriptions-item label="预警时间">
<span style="font-weight: 600;">{{ warningData.warningTime }}</span>
</el-descriptions-item>
<el-descriptions-item label="预警类型">
<el-tag :type="warningData.warningType == 5 ? 'danger' : 'primary'" size="large">
{{ warningData.warningTypeDesc }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="预警温度">
<span :style="{
color: getTemperatureColor(parseFloat(warningData.deviceTemp)),
fontWeight: 'bold',
fontSize: '18px'
}">
{{ warningData.deviceTemp || '--' }}°C
</span>
</el-descriptions-item>
<el-descriptions-item label="设备ID">
{{ warningData.serverDeviceSn || warningData.deviceId || '未知' }}
</el-descriptions-item>
<el-descriptions-item label="运单号">{{ warningData.deliveryNumber }}</el-descriptions-item>
<el-descriptions-item label="车牌号">{{ warningData.licensePlate }}</el-descriptions-item>
<el-descriptions-item label="司机姓名">{{ warningData.driverName }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ warningData.createByName || '--' }}</el-descriptions-item>
</el-descriptions>
<!-- 预警详情描述 -->
<div v-if="warningData.warningDetail" class="warning-description">
<el-alert
:title="warningData.warningDetail"
:type="warningData.warningType == 5 ? 'error' : 'warning'"
:closable="false"
show-icon
style="margin-top: 20px;"
/>
</div>
<el-divider content-position="left">
<el-icon><InfoFilled /></el-icon>
<span style="margin-left: 5px;">设备详细信息</span>
</el-divider>
<!-- 绑定设备列表 -->
<div v-if="deviceList.length > 0" class="device-list-section">
<!-- 调试信息开发环境可显示 -->
<div v-if="false" style="font-size: 12px; color: #909399; margin-bottom: 10px;">
调试设备数量={{ deviceList.length }}, deliveryId={{ warningData.deliveryId }}
</div>
<div class="section-header">
<h4>
<el-icon style="vertical-align: middle;"><Connection /></el-icon>
绑定设备列表
<el-tag type="info" size="small" style="margin-left: 10px;">{{ deviceList.length }}</el-tag>
</h4>
</div>
<el-table :data="deviceList" border style="width: 100%" size="small">
<el-table-column prop="deviceId" label="设备ID" width="150" />
<el-table-column prop="deviceTypeName" label="设备类型" width="120">
<template #default="scope">
<el-tag
:type="scope.row.deviceType == 1 || scope.row.deviceType == 4 ? 'primary' : (scope.row.deviceType == 2 ? 'success' : 'warning')"
>
{{ scope.row.deviceTypeName || '未知' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sn" label="设备SN" min-width="150" />
<el-table-column prop="battery" label="电量" width="100">
<template #default="scope">
<span v-if="scope.row.battery || scope.row.batteryPercentage">
{{ scope.row.battery || scope.row.batteryPercentage }}%
</span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'info'">
{{ scope.row.status == 1 ? '在线' : '离线' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<div v-else-if="!loadingDevices" class="no-data-tip">
<el-empty description="暂无绑定设备信息" :image-size="80" />
<div style="margin-top: 10px; font-size: 12px; color: #909399;">
<p v-if="!warningData.deliveryId">提示运单ID为空无法查询设备</p>
<p v-else>提示该运单可能没有绑定设备或设备已被删除</p>
</div>
</div>
<div v-else class="no-data-tip">
<el-icon class="is-loading"><Loading /></el-icon>
<span style="margin-left: 10px;">正在加载设备列表...</span>
</div>
<!-- 设备温度日志重点显示温度数据 -->
<div v-if="deviceLogs.length > 0" class="device-logs-section">
<div class="section-header">
<h4>
<el-icon style="vertical-align: middle;"><DataLine /></el-icon>
设备温度记录
<el-tag type="info" size="small" style="margin-left: 10px;">{{ deviceLogs.length }}</el-tag>
</h4>
<p class="section-desc">显示设备的温度历史记录可以查看温度变化趋势</p>
</div>
<el-table
:data="deviceLogs"
border
style="width: 100%"
size="small"
max-height="350"
v-loading="loadingLogs"
:default-sort="{ prop: 'createTime', order: 'descending' }"
>
<el-table-column label="记录时间" width="170" sortable>
<template #default="scope">
{{ scope.row.hourTime || scope.row.createTime || '--' }}
</template>
</el-table-column>
<el-table-column prop="deviceTypeName" label="设备类型" width="110">
<template #default="scope">
<el-tag
size="small"
:type="scope.row.deviceType == 1 || scope.row.deviceType == 4 ? 'primary' : (scope.row.deviceType == 2 ? 'success' : 'warning')"
>
{{ scope.row.deviceTypeName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="deviceId" label="设备ID" width="140" />
<el-table-column label="温度°C" width="120" sortable>
<template #default="scope">
<span v-if="scope.row.deviceTemp"
:style="{
color: getTemperatureColor(parseFloat(scope.row.deviceTemp)),
fontWeight: '600',
fontSize: '14px'
}">
{{ scope.row.deviceTemp }}°C
</span>
<span v-else style="color: #909399;">--</span>
</template>
</el-table-column>
<el-table-column prop="heartRate" label="心率" width="80" />
<el-table-column label="步数" width="90">
<template #default="scope">
{{ scope.row.stepCount || scope.row.steps || '--' }}
</template>
</el-table-column>
<el-table-column prop="latitude" label="纬度" width="100" />
<el-table-column prop="longitude" label="经度" width="100" />
</el-table>
</div>
<div v-else-if="!loadingLogs" class="no-data-tip">
<el-empty description="暂无设备日志记录" :image-size="80" />
</div>
</div>
<!-- 停留预警/位置偏离预警 - 显示地图 -->
<div v-else-if="isLocationWarning" class="warning-content">
<el-descriptions title="位置预警详情" :column="2" border>
<el-descriptions-item label="预警时间">{{ warningData.warningTime }}</el-descriptions-item>
<el-descriptions-item label="预警类型">
<el-tag :type="getWarningTagType(warningData.warningType)">
{{ warningData.warningTypeDesc }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="预警经度">{{ warningData.longitude || '未知' }}</el-descriptions-item>
<el-descriptions-item label="预警纬度">{{ warningData.latitude || '未知' }}</el-descriptions-item>
<el-descriptions-item label="运单号">{{ warningData.deliveryNumber }}</el-descriptions-item>
<el-descriptions-item label="车牌号">{{ warningData.licensePlate }}</el-descriptions-item>
<el-descriptions-item label="司机姓名">{{ warningData.driverName }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ warningData.createByName }}</el-descriptions-item>
</el-descriptions>
<el-divider />
<!-- 百度地图显示 -->
<div class="map-container">
<h4>预警位置地图</h4>
<div id="warningMap" style="width: 100%; height: 400px;"></div>
</div>
<!-- 预警详情描述 -->
<div v-if="warningData.warningDetail" class="warning-description">
<h4>预警详情</h4>
<p>{{ warningData.warningDetail }}</p>
</div>
<!-- 新增绑定设备列表 -->
<div v-if="deviceList.length > 0" class="device-list-section">
<h4>绑定设备列表{{ deviceList.length }}</h4>
<el-table :data="deviceList" border style="width: 100%" size="small">
<el-table-column prop="deviceId" label="设备ID" width="150" />
<el-table-column prop="deviceTypeName" label="设备类型" width="120">
<template #default="scope">
<el-tag :type="scope.row.deviceType == 1 ? 'primary' : (scope.row.deviceType == 2 ? 'success' : 'warning')">
{{ scope.row.deviceTypeName || '未知' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sn" label="设备SN" min-width="150" />
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'info'">
{{ scope.row.status == 1 ? '在线' : '离线' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<!-- 新增设备日志列表 -->
<div v-if="deviceLogs.length > 0" class="device-logs-section">
<h4>设备日志记录{{ deviceLogs.length }}</h4>
<el-table
:data="deviceLogs"
border
style="width: 100%"
size="small"
max-height="300"
v-loading="loadingLogs"
>
<el-table-column label="时间" width="160">
<template #default="scope">
{{ scope.row.hourTime || scope.row.createTime || '--' }}
</template>
</el-table-column>
<el-table-column prop="deviceTypeName" label="设备类型" width="110">
<template #default="scope">
<el-tag size="small" :type="scope.row.deviceType == 1 ? 'primary' : (scope.row.deviceType == 2 ? 'success' : 'warning')">
{{ scope.row.deviceTypeName }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="deviceId" label="设备ID" width="130" />
<el-table-column prop="latitude" label="纬度" width="90" />
<el-table-column prop="longitude" label="经度" width="90" />
<el-table-column prop="deviceTemp" label="温度°C" width="100">
<template #default="scope">
<span v-if="scope.row.deviceTemp" :style="{ color: getTemperatureColor(parseFloat(scope.row.deviceTemp)) }">
{{ scope.row.deviceTemp }}°C
</span>
<span v-else>--</span>
</template>
</el-table-column>
<el-table-column prop="heartRate" label="心率" width="80" />
<el-table-column label="步数" width="80">
<template #default="scope">
{{ scope.row.stepCount || scope.row.steps || '--' }}
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 其他类型预警 -->
<div v-else class="warning-content">
<el-descriptions title="预警详情" :column="2" border>
<el-descriptions-item label="预警时间">{{ warningData.warningTime }}</el-descriptions-item>
<el-descriptions-item label="预警类型">
<el-tag :type="getWarningTagType(warningData.warningType)">
{{ warningData.warningTypeDesc }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="运单号">{{ warningData.deliveryNumber }}</el-descriptions-item>
<el-descriptions-item label="车牌号">{{ warningData.licensePlate }}</el-descriptions-item>
<el-descriptions-item label="司机姓名">{{ warningData.driverName }}</el-descriptions-item>
<el-descriptions-item label="创建人">{{ warningData.createByName }}</el-descriptions-item>
</el-descriptions>
<el-divider />
<div v-if="warningData.warningDetail" class="warning-description">
<h4>预警详情</h4>
<p>{{ warningData.warningDetail }}</p>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
<!-- 轨迹定位对话框 -->
<el-dialog
v-model="trackDialogVisible"
title="轨迹定位"
width="1000px"
:close-on-click-modal="false"
@close="handleTrackDialogClose"
>
<div v-loading="trackLoading" style="min-height: 500px;">
<!-- 状态提示 -->
<div v-if="deliveryStatus === 1" class="status-tip">
<el-alert
title="运单尚未开始运输"
description="当前运单状态为准备中,暂无轨迹数据"
type="info"
:closable="false"
show-icon
/>
</div>
<!-- 轨迹地图容器 -->
<div v-else>
<!-- 控制按钮 -->
<div class="track-controls" style="margin-bottom: 15px;">
<el-button
type="primary"
:icon="VideoPlay"
@click="handlePlayTrack"
:disabled="!trackMapShow || trackPath.length === 0"
>
{{ isPlaying ? '暂停' : '播放' }}
</el-button>
<el-button
:icon="Refresh"
@click="handleResetTrack"
:disabled="!trackMapShow || trackPath.length === 0"
>
重置
</el-button>
<el-tag type="info" style="margin-left: 10px;">
轨迹点数{{ trackPath.length }}
</el-tag>
<el-tag type="info" style="margin-left: 10px;">
状态{{ getDeliveryStatusText(deliveryStatus) }}
</el-tag>
</div>
<!-- 地图容器 -->
<div
id="trackMap"
style="width: 100%; height: 500px; border: 1px solid #dcdfe6; border-radius: 4px;"
></div>
<!-- 无轨迹数据提示 -->
<div v-if="!trackMapShow && !trackLoading" class="no-track-tip">
<el-empty description="暂无轨迹数据" :image-size="100" />
</div>
<div v-if="yingyanMeta.entityName" class="track-meta-panel">
<el-descriptions :column="3" border>
<el-descriptions-item label="终端名称">{{ yingyanMeta.entityName }}</el-descriptions-item>
<el-descriptions-item label="查询开始">{{ formatTimestamp(yingyanMeta.startTime) }}</el-descriptions-item>
<el-descriptions-item label="查询结束">{{ formatTimestamp(yingyanMeta.endTime) }}</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="latestPoint" class="latest-point-panel">
<h4>最新定位</h4>
<el-descriptions :column="3" border>
<el-descriptions-item label="定位时间">
{{ formatTimestamp(latestPoint.locTime) }}
</el-descriptions-item>
<el-descriptions-item label="纬度">
{{ latestPoint.lat }}
</el-descriptions-item>
<el-descriptions-item label="经度">
{{ latestPoint.lng }}
</el-descriptions-item>
<el-descriptions-item label="速度(米/秒)">
{{ latestPoint.speed ?? '--' }}
</el-descriptions-item>
<el-descriptions-item label="航向角">
{{ latestPoint.direction ?? '--' }}
</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="segmentStats && segmentStats.length > 0" class="segment-stats">
<h4>轨迹查询分段{{ segmentStats.length }}</h4>
<el-table :data="segmentStats" border size="small" max-height="220">
<el-table-column label="段次" width="80" prop="index" />
<el-table-column label="开始时间" min-width="160">
<template #default="scope">
{{ formatTimestamp(scope.row.startTime) }}
</template>
</el-table-column>
<el-table-column label="结束时间" min-width="160">
<template #default="scope">
{{ formatTimestamp(scope.row.endTime) }}
</template>
</el-table-column>
<el-table-column label="轨迹点数" width="120" prop="trackCount" />
<el-table-column label="停留点数" width="120" prop="stayCount" />
</el-table>
</div>
<div v-if="stayPoints && stayPoints.length > 0" class="staypoint-section">
<h4>停留点分析15分钟</h4>
<el-table :data="stayPoints" border style="width: 100%;" size="small">
<el-table-column label="开始时间" min-width="160">
<template #default="scope">
{{ formatTimestamp(scope.row.startTime) }}
</template>
</el-table-column>
<el-table-column label="结束时间" min-width="160">
<template #default="scope">
{{ formatTimestamp(scope.row.endTime) }}
</template>
</el-table-column>
<el-table-column label="停留时长" width="120">
<template #default="scope">
{{ formatDuration(scope.row.duration) }}
</template>
</el-table-column>
<el-table-column label="位置" min-width="160">
<template #default="scope">
{{ `${scope.row.latitude || '--'}, ${scope.row.longitude || '--'}` }}
</template>
</el-table-column>
</el-table>
</div>
<div v-else-if="trackMapShow && !trackLoading" class="no-data-tip">
暂无满足 15 分钟的停留点数据
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="trackDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, nextTick, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Location, VideoPlay, Refresh, InfoFilled, Connection, DataLine, Loading } from '@element-plus/icons-vue';
import { BMPGL } from '@/utils/loadBmap.js';
import { pageDeviceList, getCollarLogs, getEarTagLogs, getHostLogs, getYingyanTrack, waybillDetail } from '@/api/abroad.js';
const dialogVisible = ref(false);
const warningData = reactive({
id: null,
warningType: null,
warningTypeDesc: '',
warningTime: '',
latitude: '',
longitude: '',
deviceId: '',
deviceName: '',
deviceTemp: '', // 修改:使用 deviceTemp
temperature: null,
warningDetail: '',
deliveryNumber: '',
licensePlate: '',
driverName: '',
createByName: '',
deliveryId: null, // 新增运单ID
serverDeviceSn: '', // 新增主机设备SN
});
const temperatureHistory = ref([]);
const deviceList = ref([]); // 新增:设备列表
const deviceLogs = ref([]); // 新增:设备日志列表
const loadingDevices = ref(false); // 新增:加载设备列表状态
const loadingLogs = ref(false); // 新增:加载日志状态
let mapInstance = null;
let markerInstance = null;
// 轨迹定位相关
const trackDialogVisible = ref(false);
const trackLoading = ref(false);
const trackMapShow = ref(false);
const trackPath = ref([]); // 轨迹点数组
const trackMapInstance = ref(null); // 轨迹地图实例
const trackPolyline = ref(null); // 轨迹线条
const trackStartMarker = ref(null); // 起点标记
const trackEndMarker = ref(null); // 终点标记
const trackPlayMarker = ref(null); // 播放位置标记
const deliveryStatus = ref(null); // 运输状态1-准备中2-运输中3-已结束
const isPlaying = ref(false); // 是否正在播放
const playTimer = ref(null); // 播放定时器
const currentPlayIndex = ref(0); // 当前播放到的轨迹点索引
const trackBMapGL = ref(null); // 保存 BMapGL 实例,避免重复加载
const stayPoints = ref([]); // 停留点列表
const latestPoint = ref(null); // 最新轨迹点
const segmentStats = ref([]);
const yingyanMeta = reactive({
entityName: '',
startTime: null,
endTime: null
});
// 计算属性:判断预警类型
const isTemperatureWarning = computed(() => {
// 5-高温预警6-低温预警
const type = parseInt(warningData.warningType);
const isTempWarning = type === 5 || type === 6;
return isTempWarning;
});
const isLocationWarning = computed(() => {
// 4-设备停留预警7-位置偏离预警8-延误预警
const type = parseInt(warningData.warningType);
const isLocWarning = type === 4 || type === 7 || type === 8;
return isLocWarning;
});
const isStaticTrack = computed(() => {
const status = Number(warningData.status || 0);
return status >= 3;
});
const dialogTitle = computed(() => {
return `${warningData.warningTypeDesc || '预警'}详情`;
});
// 打开对话框
const open = async (row) => {
console.log('[WARNING-DETAIL] 打开预警详情对话框,原始数据:', row);
// 填充数据
Object.keys(warningData).forEach(key => {
if (row[key] !== undefined) {
warningData[key] = row[key];
}
});
console.log('[WARNING-DETAIL] 填充后的 warningData:', warningData);
console.log('[WARNING-DETAIL] deliveryId:', warningData.deliveryId);
dialogVisible.value = true;
// ✅ 查询运单绑定的设备列表
if (warningData.deliveryId) {
console.log('[WARNING-DETAIL] 开始加载设备列表deliveryId:', warningData.deliveryId);
await loadDeviceList(warningData.deliveryId);
} else {
console.warn('[WARNING-DETAIL] 警告deliveryId 为空,无法加载设备列表');
console.warn('[WARNING-DETAIL] 请检查预警详情 API 是否返回了 deliveryId 字段');
}
// 如果是位置相关预警,加载地图
if (isLocationWarning.value && warningData.latitude && warningData.longitude) {
await nextTick();
initMap();
}
// 注意:温度预警的日志已经通过 loadDeviceList 自动加载,无需单独调用
// 设备列表加载后会自动调用 loadAllDeviceLogs()
};
// ✅ 新增:加载运单绑定的设备列表
const loadDeviceList = async (deliveryId) => {
if (!deliveryId) {
console.warn('[WARNING-DETAIL] 运单ID为空无法加载设备列表');
return;
}
loadingDevices.value = true;
try {
console.log('[WARNING-DETAIL] 调用 pageDeviceList API参数:', {
deliveryId: deliveryId,
pageNum: 1,
pageSize: 100
});
const res = await pageDeviceList({
deliveryId: deliveryId,
pageNum: 1,
pageSize: 100, // 一次性加载所有设备
});
console.log('[WARNING-DETAIL] pageDeviceList API 返回结果:', res);
if (res.code === 200 && res.data) {
// ✅ 修复:后端直接返回数组,不是嵌套在 list 或 rows 中
let devices = [];
if (Array.isArray(res.data)) {
devices = res.data;
} else {
devices = res.data.list || res.data.rows || [];
}
console.log('[WARNING-DETAIL] 解析后的设备列表:', devices);
console.log('[WARNING-DETAIL] 设备数量:', devices.length);
deviceList.value = devices;
if (devices.length === 0) {
console.warn('[WARNING-DETAIL] 警告:设备列表为空');
console.warn('[WARNING-DETAIL] 可能原因:');
console.warn('[WARNING-DETAIL] 1. 该运单确实没有绑定设备');
console.warn('[WARNING-DETAIL] 2. 设备被标记为已删除is_delet=1');
console.warn('[WARNING-DETAIL] 3. 设备表中的 delivery_id 字段为空或不匹配');
ElMessage.warning('该运单暂无绑定设备');
} else {
console.log('[WARNING-DETAIL] 成功加载设备列表,设备数量:', devices.length);
// 自动加载所有设备的日志
await loadAllDeviceLogs();
}
} else {
console.error('[WARNING-DETAIL] API 返回错误:', res);
ElMessage.warning('加载设备列表失败:' + (res.msg || '未知错误'));
}
} catch (error) {
console.error('[WARNING-DETAIL] 加载设备列表异常:', error);
ElMessage.error('加载设备列表失败:' + (error.message || '网络错误'));
} finally {
loadingDevices.value = false;
}
};
// ✅ 新增:加载所有设备的日志数据
const loadAllDeviceLogs = async () => {
if (deviceList.value.length === 0) {
console.warn('[WARNING-DETAIL] 设备列表为空,无法加载日志');
return;
}
loadingLogs.value = true;
deviceLogs.value = []; // 清空之前的日志
try {
// 并行加载所有设备的日志
const logPromises = deviceList.value.map(device => {
return loadDeviceLogs(device.deviceId || device.sn, device.deviceType, warningData.deliveryId);
});
await Promise.all(logPromises);
} catch (error) {
console.error('[WARNING-DETAIL] 加载设备日志失败:', error);
ElMessage.error('加载设备日志失败');
} finally {
loadingLogs.value = false;
}
};
// ✅ 新增:加载单个设备的日志数据
const loadDeviceLogs = async (deviceId, deviceType, deliveryId) => {
if (!deviceId) {
console.warn('[WARNING-DETAIL] 设备ID为空无法加载日志');
return;
}
if (!deliveryId) {
console.warn('[WARNING-DETAIL] 运单ID为空无法加载日志');
return;
}
// 确保 deviceType 是数字
const typeNum = parseInt(deviceType);
try {
// 根据设备类型选择不同的API
let apiFunc;
let deviceTypeName;
switch (typeNum) {
case 1: // 智能主机
apiFunc = getHostLogs;
deviceTypeName = '智能主机';
break;
case 2: // 智能耳标
apiFunc = getEarTagLogs;
deviceTypeName = '智能耳标';
break;
case 3: // 智能项圈
case 4: // 也可能是4
apiFunc = getCollarLogs;
deviceTypeName = '智能项圈';
break;
default:
console.warn(`[WARNING-DETAIL] 未知的设备类型: ${typeNum} (原始值: ${deviceType})`);
return;
}
// 调用对应的日志查询API必须传入 deliveryId
const res = await apiFunc({
deviceId: deviceId,
deliveryId: deliveryId, // ✅ 新增:后端必需参数
pageNum: 1,
pageSize: 50, // 查询最近50条日志
// 可选添加时间范围过滤预警时间前后1小时
// startTime: getStartTime(warningData.warningTime),
// endTime: getEndTime(warningData.warningTime),
});
if (res.code === 200 && res.data) {
// ✅ 修复:后端可能直接返回数组,也可能嵌套在 list/rows 中
let logs = [];
if (Array.isArray(res.data)) {
logs = res.data;
} else {
logs = res.data.list || res.data.rows || [];
}
console.log('[WARNING-DETAIL] 原始日志数据:', logs);
// 为每条日志添加设备信息
const logsWithDeviceInfo = logs.map(log => ({
...log,
deviceId: deviceId,
deviceType: typeNum, // 使用转换后的数字类型
deviceTypeName: deviceTypeName,
}));
deviceLogs.value.push(...logsWithDeviceInfo);
} else {
console.warn('[WARNING-DETAIL] 加载' + deviceTypeName + '日志失败:', res.msg);
}
} catch (error) {
console.error('[WARNING-DETAIL] 加载设备(' + deviceId + ')日志失败:', error);
}
};
// 初始化地图
const initMap = async () => {
try {
// 使用百度地图 API Key
const BMapGL = await BMPGL('3AN3VahoqaXUs32U8luXD2Dwn86KK5B7');
const lat = parseFloat(warningData.latitude);
const lon = parseFloat(warningData.longitude);
if (isNaN(lat) || isNaN(lon)) {
ElMessage.warning('经纬度数据无效');
return;
}
// 创建地图实例(使用 BMapGL
mapInstance = new BMapGL.Map('warningMap');
const point = new BMapGL.Point(lon, lat);
mapInstance.centerAndZoom(point, 15);
mapInstance.enableScrollWheelZoom(true);
// 添加标注
markerInstance = new BMapGL.Marker(point);
mapInstance.addOverlay(markerInstance);
// 添加信息窗口
const warningTypeText = warningData.warningTypeDesc || '预警位置';
const infoWindow = new BMapGL.InfoWindow(
'<div style="padding: 10px;">' +
'<p style="margin: 0; font-weight: bold; color: #f56c6c;">' + warningTypeText + '</p>' +
'<p style="margin: 5px 0 0 0;">时间: ' + warningData.warningTime + '</p>' +
'<p style="margin: 5px 0 0 0;">经度: ' + lon + '</p>' +
'<p style="margin: 5px 0 0 0;">纬度: ' + lat + '</p>' +
'</div>',
{ width: 250, height: 120 }
);
markerInstance.addEventListener('click', function () {
mapInstance.openInfoWindow(infoWindow, point);
});
// 默认打开信息窗口
mapInstance.openInfoWindow(infoWindow, point);
} catch (error) {
console.error('[WARNING-DETAIL] 地图初始化失败:', error);
ElMessage.error('地图加载失败');
}
};
// 根据温度值返回颜色
const getTemperatureColor = (temp) => {
if (temp == null) return '#909399';
if (temp >= 35) return '#f56c6c'; // 高温-红色
if (temp <= 5) return '#409eff'; // 低温-蓝色
return '#67c23a'; // 正常-绿色
};
// 根据预警类型返回标签类型
const getWarningTagType = (type) => {
const typeNum = parseInt(type);
switch (typeNum) {
case 2: return 'danger'; // 数量盘单预警
case 3: return 'warning'; // 运输距离预警
case 4: return 'info'; // 设备停留预警
case 5: return 'danger'; // 高温预警
case 6: return 'info'; // 低温预警
case 7: return 'warning'; // 位置偏离预警
case 8: return 'danger'; // 延误预警
case 9: return 'success'; // 超前到达预警
default: return 'info';
}
};
const normalizeTimestamp = (value) => {
if (value === null || value === undefined || value === '') {
return null;
}
const num = Number(value);
if (Number.isNaN(num)) {
return null;
}
return num < 1e12 ? num * 1000 : num;
};
const formatTimestamp = (value) => {
const ms = normalizeTimestamp(value);
if (!ms) return '--';
const date = new Date(ms);
if (Number.isNaN(date.getTime())) {
return '--';
}
const pad = (num) => `${num}`.padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
const formatDuration = (seconds) => {
if (seconds === null || seconds === undefined) {
return '--';
}
const total = Number(seconds);
if (Number.isNaN(total) || total <= 0) {
return '--';
}
const hours = Math.floor(total / 3600);
const minutes = Math.floor((total % 3600) / 60);
const sec = Math.floor(total % 60);
const parts = [];
if (hours > 0) {
parts.push(`${hours}小时`);
}
if (minutes > 0) {
parts.push(`${minutes}分钟`);
}
if (sec > 0 && hours === 0) {
parts.push(`${sec}`);
}
return parts.join('') || `${sec}`;
};
function parseLatestPoint(point) {
if (!point) {
return null;
}
const lng = parseFloat(point.longitude ?? point.lng ?? 0);
const lat = parseFloat(point.latitude ?? point.lat ?? 0);
if (Number.isNaN(lng) || Number.isNaN(lat) || (lng === 0 && lat === 0)) {
return null;
}
return {
lng,
lat,
locTime: point.locTime ?? point.loc_time ?? null,
speed: point.speed ?? null,
direction: point.direction ?? null,
};
}
// 关闭对话框
const handleClose = () => {
// 清理地图实例
if (mapInstance) {
mapInstance.clearOverlays();
mapInstance = null;
markerInstance = null;
}
// 清空温度历史数据
temperatureHistory.value = [];
// ✅ 清空设备列表和日志数据
deviceList.value = [];
deviceLogs.value = [];
loadingDevices.value = false;
loadingLogs.value = false;
// 重置数据
Object.keys(warningData).forEach(key => {
if (typeof warningData[key] === 'number') {
warningData[key] = null;
} else {
warningData[key] = '';
}
});
};
// 轨迹定位按钮点击
const handleTrackClick = async () => {
if (!warningData.deliveryId) {
ElMessage.warning('运单ID不存在无法查看轨迹');
return;
}
console.info('[TRACK] 开始轨迹定位流程', {
deliveryId: warningData.deliveryId,
status: warningData.status,
estimatedDepartureTime: warningData.estimatedDepartureTime,
});
trackDialogVisible.value = true;
trackLoading.value = true;
trackMapShow.value = false;
trackPath.value = [];
latestPoint.value = null;
isPlaying.value = false;
currentPlayIndex.value = 0;
// 获取运单运输状态
await getDeliveryStatus();
// 如果状态为准备中,直接返回
if (deliveryStatus.value === 1) {
trackLoading.value = false;
return;
}
await loadYingyanTrack();
if (trackPath.value.length === 0) {
trackLoading.value = false;
return;
}
// 初始化地图
await nextTick();
await initTrackMap();
};
// 获取运送清单运输状态
const getDeliveryStatus = async () => {
// 先检查 warningData 中是否包含 status
if (warningData.status !== undefined && warningData.status !== null) {
// 注意:这里的 status 可能是业务状态1-7需要判断是否是运输状态1-3
// 如果 status 在 1-3 范围内,可能是运输状态
const status = parseInt(warningData.status);
if (status >= 1 && status <= 3) {
deliveryStatus.value = status;
return;
}
}
// 如果没有,通过 API 获取运单详情
try {
const res = await waybillDetail(warningData.deliveryId);
if (res.code === 200 && res.data) {
const delivery = res.data.delivery || res.data;
// 注意Delivery 实体中的 status 是业务状态1-7不是运输状态
// 需要查看是否有专门的运输状态字段,或者根据业务状态推断
// 这里先假设 status 字段就是运输状态,如果不对需要调整
deliveryStatus.value = delivery.status || null;
}
} catch (error) {
console.error('[TRACK] 获取运单状态失败:', error);
// 默认设置为运输中,允许查看轨迹
deliveryStatus.value = 2;
}
};
// 加载百度鹰眼轨迹与停留点
const loadYingyanTrack = async () => {
stayPoints.value = [];
trackPath.value = [];
latestPoint.value = null;
segmentStats.value = [];
trackMapShow.value = false;
yingyanMeta.entityName = '';
yingyanMeta.startTime = null;
yingyanMeta.endTime = null;
if (!warningData.deliveryId) {
ElMessage.warning('运单ID缺失无法查询轨迹');
return;
}
try {
const res = await getYingyanTrack({ deliveryId: warningData.deliveryId });
console.info('[TRACK] 后端轨迹接口响应', res);
if (res.code === 200 && res.data) {
segmentStats.value = Array.isArray(res.data.segmentStats) ? res.data.segmentStats : [];
console.info('[TRACK] 分段统计', segmentStats.value);
const rawPoints = Array.isArray(res.data.trackPoints) ? res.data.trackPoints : [];
trackPath.value = rawPoints
.map(item => {
const lng = parseFloat(item.longitude ?? item.lng ?? 0);
const lat = parseFloat(item.latitude ?? item.lat ?? 0);
if (Number.isNaN(lng) || Number.isNaN(lat) || lng === 0 || lat === 0) {
return null;
}
return {
lng,
lat,
locTime: item.locTime
};
})
.filter(Boolean);
stayPoints.value = Array.isArray(res.data.stayPoints) ? res.data.stayPoints : [];
yingyanMeta.entityName = res.data.entityName || '';
yingyanMeta.startTime = res.data.startTime || null;
yingyanMeta.endTime = res.data.endTime || null;
latestPoint.value = parseLatestPoint(res.data.latestPoint);
console.info('[TRACK] 轨迹点数量', trackPath.value.length);
if (trackPath.value.length > 0) {
trackMapShow.value = true;
} else if (latestPoint.value) {
trackPath.value.push({
lng: latestPoint.value.lng,
lat: latestPoint.value.lat,
locTime: latestPoint.value.locTime || null,
});
trackMapShow.value = true;
} else {
ElMessage.warning('暂无有效轨迹点');
}
} else {
ElMessage.warning(res.msg || '暂无轨迹数据');
}
} catch (error) {
console.error('[TRACK] 加载百度鹰眼轨迹失败:', error);
ElMessage.error('加载轨迹数据失败');
}
};
// 初始化轨迹地图
const initTrackMap = async () => {
if (trackPath.value.length === 0) {
return;
}
try {
const BMapGL = await BMPGL('3AN3VahoqaXUs32U8luXD2Dwn86KK5B7');
trackBMapGL.value = BMapGL; // 保存 BMapGL 实例
// 创建地图实例
trackMapInstance.value = new BMapGL.Map('trackMap');
// 计算地图中心点和缩放级别
const bounds = calculateBounds(trackPath.value);
const centerPoint = new BMapGL.Point(bounds.center.lng, bounds.center.lat);
trackMapInstance.value.centerAndZoom(centerPoint, bounds.zoom);
trackMapInstance.value.enableScrollWheelZoom(true);
// 根据状态绘制轨迹
if (deliveryStatus.value === 3) {
// 已结束:静态轨迹
drawStaticTrack(BMapGL);
} else if (deliveryStatus.value === 2) {
// 运输中:动态轨迹(先绘制已有部分)
drawDynamicTrack(BMapGL);
}
trackLoading.value = false;
} catch (error) {
console.error('[TRACK] 地图初始化失败:', error);
ElMessage.error('地图加载失败');
trackLoading.value = false;
}
};
// 计算轨迹边界
const calculateBounds = (points) => {
if (points.length === 0) {
return { center: { lng: 116.404, lat: 39.915 }, zoom: 15 };
}
let minLng = points[0].lng;
let maxLng = points[0].lng;
let minLat = points[0].lat;
let maxLat = points[0].lat;
points.forEach(point => {
minLng = Math.min(minLng, point.lng);
maxLng = Math.max(maxLng, point.lng);
minLat = Math.min(minLat, point.lat);
maxLat = Math.max(maxLat, point.lat);
});
const center = {
lng: (minLng + maxLng) / 2,
lat: (minLat + maxLat) / 2
};
// 计算缩放级别(简单估算)
const lngDiff = maxLng - minLng;
const latDiff = maxLat - minLat;
const maxDiff = Math.max(lngDiff, latDiff);
let zoom = 15;
if (maxDiff > 0.1) zoom = 10;
else if (maxDiff > 0.05) zoom = 11;
else if (maxDiff > 0.02) zoom = 12;
else if (maxDiff > 0.01) zoom = 13;
else if (maxDiff > 0.005) zoom = 14;
return { center, zoom };
};
// 绘制静态轨迹(已结束)
const drawStaticTrack = (BMapGL) => {
if (!trackMapInstance.value || trackPath.value.length === 0) {
return;
}
// 清除之前的覆盖物
trackMapInstance.value.clearOverlays();
// 绘制轨迹线
const polyline = new BMapGL.Polyline(
trackPath.value.map(p => new BMapGL.Point(p.lng, p.lat)),
{
strokeColor: '#3388ff',
strokeWeight: 4,
strokeOpacity: 0.8
}
);
trackMapInstance.value.addOverlay(polyline);
trackPolyline.value = polyline;
// 添加起点标记
if (trackPath.value.length > 0) {
const startPoint = new BMapGL.Point(trackPath.value[0].lng, trackPath.value[0].lat);
// 使用简单的圆形标记作为起点(绿色)
const startMarker = new BMapGL.Marker(startPoint, {
icon: new BMapGL.Icon(
'https://api.map.baidu.com/images/marker_red.png',
new BMapGL.Size(32, 32),
{ anchor: new BMapGL.Size(16, 32) }
)
});
trackMapInstance.value.addOverlay(startMarker);
trackStartMarker.value = startMarker;
// 起点信息窗口
const startInfoWindow = new BMapGL.InfoWindow(
'<div style="padding: 5px;"><strong style="color: #67c23a;">起点</strong></div>',
{ width: 80, height: 30 }
);
startMarker.addEventListener('click', () => {
trackMapInstance.value.openInfoWindow(startInfoWindow, startPoint);
});
}
// 添加终点标记
if (trackPath.value.length > 1) {
const endPoint = new BMapGL.Point(
trackPath.value[trackPath.value.length - 1].lng,
trackPath.value[trackPath.value.length - 1].lat
);
// 使用红色标记作为终点
const endMarker = new BMapGL.Marker(endPoint, {
icon: new BMapGL.Icon(
'https://api.map.baidu.com/images/marker_red.png',
new BMapGL.Size(32, 32),
{ anchor: new BMapGL.Size(16, 32) }
)
});
trackMapInstance.value.addOverlay(endMarker);
trackEndMarker.value = endMarker;
// 终点信息窗口
const endInfoWindow = new BMapGL.InfoWindow(
'<div style="padding: 5px;"><strong style="color: #f56c6c;">终点</strong></div>',
{ width: 80, height: 30 }
);
endMarker.addEventListener('click', () => {
trackMapInstance.value.openInfoWindow(endInfoWindow, endPoint);
});
}
};
// 绘制动态轨迹(运输中)
const drawDynamicTrack = (BMapGL) => {
if (!trackMapInstance.value || trackPath.value.length === 0) {
return;
}
// 清除之前的覆盖物
trackMapInstance.value.clearOverlays();
// 绘制已有轨迹线
const polyline = new BMapGL.Polyline(
trackPath.value.map(p => new BMapGL.Point(p.lng, p.lat)),
{
strokeColor: '#3388ff',
strokeWeight: 4,
strokeOpacity: 0.6
}
);
trackMapInstance.value.addOverlay(polyline);
trackPolyline.value = polyline;
// 添加起点标记
if (trackPath.value.length > 0) {
const startPoint = new BMapGL.Point(trackPath.value[0].lng, trackPath.value[0].lat);
const startMarker = new BMapGL.Marker(startPoint, {
icon: new BMapGL.Icon(
'https://api.map.baidu.com/images/marker_red.png',
new BMapGL.Size(32, 32),
{ anchor: new BMapGL.Size(16, 32) }
)
});
trackMapInstance.value.addOverlay(startMarker);
trackStartMarker.value = startMarker;
}
// 添加当前位置标记(用于动画)
const currentPoint = new BMapGL.Point(trackPath.value[0].lng, trackPath.value[0].lat);
const playMarker = new BMapGL.Marker(currentPoint, {
icon: new BMapGL.Icon(
'https://api.map.baidu.com/images/marker_red.png',
new BMapGL.Size(20, 20),
{ anchor: new BMapGL.Size(10, 20) }
)
});
trackMapInstance.value.addOverlay(playMarker);
trackPlayMarker.value = playMarker;
currentPlayIndex.value = 0;
};
// 播放轨迹动画
const handlePlayTrack = () => {
if (trackPath.value.length === 0 || !trackMapInstance.value) {
return;
}
if (isPlaying.value) {
// 暂停
if (playTimer.value) {
clearInterval(playTimer.value);
playTimer.value = null;
}
isPlaying.value = false;
} else {
// 播放
if (currentPlayIndex.value >= trackPath.value.length - 1) {
// 如果已经播放完,重新开始
currentPlayIndex.value = 0;
}
isPlaying.value = true;
playTimer.value = setInterval(() => {
if (currentPlayIndex.value < trackPath.value.length - 1) {
currentPlayIndex.value++;
updatePlayMarker();
} else {
// 播放完成
if (playTimer.value) {
clearInterval(playTimer.value);
playTimer.value = null;
}
isPlaying.value = false;
}
}, 200); // 每200ms移动一次
}
};
// 更新播放位置标记
const updatePlayMarker = () => {
if (!trackMapInstance.value || !trackPlayMarker.value || !trackBMapGL.value || currentPlayIndex.value >= trackPath.value.length) {
return;
}
const point = trackPath.value[currentPlayIndex.value];
const bdPoint = new trackBMapGL.value.Point(point.lng, point.lat);
trackPlayMarker.value.setPosition(bdPoint);
// 地图跟随
trackMapInstance.value.panTo(bdPoint);
};
// 重置轨迹动画
const handleResetTrack = () => {
if (playTimer.value) {
clearInterval(playTimer.value);
playTimer.value = null;
}
isPlaying.value = false;
currentPlayIndex.value = 0;
if (trackPlayMarker.value && trackPath.value.length > 0 && trackMapInstance.value && trackBMapGL.value) {
const point = trackPath.value[0];
const bdPoint = new trackBMapGL.value.Point(point.lng, point.lat);
trackPlayMarker.value.setPosition(bdPoint);
trackMapInstance.value.panTo(bdPoint);
}
};
// 获取运输状态文本
const getDeliveryStatusText = (status) => {
const statusMap = {
1: '准备中',
2: '运输中',
3: '已结束'
};
return statusMap[status] || '未知';
};
// 关闭轨迹对话框
const handleTrackDialogClose = () => {
// 清理定时器
if (playTimer.value) {
clearInterval(playTimer.value);
playTimer.value = null;
}
// 清理地图实例
if (trackMapInstance.value) {
trackMapInstance.value.clearOverlays();
trackMapInstance.value = null;
}
trackPolyline.value = null;
trackStartMarker.value = null;
trackEndMarker.value = null;
trackPlayMarker.value = null;
trackBMapGL.value = null;
isPlaying.value = false;
currentPlayIndex.value = 0;
trackPath.value = [];
trackMapShow.value = false;
stayPoints.value = [];
yingyanMeta.entityName = '';
yingyanMeta.startTime = null;
yingyanMeta.endTime = null;
};
// 组件卸载时清理
onUnmounted(() => {
if (playTimer.value) {
clearInterval(playTimer.value);
}
if (trackMapInstance.value) {
trackMapInstance.value.clearOverlays();
}
});
// 导出方法
defineExpose({
open
});
</script>
<style scoped lang="less">
.warning-content {
padding: 10px 0;
}
// 温度预警专用样式
.temperature-warning {
.warning-description {
margin-top: 20px;
}
.section-header {
margin-bottom: 15px;
h4 {
margin: 0 0 8px 0;
color: #303133;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
}
.section-desc {
margin: 0;
font-size: 13px;
color: #909399;
}
}
.no-data-tip {
padding: 40px 0;
text-align: center;
}
}
.warning-description {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
}
p {
margin: 0;
padding: 10px;
background-color: #f5f7fa;
border-left: 3px solid #409eff;
border-radius: 4px;
line-height: 1.6;
color: #606266;
}
}
.track-meta-panel {
margin-top: 20px;
}
.latest-point-panel {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
font-size: 15px;
font-weight: 600;
color: #303133;
}
}
.segment-stats {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
font-size: 15px;
font-weight: 600;
color: #303133;
}
}
.staypoint-section {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
font-size: 15px;
color: #303133;
font-weight: 600;
}
}
.map-container {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
}
}
.temperature-chart {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
}
}
.device-list-section {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
font-weight: 600;
}
}
.device-logs-section {
margin-top: 20px;
h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
font-weight: 600;
}
:deep(.el-table) {
font-size: 12px;
}
}
// 轨迹对话框样式
.status-tip {
margin-bottom: 20px;
}
.track-controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.no-track-tip {
display: flex;
align-items: center;
justify-content: center;
height: 500px;
}
</style>