Files
cattleTransportation/pc-cattle-transportation/src/views/earlywarning/warningDetailDialog.vue

726 lines
28 KiB
Vue
Raw Normal View History

2025-10-30 16:58:39 +08:00
<template>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="900px"
:close-on-click-modal="false"
@close="handleClose"
>
<!-- 温度预警 - 只显示设备信息不显示地图 -->
<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 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 class="no-data-tip">
<el-empty description="暂无绑定设备信息" :image-size="80" />
</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>
</template>
<script setup>
import { ref, reactive, computed, nextTick } from 'vue';
import { ElMessage } from 'element-plus';
import { BMPGL } from '@/utils/loadBmap.js';
import { pageDeviceList, getCollarLogs, getEarTagLogs, getHostLogs } 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 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 dialogTitle = computed(() => {
return `${warningData.warningTypeDesc || '预警'}详情`;
});
// 打开对话框
const open = async (row) => {
// 填充数据
Object.keys(warningData).forEach(key => {
if (row[key] !== undefined) {
warningData[key] = row[key];
}
});
dialogVisible.value = true;
// ✅ 查询运单绑定的设备列表
if (warningData.deliveryId) {
await loadDeviceList(warningData.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 {
const res = await pageDeviceList({
deliveryId: deliveryId,
pageNum: 1,
pageSize: 100, // 一次性加载所有设备
});
if (res.code === 200 && res.data) {
// ✅ 修复:后端直接返回数组,不是嵌套在 list 或 rows 中
if (Array.isArray(res.data)) {
deviceList.value = res.data;
} else {
deviceList.value = res.data.list || res.data.rows || [];
}
// 自动加载所有设备的日志
await loadAllDeviceLogs();
} else {
ElMessage.warning('加载设备列表失败:' + (res.msg || '未知错误'));
}
} catch (error) {
console.error('[WARNING-DETAIL] 加载设备列表失败:', error);
ElMessage.error('加载设备列表失败');
} 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
2025-11-04 09:38:19 +08:00
const BMapGL = await BMPGL('fLz8UwJSM3ayYl6dtsWYp7TQ8993R6kC');
2025-10-30 16:58:39 +08:00
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 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] = '';
}
});
};
// 导出方法
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;
}
}
.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;
}
}
</style>