<template>
|
<map-board :open="!!current" :title="current?.vehicleNo" :sub="current?.vehicleType" @close="close">
|
<template #side-head>
|
<el-input v-model="keyword" placeholder="搜索车牌" clearable size="small" />
|
<div class="hint">最近 15 分钟有 GPS 视为在线 · {{ list.length }} 辆</div>
|
</template>
|
<template #side>
|
<div
|
v-for="item in visibleList"
|
:key="item.vehicleNo"
|
class="list-item"
|
:class="{ 'is-active': current?.vehicleNo === item.vehicleNo }"
|
@click="select(item)"
|
>
|
<i />
|
<div>
|
<div class="list-item__name">{{ item.vehicleNo }}</div>
|
<div class="list-item__meta">{{ formatTime(item.gpsTime) }} · {{ item.speed || 0 }} km/h</div>
|
</div>
|
</div>
|
<el-empty v-if="!visibleList.length" description="当前没有在线车辆" :image-size="64" />
|
</template>
|
<template #legend>
|
<div class="legend"><i style="background:#7c3aed" />在线车辆</div>
|
<div class="legend"><i style="background:#ef4444" />轨迹回放</div>
|
</template>
|
<template #map>
|
<div class="mapDiv">
|
<tdt-map :center="center" :zoom="zoom" :controls="controls" :loadConfig="MAP_LOAD">
|
<tdt-marker
|
v-for="item in visibleList"
|
:key="item.vehicleNo"
|
:position="pos(item)"
|
:icon="VEHICLE_ICON"
|
:title="item.vehicleNo"
|
@click="select(item)"
|
/>
|
<tdt-polyline v-if="trackPath.length" :path="trackPath" color="#7c3aed" :weight="4" :opacity="0.85" />
|
<tdt-marker v-if="playPos" :position="playPos" :icon="PLAY_ICON" title="回放位置" />
|
</tdt-map>
|
</div>
|
</template>
|
<template #panel>
|
<el-tabs v-model="tab">
|
<el-tab-pane label="基本信息" name="info">
|
<el-descriptions :column="1" border size="small">
|
<el-descriptions-item label="车牌">{{ current?.vehicleNo }}</el-descriptions-item>
|
<el-descriptions-item label="车型">{{ current?.vehicleType || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="载重">{{ current?.capacityTon || '-' }} 吨</el-descriptions-item>
|
<el-descriptions-item label="设备号">{{ current?.sn || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="当前速度">{{ current?.speed || 0 }} km/h</el-descriptions-item>
|
<el-descriptions-item label="定位时间">{{ formatTime(current?.gpsTime) }}</el-descriptions-item>
|
</el-descriptions>
|
</el-tab-pane>
|
<el-tab-pane label="司机行为" name="event">
|
<el-table :data="events" size="small" height="280" stripe>
|
<el-table-column label="时间" min-width="140">
|
<template #default="{ row }">{{ formatTime(row.alarmTime || row.createTime) }}</template>
|
</el-table-column>
|
<el-table-column prop="alarmType" label="类型" min-width="90" />
|
<el-table-column prop="describe" label="描述" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="deviceName" label="设备" min-width="90" show-overflow-tooltip />
|
</el-table>
|
</el-tab-pane>
|
<el-tab-pane label="车载监控" name="live">
|
<el-empty v-if="!cameras.length" description="该车辆没有关联车载摄像头" :image-size="72" />
|
<div v-for="dev in cameras" :key="dev.id" class="cam">
|
<div class="cam__name">{{ dev.iotName || dev.iotCode }}</div>
|
<ezuikit-player
|
v-if="ysToken"
|
:key="'v_' + dev.id + '_' + ysToken"
|
:container-id="'vehicle_player_' + dev.id"
|
:access-token="ysToken"
|
:url="`ezopen://open.ys7.com/${dev.iotCode}/1.hd.live`"
|
/>
|
</div>
|
</el-tab-pane>
|
<el-tab-pane label="历史轨迹" name="track">
|
<track-toolbar
|
v-model:speed="speed"
|
:playing="playing"
|
:progress="progress"
|
:has-track="trackPoints.length > 1"
|
:start-time="trackStart"
|
:end-time="trackEnd"
|
@play="play"
|
@pause="pause"
|
@stop="stop"
|
@query="onQueryTrack"
|
/>
|
<div class="hint">默认最近 1 小时,回放默认 5 倍速</div>
|
</el-tab-pane>
|
</el-tabs>
|
</template>
|
</map-board>
|
</template>
|
|
<script lang="ts" setup>
|
defineOptions({ name: 'maps-vehicle' });
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
import { TdtMap, TdtMarker, TdtPolyline, type LngLat } from 'vue-tianditu2';
|
import { useCool } from '/@/cool';
|
import EzuikitPlayer from '../../ezuikit/components/ezuikit-player.vue';
|
import MapBoard from '../components/map-board.vue';
|
import TrackToolbar from '../components/track-toolbar.vue';
|
import {
|
DEFAULT_CENTER,
|
MAP_LOAD,
|
PLAY_ICON,
|
VEHICLE_ICON,
|
fetchYsToken,
|
formatTime,
|
lastHours,
|
useMapsApi
|
} from '../hooks/use-map';
|
import { useTrackPlayback, type TrackPoint } from '../hooks/use-track';
|
|
const { service } = useCool();
|
const api = useMapsApi();
|
const list = ref<any[]>([]);
|
const keyword = ref('');
|
const current = ref<any>(null);
|
const center = ref<LngLat>(DEFAULT_CENTER);
|
const zoom = ref(12);
|
const tab = ref('info');
|
const events = ref<any[]>([]);
|
const cameras = ref<any[]>([]);
|
const ysToken = ref('');
|
const trackPoints = ref<TrackPoint[]>([]);
|
const trackStart = ref('');
|
const trackEnd = ref('');
|
const { playing, speed, progress, playPos, path, play, pause, stop, reset } = useTrackPlayback(trackPoints);
|
const trackPath = computed(() => path());
|
const controls = ['Zoom', 'Scale', { name: 'MapType', position: 'topright' }];
|
let timer: number;
|
|
const visibleList = computed(() => {
|
if (!keyword.value) return list.value;
|
const k = keyword.value.toLowerCase();
|
return list.value.filter(e => String(e.vehicleNo || '').toLowerCase().includes(k));
|
});
|
|
function pos(item: any): LngLat {
|
return [Number(item.longitude), Number(item.latitude)];
|
}
|
|
function fmt(d: Date) {
|
const p = (n: number) => String(n).padStart(2, '0');
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
}
|
|
async function loadOnline() {
|
list.value = (await api.vehicleOnline()) || [];
|
if (!current.value && list.value[0]) {
|
center.value = pos(list.value[0]);
|
zoom.value = 13;
|
}
|
}
|
|
async function select(item: any) {
|
current.value = item;
|
center.value = pos(item);
|
zoom.value = 16;
|
tab.value = 'info';
|
reset();
|
cameras.value = [];
|
events.value = [];
|
const { start, end } = lastHours(1);
|
trackStart.value = fmt(start);
|
trackEnd.value = fmt(end);
|
if (!ysToken.value) {
|
try {
|
ysToken.value = await fetchYsToken(service);
|
} catch {}
|
}
|
const [ev, cam, track] = await Promise.all([
|
api.vehicleEvents(item.vehicleNo),
|
service.basicdata.iot.getYsDevices({ businessId: item.vehicleNo }).catch(() => []),
|
api.vehicleTrack({ vehicleNo: item.vehicleNo, startTime: trackStart.value, endTime: trackEnd.value })
|
]);
|
events.value = ev || [];
|
cameras.value = cam || [];
|
trackPoints.value = track || [];
|
if (trackPoints.value.length) {
|
playPos.value = [Number(trackPoints.value[0].longitude), Number(trackPoints.value[0].latitude)];
|
}
|
}
|
|
async function onQueryTrack(range: [string, string]) {
|
if (!current.value) return;
|
trackStart.value = range[0];
|
trackEnd.value = range[1];
|
reset();
|
trackPoints.value = (await api.vehicleTrack({
|
vehicleNo: current.value.vehicleNo,
|
startTime: range[0],
|
endTime: range[1]
|
})) || [];
|
}
|
|
function close() {
|
current.value = null;
|
reset();
|
trackPoints.value = [];
|
}
|
|
onMounted(async () => {
|
await loadOnline();
|
timer = window.setInterval(loadOnline, 30000);
|
});
|
|
onBeforeUnmount(() => clearInterval(timer));
|
</script>
|
|
<style lang="scss" scoped>
|
.hint {
|
margin-top: 8px;
|
font-size: 12px;
|
color: var(--el-text-color-secondary);
|
line-height: 1.4;
|
}
|
|
.list-item {
|
display: flex;
|
gap: 8px;
|
padding: 10px 12px;
|
cursor: pointer;
|
border-bottom: 1px solid var(--el-border-color-extra-light);
|
|
i {
|
width: 8px;
|
height: 8px;
|
margin-top: 6px;
|
border-radius: 50%;
|
background: #7c3aed;
|
flex-shrink: 0;
|
}
|
|
&:hover,
|
&.is-active {
|
background: var(--el-color-primary-light-9);
|
}
|
|
&__name {
|
font-size: 13px;
|
font-weight: 600;
|
}
|
|
&__meta {
|
margin-top: 2px;
|
font-size: 12px;
|
color: var(--el-text-color-secondary);
|
}
|
}
|
|
.legend {
|
display: flex;
|
align-items: center;
|
gap: 6px;
|
margin: 2px 0;
|
font-size: 12px;
|
|
i {
|
width: 8px;
|
height: 8px;
|
border-radius: 50%;
|
}
|
}
|
|
.cam {
|
margin-bottom: 12px;
|
|
&__name {
|
margin-bottom: 6px;
|
font-size: 13px;
|
color: var(--el-text-color-regular);
|
}
|
}
|
|
.mapDiv {
|
width: 100%;
|
height: 100%;
|
}
|
</style>
|