<template>
|
<div class="map-panel">
|
<!-- 顶部:图层切换 + 实时统计 -->
|
<div class="map-panel__bar">
|
<cl-select-button v-model="layer" :options="layerOptions" small />
|
<div class="map-panel__stat">
|
<span class="map-panel__pulse" :style="{ background: statColor }" />
|
<strong>{{ list.length }}</strong>
|
<span class="map-panel__unit">{{ statUnit }}</span>
|
<span class="map-panel__update">{{ lastUpdate }}</span>
|
</div>
|
</div>
|
|
<!-- 地图主体 -->
|
<div class="map-panel__map">
|
<tdt-map :center="center" :zoom="zoom" :controls="controls" :loadConfig="MAP_LOAD">
|
<tdt-marker
|
v-for="item in list"
|
:key="keyOf(item)"
|
:position="pos(item)"
|
:icon="iconOf(item)"
|
:title="nameOf(item)"
|
/>
|
</tdt-map>
|
</div>
|
|
<!-- 图例 -->
|
<div class="map-panel__legend">
|
<div v-for="l in activeLegends" :key="l.name" class="map-panel__legend-item">
|
<i :style="{ background: l.color }" />{{ l.name }}
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script lang="ts" setup>
|
defineOptions({
|
name: 'screen-map-panel'
|
});
|
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { TdtMap, TdtMarker, type LngLat } from 'vue-tianditu2';
|
import {
|
DEFAULT_CENTER,
|
MAP_LOAD,
|
SORTER_ICON,
|
VEHICLE_ICON,
|
formatTime,
|
siteIcon,
|
useMapsApi
|
} from '../../maps/hooks/use-map';
|
|
type LayerKey = 'site' | 'vehicle' | 'sorter';
|
|
/** 三图层统一配置:取数 / 唯一键 / 名称 / 图标 / 图例 */
|
const LAYERS: Record<
|
LayerKey,
|
{
|
unit: string;
|
color: string;
|
load: () => Promise<any[]>;
|
key: (e: any) => string;
|
name: (e: any) => string;
|
icon: (e: any) => any;
|
legends: { name: string; color: string }[];
|
}
|
> = {
|
site: {
|
unit: '个站点',
|
color: '#16a34a',
|
load: () => api.sites(),
|
key: (e: any) => e.id,
|
name: (e: any) => e.businessName,
|
icon: (e: any) => siteIcon(e.siteType),
|
legends: [
|
{ name: '收集点', color: '#16a34a' },
|
{ name: '压缩站', color: '#f59e0b' }
|
]
|
},
|
vehicle: {
|
unit: '辆在线',
|
color: '#7c3aed',
|
load: () => api.vehicleOnline(),
|
key: (e: any) => e.vehicleNo,
|
name: (e: any) => e.vehicleNo,
|
icon: () => VEHICLE_ICON,
|
legends: [{ name: '在线车辆', color: '#7c3aed' }]
|
},
|
sorter: {
|
unit: '人在线',
|
color: '#2563eb',
|
load: () => api.sorterOnline(),
|
key: (e: any) => e.idCard,
|
name: (e: any) => e.name || e.idCard,
|
icon: () => SORTER_ICON,
|
legends: [{ name: '在线分拣员', color: '#2563eb' }]
|
}
|
};
|
|
const api = useMapsApi();
|
|
const layer = ref<LayerKey>('site');
|
const list = ref<any[]>([]);
|
const center = ref<LngLat>(DEFAULT_CENTER);
|
const zoom = ref(12);
|
const lastUpdate = ref('');
|
|
const controls = ['Zoom', 'Scale'];
|
|
const layerOptions = [
|
{ label: '站点', value: 'site' },
|
{ label: '车辆', value: 'vehicle' },
|
{ label: '分拣员', value: 'sorter' }
|
];
|
|
const cfg = computed(() => LAYERS[layer.value]);
|
const activeLegends = computed(() => cfg.value.legends);
|
const statColor = computed(() => cfg.value.color);
|
const statUnit = computed(() => cfg.value.unit);
|
|
function pos(item: any): LngLat {
|
return [Number(item.longitude), Number(item.latitude)];
|
}
|
function keyOf(item: any) {
|
return cfg.value.key(item);
|
}
|
function nameOf(item: any) {
|
return cfg.value.name(item);
|
}
|
function iconOf(item: any) {
|
return cfg.value.icon(item);
|
}
|
|
async function load() {
|
const res = await cfg.value.load();
|
list.value = Array.isArray(res) ? res : [];
|
lastUpdate.value = formatTime(new Date());
|
if (list.value[0]) {
|
center.value = pos(list.value[0]);
|
zoom.value = 13;
|
}
|
}
|
|
// 车辆 / 分拣员为实时位置,30s 轮询;站点是静态台账,不轮询
|
let timer: number | undefined;
|
function startPolling() {
|
stopPolling();
|
if (layer.value !== 'site') {
|
timer = window.setInterval(load, 30000);
|
}
|
}
|
function stopPolling() {
|
if (timer) {
|
clearInterval(timer);
|
timer = undefined;
|
}
|
}
|
|
watch(layer, () => {
|
load();
|
startPolling();
|
});
|
|
onMounted(async () => {
|
await load();
|
startPolling();
|
});
|
|
onBeforeUnmount(stopPolling);
|
</script>
|
|
<style lang="scss" scoped>
|
.map-panel {
|
display: flex;
|
flex-direction: column;
|
height: 100%;
|
min-height: 0;
|
|
&__bar {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
padding: 0 4px 8px;
|
flex-shrink: 0;
|
}
|
|
&__stat {
|
display: flex;
|
align-items: baseline;
|
gap: 6px;
|
font-size: 13px;
|
color: var(--el-text-color-secondary);
|
|
strong {
|
font-size: 22px;
|
font-weight: 700;
|
color: var(--el-text-color-primary);
|
}
|
}
|
|
&__pulse {
|
width: 8px;
|
height: 8px;
|
border-radius: 50%;
|
align-self: center;
|
animation: map-pulse 1.6s ease-in-out infinite;
|
}
|
|
&__unit {
|
font-size: 12px;
|
}
|
|
&__update {
|
margin-left: 4px;
|
font-size: 11px;
|
color: var(--el-text-color-secondary);
|
opacity: 0.75;
|
}
|
|
&__map {
|
flex: 1;
|
min-height: 0;
|
border-radius: 8px;
|
overflow: hidden;
|
|
> * {
|
width: 100%;
|
height: 100%;
|
}
|
}
|
|
&__legend {
|
display: flex;
|
align-items: center;
|
gap: 16px;
|
padding-top: 8px;
|
flex-shrink: 0;
|
}
|
|
&__legend-item {
|
display: flex;
|
align-items: center;
|
gap: 6px;
|
font-size: 12px;
|
color: var(--el-text-color-secondary);
|
|
i {
|
width: 8px;
|
height: 8px;
|
border-radius: 50%;
|
}
|
}
|
}
|
|
@media (max-width: 768px) {
|
.map-panel__bar,
|
.map-panel__legend {
|
flex-wrap: wrap;
|
gap: 8px;
|
}
|
|
.map-panel__map {
|
min-height: 280px;
|
}
|
}
|
|
@keyframes map-pulse {
|
0%,
|
100% {
|
opacity: 1;
|
transform: scale(1);
|
}
|
50% {
|
opacity: 0.4;
|
transform: scale(0.75);
|
}
|
}
|
</style>
|