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"
|
|
|
|
|
|
>
|
2025-11-21 17:22:20 +08:00
|
|
|
|
<!-- 轨迹定位按钮 -->
|
|
|
|
|
|
<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>
|
2025-10-30 16:58:39 +08:00
|
|
|
|
<!-- 温度预警 - 只显示设备信息,不显示地图 -->
|
|
|
|
|
|
<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">
|
2025-11-21 17:22:20 +08:00
|
|
|
|
<!-- 调试信息(开发环境可显示) -->
|
|
|
|
|
|
<div v-if="false" style="font-size: 12px; color: #909399; margin-bottom: 10px;">
|
|
|
|
|
|
调试:设备数量={{ deviceList.length }}, deliveryId={{ warningData.deliveryId }}
|
|
|
|
|
|
</div>
|
2025-10-30 16:58:39 +08:00
|
|
|
|
<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>
|
2025-11-21 17:22:20 +08:00
|
|
|
|
<div v-else-if="!loadingDevices" class="no-data-tip">
|
2025-10-30 16:58:39 +08:00
|
|
|
|
<el-empty description="暂无绑定设备信息" :image-size="80" />
|
2025-11-21 17:22:20 +08:00
|
|
|
|
<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>
|
2025-10-30 16:58:39 +08:00
|
|
|
|
</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>
|
2025-11-21 17:22:20 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 轨迹定位对话框 -->
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2025-11-24 16:10:39 +08:00
|
|
|
|
<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">
|
2025-11-21 17:22:20 +08:00
|
|
|
|
<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>
|
2025-10-30 16:58:39 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-11-21 17:22:20 +08:00
|
|
|
|
import { ref, reactive, computed, nextTick, onUnmounted } from 'vue';
|
2025-10-30 16:58:39 +08:00
|
|
|
|
import { ElMessage } from 'element-plus';
|
2025-11-21 17:22:20 +08:00
|
|
|
|
import { Location, VideoPlay, Refresh, InfoFilled, Connection, DataLine, Loading } from '@element-plus/icons-vue';
|
2025-10-30 16:58:39 +08:00
|
|
|
|
import { BMPGL } from '@/utils/loadBmap.js';
|
2025-11-21 17:22:20 +08:00
|
|
|
|
import { pageDeviceList, getCollarLogs, getEarTagLogs, getHostLogs, getYingyanTrack, waybillDetail } from '@/api/abroad.js';
|
2025-10-30 16:58:39 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2025-11-21 17:22:20 +08:00
|
|
|
|
// 轨迹定位相关
|
|
|
|
|
|
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([]); // 停留点列表
|
2025-11-24 16:10:39 +08:00
|
|
|
|
const latestPoint = ref(null); // 最新轨迹点
|
|
|
|
|
|
const segmentStats = ref([]);
|
2025-11-21 17:22:20 +08:00
|
|
|
|
const yingyanMeta = reactive({
|
|
|
|
|
|
entityName: '',
|
|
|
|
|
|
startTime: null,
|
|
|
|
|
|
endTime: null
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-30 16:58:39 +08:00
|
|
|
|
// 计算属性:判断预警类型
|
|
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-24 16:10:39 +08:00
|
|
|
|
const isStaticTrack = computed(() => {
|
|
|
|
|
|
const status = Number(warningData.status || 0);
|
|
|
|
|
|
return status >= 3;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-30 16:58:39 +08:00
|
|
|
|
const dialogTitle = computed(() => {
|
|
|
|
|
|
return `${warningData.warningTypeDesc || '预警'}详情`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 打开对话框
|
|
|
|
|
|
const open = async (row) => {
|
2025-11-21 17:22:20 +08:00
|
|
|
|
console.log('[WARNING-DETAIL] 打开预警详情对话框,原始数据:', row);
|
2025-10-30 16:58:39 +08:00
|
|
|
|
|
|
|
|
|
|
// 填充数据
|
|
|
|
|
|
Object.keys(warningData).forEach(key => {
|
|
|
|
|
|
if (row[key] !== undefined) {
|
|
|
|
|
|
warningData[key] = row[key];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-21 17:22:20 +08:00
|
|
|
|
console.log('[WARNING-DETAIL] 填充后的 warningData:', warningData);
|
|
|
|
|
|
console.log('[WARNING-DETAIL] deliveryId:', warningData.deliveryId);
|
2025-10-30 16:58:39 +08:00
|
|
|
|
|
|
|
|
|
|
dialogVisible.value = true;
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 查询运单绑定的设备列表
|
|
|
|
|
|
if (warningData.deliveryId) {
|
2025-11-21 17:22:20 +08:00
|
|
|
|
console.log('[WARNING-DETAIL] 开始加载设备列表,deliveryId:', warningData.deliveryId);
|
2025-10-30 16:58:39 +08:00
|
|
|
|
await loadDeviceList(warningData.deliveryId);
|
2025-11-21 17:22:20 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
console.warn('[WARNING-DETAIL] 警告:deliveryId 为空,无法加载设备列表');
|
|
|
|
|
|
console.warn('[WARNING-DETAIL] 请检查预警详情 API 是否返回了 deliveryId 字段');
|
2025-10-30 16:58:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是位置相关预警,加载地图
|
|
|
|
|
|
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 {
|
2025-11-21 17:22:20 +08:00
|
|
|
|
console.log('[WARNING-DETAIL] 调用 pageDeviceList API,参数:', {
|
|
|
|
|
|
deliveryId: deliveryId,
|
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
|
pageSize: 100
|
|
|
|
|
|
});
|
2025-10-30 16:58:39 +08:00
|
|
|
|
|
|
|
|
|
|
const res = await pageDeviceList({
|
|
|
|
|
|
deliveryId: deliveryId,
|
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
|
pageSize: 100, // 一次性加载所有设备
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-21 17:22:20 +08:00
|
|
|
|
console.log('[WARNING-DETAIL] pageDeviceList API 返回结果:', res);
|
|
|
|
|
|
|
2025-10-30 16:58:39 +08:00
|
|
|
|
if (res.code === 200 && res.data) {
|
|
|
|
|
|
// ✅ 修复:后端直接返回数组,不是嵌套在 list 或 rows 中
|
2025-11-21 17:22:20 +08:00
|
|
|
|
let devices = [];
|
2025-10-30 16:58:39 +08:00
|
|
|
|
if (Array.isArray(res.data)) {
|
2025-11-21 17:22:20 +08:00
|
|
|
|
devices = res.data;
|
2025-10-30 16:58:39 +08:00
|
|
|
|
} else {
|
2025-11-21 17:22:20 +08:00
|
|
|
|
devices = res.data.list || res.data.rows || [];
|
2025-10-30 16:58:39 +08:00
|
|
|
|
}
|
2025-11-21 17:22:20 +08:00
|
|
|
|
|
|
|
|
|
|
console.log('[WARNING-DETAIL] 解析后的设备列表:', devices);
|
|
|
|
|
|
console.log('[WARNING-DETAIL] 设备数量:', devices.length);
|
|
|
|
|
|
|
|
|
|
|
|
deviceList.value = devices;
|
2025-10-30 16:58:39 +08:00
|
|
|
|
|
2025-11-21 17:22:20 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
2025-10-30 16:58:39 +08:00
|
|
|
|
} else {
|
2025-11-21 17:22:20 +08:00
|
|
|
|
console.error('[WARNING-DETAIL] API 返回错误:', res);
|
2025-10-30 16:58:39 +08:00
|
|
|
|
ElMessage.warning('加载设备列表失败:' + (res.msg || '未知错误'));
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-11-21 17:22:20 +08:00
|
|
|
|
console.error('[WARNING-DETAIL] 加载设备列表异常:', error);
|
|
|
|
|
|
ElMessage.error('加载设备列表失败:' + (error.message || '网络错误'));
|
2025-10-30 16:58:39 +08:00
|
|
|
|
} 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-12-08 15:24:43 +08:00
|
|
|
|
const BMapGL = await BMPGL('3AN3VahoqaXUs32U8luXD2Dwn86KK5B7');
|
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';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-21 17:22:20 +08:00
|
|
|
|
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}秒`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-24 16:10:39 +08:00
|
|
|
|
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,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 16:58:39 +08:00
|
|
|
|
// 关闭对话框
|
|
|
|
|
|
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] = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-21 17:22:20 +08:00
|
|
|
|
// 轨迹定位按钮点击
|
|
|
|
|
|
const handleTrackClick = async () => {
|
|
|
|
|
|
if (!warningData.deliveryId) {
|
|
|
|
|
|
ElMessage.warning('运单ID不存在,无法查看轨迹');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-11-24 16:10:39 +08:00
|
|
|
|
console.info('[TRACK] 开始轨迹定位流程', {
|
|
|
|
|
|
deliveryId: warningData.deliveryId,
|
|
|
|
|
|
status: warningData.status,
|
|
|
|
|
|
estimatedDepartureTime: warningData.estimatedDepartureTime,
|
|
|
|
|
|
});
|
2025-11-21 17:22:20 +08:00
|
|
|
|
|
|
|
|
|
|
trackDialogVisible.value = true;
|
|
|
|
|
|
trackLoading.value = true;
|
|
|
|
|
|
trackMapShow.value = false;
|
|
|
|
|
|
trackPath.value = [];
|
2025-11-24 16:10:39 +08:00
|
|
|
|
latestPoint.value = null;
|
2025-11-21 17:22:20 +08:00
|
|
|
|
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 = [];
|
2025-11-24 16:10:39 +08:00
|
|
|
|
latestPoint.value = null;
|
|
|
|
|
|
segmentStats.value = [];
|
2025-11-21 17:22:20 +08:00
|
|
|
|
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 });
|
2025-11-24 16:10:39 +08:00
|
|
|
|
console.info('[TRACK] 后端轨迹接口响应', res);
|
2025-11-21 17:22:20 +08:00
|
|
|
|
if (res.code === 200 && res.data) {
|
2025-11-24 16:10:39 +08:00
|
|
|
|
segmentStats.value = Array.isArray(res.data.segmentStats) ? res.data.segmentStats : [];
|
|
|
|
|
|
console.info('[TRACK] 分段统计', segmentStats.value);
|
2025-11-21 17:22:20 +08:00
|
|
|
|
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;
|
2025-11-24 16:10:39 +08:00
|
|
|
|
latestPoint.value = parseLatestPoint(res.data.latestPoint);
|
|
|
|
|
|
console.info('[TRACK] 轨迹点数量', trackPath.value.length);
|
2025-11-21 17:22:20 +08:00
|
|
|
|
|
|
|
|
|
|
if (trackPath.value.length > 0) {
|
|
|
|
|
|
trackMapShow.value = true;
|
2025-11-24 16:10:39 +08:00
|
|
|
|
} else if (latestPoint.value) {
|
|
|
|
|
|
trackPath.value.push({
|
|
|
|
|
|
lng: latestPoint.value.lng,
|
|
|
|
|
|
lat: latestPoint.value.lat,
|
|
|
|
|
|
locTime: latestPoint.value.locTime || null,
|
|
|
|
|
|
});
|
|
|
|
|
|
trackMapShow.value = true;
|
2025-11-21 17:22:20 +08:00
|
|
|
|
} 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 {
|
2025-12-08 15:24:43 +08:00
|
|
|
|
const BMapGL = await BMPGL('3AN3VahoqaXUs32U8luXD2Dwn86KK5B7');
|
2025-11-21 17:22:20 +08:00
|
|
|
|
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(
|
2025-11-28 17:12:36 +08:00
|
|
|
|
'https://api.map.baidu.com/images/marker_red.png',
|
2025-11-21 17:22:20 +08:00
|
|
|
|
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(
|
2025-11-28 17:12:36 +08:00
|
|
|
|
'https://api.map.baidu.com/images/marker_red.png',
|
2025-11-21 17:22:20 +08:00
|
|
|
|
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(
|
2025-11-28 17:12:36 +08:00
|
|
|
|
'https://api.map.baidu.com/images/marker_red.png',
|
2025-11-21 17:22:20 +08:00
|
|
|
|
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(
|
2025-11-28 17:12:36 +08:00
|
|
|
|
'https://api.map.baidu.com/images/marker_red.png',
|
2025-11-21 17:22:20 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-30 16:58:39 +08:00
|
|
|
|
// 导出方法
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-21 17:22:20 +08:00
|
|
|
|
.track-meta-panel {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 16:10:39 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-21 17:22:20 +08:00
|
|
|
|
.staypoint-section {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
|
|
|
|
|
|
h4 {
|
|
|
|
|
|
margin: 0 0 10px 0;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 16:58:39 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-21 17:22:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 轨迹对话框样式
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2025-10-30 16:58:39 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
|