283 lines
12 KiB
Vue
283 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onBeforeUnmount, onMounted, shallowRef, useTemplateRef, watch } from 'vue'
|
|
import { init, registerMap, use } from 'echarts/core'
|
|
import { EffectScatterChart, LinesChart } from 'echarts/charts'
|
|
import { GeoComponent, TooltipComponent } from 'echarts/components'
|
|
import { CanvasRenderer } from 'echarts/renderers'
|
|
import chinaGeoJson from 'china-map-data/china'
|
|
import type { ECharts, EChartsCoreOption } from 'echarts/core'
|
|
import type { GeoJSONSourceInput } from 'echarts/types/src/coord/geo/geoTypes.js'
|
|
import type { MapScene } from '@/types/freight'
|
|
import MapOperationsHud from './MapOperationsHud.vue'
|
|
|
|
use([GeoComponent, TooltipComponent, LinesChart, EffectScatterChart, CanvasRenderer])
|
|
registerMap('network-freight-china', chinaGeoJson as GeoJSONSourceInput)
|
|
|
|
const props = defineProps<{ scene: MapScene }>()
|
|
const emit = defineEmits<{ selectRoute: [routeId: string] }>()
|
|
const mapEl = useTemplateRef<HTMLDivElement>('map')
|
|
const chart = shallowRef<ECharts>()
|
|
let resizeObserver: ResizeObserver | undefined
|
|
|
|
const cityCoordinates: Record<string, [number, number]> = {
|
|
乌鲁木齐: [87.62, 43.82], 兰州: [103.83, 36.06], 成都: [104.07, 30.67],
|
|
西安: [108.94, 34.34], 重庆: [106.55, 29.56], 昆明: [102.71, 25.04],
|
|
北京: [116.41, 39.9], 天津: [117.2, 39.13], 济南: [117.12, 36.65],
|
|
青岛: [120.38, 36.07], 郑州: [113.63, 34.75], 临沂: [118.36, 35.1],
|
|
武汉: [114.31, 30.59], 南京: [118.8, 32.06], 上海: [121.47, 31.23],
|
|
杭州: [120.16, 30.27], 长沙: [112.94, 28.23], 南昌: [115.86, 28.68],
|
|
厦门: [118.09, 24.48], 广州: [113.27, 23.13], 深圳: [114.06, 22.55],
|
|
南宁: [108.37, 22.82], 哈尔滨: [126.64, 45.76], 沈阳: [123.43, 41.8],
|
|
鄂尔多斯: [109.78, 39.61], 榆林: [109.73, 38.29], 大同: [113.3, 40.08],
|
|
神木: [110.5, 38.84], 乌海: [106.82, 39.67], 包头: [109.84, 40.66],
|
|
晋城: [112.85, 35.49], 阳泉: [113.58, 37.86], 石家庄: [114.52, 38.05],
|
|
唐山: [118.18, 39.63], 邯郸: [114.49, 36.62],
|
|
贵阳: [106.63, 26.65], 六盘水: [104.83, 26.59], 毕节: [105.29, 27.3],
|
|
曲靖: [103.8, 25.49], 昭通: [103.72, 27.34], 达州: [107.47, 31.21],
|
|
}
|
|
|
|
const selectedRoute = computed(() =>
|
|
props.scene.routes.find((route) => route.id === props.scene.selectedRouteId) ?? props.scene.routes[0],
|
|
)
|
|
|
|
const auxiliaryFlows = [
|
|
['鄂尔多斯', '上海'], ['榆林', '武汉'], ['大同', '南京'],
|
|
['乌海', '济南'], ['六盘水', '广州'], ['毕节', '成都'],
|
|
['曲靖', '南宁'], ['昭通', '重庆'],
|
|
].map(([from, to], index) => ({
|
|
id: `A${index + 1}`,
|
|
name: `${from} → ${to} · 协同通道`,
|
|
coords: [cityCoordinates[from], cityCoordinates[to]],
|
|
lineStyle: {
|
|
color: index % 3 === 0 ? '#ffc857' : '#1fcce8',
|
|
width: index % 3 === 0 ? 1.25 : 0.9,
|
|
opacity: index % 3 === 0 ? 0.62 : 0.38,
|
|
curveness: 0.3 + index % 3 * 0.05,
|
|
},
|
|
}))
|
|
|
|
const option = computed<EChartsCoreOption>(() => {
|
|
const activeRouteId = props.scene.selectedRouteId
|
|
const routeData = props.scene.routes.map((route) => ({
|
|
id: route.id,
|
|
name: `${route.from.name} → ${route.to.name}`,
|
|
coords: [cityCoordinates[route.from.name], cityCoordinates[route.to.name]],
|
|
lineStyle: {
|
|
color: route.id === activeRouteId ? '#ffc857' : route.riskCount > 2 ? '#ff415d' : '#27d8ff',
|
|
width: route.id === activeRouteId ? 2.4 : 1.2,
|
|
opacity: route.id === activeRouteId ? 1 : 0.66,
|
|
curveness: 0.24,
|
|
},
|
|
}))
|
|
|
|
const involvedCities = new Map<string, number>()
|
|
props.scene.routes.forEach((route) => {
|
|
involvedCities.set(route.from.name, route.from.volume)
|
|
involvedCities.set(route.to.name, route.to.volume)
|
|
})
|
|
auxiliaryFlows.forEach((flow) => {
|
|
const [from, to] = flow.name.split(' · ')[0].split(' → ')
|
|
if (!involvedCities.has(from)) involvedCities.set(from, 74)
|
|
if (!involvedCities.has(to)) involvedCities.set(to, 68)
|
|
})
|
|
|
|
return {
|
|
animation: true,
|
|
animationDurationUpdate: 500,
|
|
tooltip: {
|
|
trigger: 'item',
|
|
backgroundColor: 'rgba(3,20,33,.94)',
|
|
borderColor: 'rgba(25,217,255,.42)',
|
|
textStyle: { color: '#d9f9ff', fontSize: 11 },
|
|
},
|
|
geo: [
|
|
{
|
|
map: 'network-freight-china',
|
|
roam: false,
|
|
layoutCenter: ['50%', '50%'],
|
|
layoutSize: '100%',
|
|
silent: true,
|
|
z: 2,
|
|
itemStyle: {
|
|
areaColor: {
|
|
type: 'linear',
|
|
x: 0, y: 0, x2: 1, y2: 1,
|
|
colorStops: [
|
|
{ offset: 0, color: '#0c5471' },
|
|
{ offset: 0.52, color: '#08344f' },
|
|
{ offset: 1, color: '#061b31' },
|
|
],
|
|
},
|
|
borderColor: '#32d8ff',
|
|
borderWidth: 1.1,
|
|
shadowColor: 'rgba(24,206,255,.48)',
|
|
shadowBlur: 12,
|
|
},
|
|
emphasis: {
|
|
disabled: false,
|
|
itemStyle: { areaColor: '#0d6684', borderColor: '#b8f8ff', borderWidth: 1.6 },
|
|
label: { show: true, color: '#e9fdff', fontSize: 10 },
|
|
},
|
|
label: { show: false },
|
|
},
|
|
{
|
|
map: 'network-freight-china',
|
|
layoutCenter: ['50%', '52.3%'],
|
|
layoutSize: '100%',
|
|
silent: true,
|
|
z: 1,
|
|
itemStyle: {
|
|
areaColor: '#020b16',
|
|
borderColor: '#0d6a8b',
|
|
borderWidth: 1.3,
|
|
shadowColor: '#000',
|
|
shadowBlur: 20,
|
|
shadowOffsetY: 12,
|
|
},
|
|
},
|
|
],
|
|
series: [
|
|
{
|
|
type: 'lines',
|
|
coordinateSystem: 'geo',
|
|
geoIndex: 0,
|
|
zlevel: 3,
|
|
data: routeData,
|
|
lineStyle: {
|
|
color: '#0fc8ef',
|
|
width: 5,
|
|
opacity: 0.11,
|
|
curveness: 0.24,
|
|
shadowColor: '#19d9ff',
|
|
shadowBlur: 12,
|
|
},
|
|
silent: true,
|
|
},
|
|
{
|
|
type: 'lines',
|
|
coordinateSystem: 'geo',
|
|
geoIndex: 0,
|
|
zlevel: 4,
|
|
data: routeData,
|
|
polyline: false,
|
|
lineStyle: { curveness: 0.24 },
|
|
symbol: ['none', 'arrow'],
|
|
symbolSize: 6,
|
|
effect: {
|
|
show: true,
|
|
period: 3.7,
|
|
trailLength: 0.36,
|
|
symbol: 'arrow',
|
|
symbolSize: 7,
|
|
color: '#d8fbff',
|
|
},
|
|
emphasis: { lineStyle: { width: 3, opacity: 1 } },
|
|
},
|
|
{
|
|
type: 'lines',
|
|
coordinateSystem: 'geo',
|
|
geoIndex: 0,
|
|
zlevel: 4,
|
|
data: auxiliaryFlows,
|
|
lineStyle: { curveness: 0.34, type: 'dashed' },
|
|
effect: {
|
|
show: true,
|
|
period: 5.8,
|
|
trailLength: 0.5,
|
|
symbol: 'circle',
|
|
symbolSize: 4,
|
|
color: '#ffe4a3',
|
|
},
|
|
silent: true,
|
|
},
|
|
{
|
|
type: 'effectScatter',
|
|
coordinateSystem: 'geo',
|
|
geoIndex: 0,
|
|
zlevel: 5,
|
|
symbolSize: (value: number[]) => Math.max(6, Math.min(13, value[2] / 18)),
|
|
rippleEffect: { period: 3.6, scale: 3, brushType: 'stroke' },
|
|
itemStyle: { color: '#8ff6ff', shadowColor: '#19d9ff', shadowBlur: 12 },
|
|
label: {
|
|
show: true,
|
|
position: 'right',
|
|
formatter: '{b}',
|
|
color: '#c9f8ff',
|
|
fontSize: 9,
|
|
textBorderColor: '#02101a',
|
|
textBorderWidth: 3,
|
|
},
|
|
data: [...involvedCities].map(([name, volume]) => ({
|
|
name,
|
|
value: [...cityCoordinates[name], volume],
|
|
})),
|
|
},
|
|
{
|
|
type: 'effectScatter',
|
|
coordinateSystem: 'geo',
|
|
geoIndex: 0,
|
|
zlevel: 6,
|
|
symbolSize: 15,
|
|
rippleEffect: { period: 2.8, scale: 4.2, brushType: 'stroke' },
|
|
itemStyle: { color: '#ffc857', shadowColor: '#ffc857', shadowBlur: 16 },
|
|
label: { show: false },
|
|
data: ['鄂尔多斯', '榆林', '大同', '神木', '六盘水', '毕节', '曲靖', '昭通', '达州'].map(name => ({
|
|
name: `${name}煤源`,
|
|
value: [...cityCoordinates[name], 180],
|
|
})),
|
|
silent: true,
|
|
},
|
|
],
|
|
}
|
|
})
|
|
|
|
function handleRouteClick(params: unknown) {
|
|
const data = (params as { data?: { id?: string } | null }).data
|
|
if (data?.id) emit('selectRoute', data.id)
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (!mapEl.value) return
|
|
chart.value = init(mapEl.value, undefined, { renderer: 'canvas' })
|
|
chart.value.setOption(option.value)
|
|
chart.value.on('click', handleRouteClick)
|
|
resizeObserver = new ResizeObserver(() => chart.value?.resize())
|
|
resizeObserver.observe(mapEl.value)
|
|
})
|
|
|
|
watch(option, (value) => chart.value?.setOption(value, { notMerge: true }))
|
|
|
|
onBeforeUnmount(() => {
|
|
resizeObserver?.disconnect()
|
|
chart.value?.off('click', handleRouteClick)
|
|
chart.value?.dispose()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="china-freight-map">
|
|
<div ref="map" class="china-freight-map__canvas" />
|
|
<div class="china-freight-map__grid" />
|
|
<div class="china-freight-map__orbits" aria-hidden="true"><i /><i /><i /></div>
|
|
<MapOperationsHud :scene="scene" @select-route="emit('selectRoute', $event)" />
|
|
<div class="china-freight-map__caption">REAL-TIME NATIONAL FREIGHT NETWORK</div>
|
|
<div class="route-focus">
|
|
<span>重点通道 / {{ selectedRoute.id }}</span>
|
|
<strong>{{ selectedRoute.from.name }} <i>→</i> {{ selectedRoute.to.name }}</strong>
|
|
<div><b>{{ selectedRoute.orders.toLocaleString() }}</b> 单 <b>{{ selectedRoute.tonnage.toLocaleString() }}</b> 吨 <b>{{ selectedRoute.punctuality }}%</b> 准时</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.china-freight-map { position: relative; width: 100%; height: 100%; overflow: hidden; background: radial-gradient(ellipse at 50% 54%,rgba(7,91,126,.18),transparent 62%); }
|
|
.china-freight-map::before { content:''; position:absolute; left:14%; right:14%; bottom:1%; height:18%; border:1px solid rgba(25,217,255,.2); border-radius:50%; box-shadow:0 0 30px rgba(25,217,255,.12),inset 0 0 25px rgba(22,104,255,.1); }
|
|
.china-freight-map__canvas { position:absolute; inset:0; z-index:2; }
|
|
.china-freight-map__grid { position:absolute; z-index:1; left:8%; right:8%; bottom:1%; height:36%; transform:perspective(220px) rotateX(66deg); transform-origin:bottom; background-image:linear-gradient(rgba(25,217,255,.1) 1px,transparent 1px),linear-gradient(90deg,rgba(25,217,255,.1) 1px,transparent 1px); background-size:25px 25px; mask-image:radial-gradient(ellipse,black,transparent 70%); }
|
|
.china-freight-map__orbits{position:absolute;z-index:3;left:54%;top:38%;width:210px;height:120px;transform:translate(-50%,-50%);pointer-events:none}.china-freight-map__orbits i{position:absolute;inset:0;border:1px solid rgba(255,200,87,.23);border-radius:50%;animation:orbitPulse 4.5s ease-in-out infinite}.china-freight-map__orbits i:nth-child(2){inset:15px 28px;border-color:rgba(25,217,255,.28);animation-delay:-1.5s}.china-freight-map__orbits i:nth-child(3){inset:30px 56px;border-color:rgba(255,200,87,.35);animation-delay:-3s}
|
|
.china-freight-map__caption { position:absolute; z-index:3; left:18px; bottom:12px; color:rgba(76,221,255,.42); font:600 10px Rajdhani; letter-spacing:4px; }
|
|
.route-focus { position:absolute; z-index:4; right:15px; top:15px; width:195px; padding:10px 12px; border-left:2px solid var(--gold); background:linear-gradient(90deg,rgba(10,40,59,.94),rgba(4,18,31,.38)); pointer-events:none; }
|
|
.route-focus span{color:#7daab9;font-size:9px;letter-spacing:1px}.route-focus strong{display:block;margin:4px 0 6px;color:#f4fbff;font-size:14px}.route-focus i{color:var(--gold);font-style:normal}.route-focus div{display:flex;gap:7px;color:#729baa;font-size:8px}.route-focus b{color:var(--gold);font:600 12px Rajdhani}
|
|
@keyframes orbitPulse{0%,100%{opacity:.18;transform:scale(.88) rotate(-4deg)}50%{opacity:.72;transform:scale(1.08) rotate(4deg);box-shadow:0 0 18px rgba(25,217,255,.12)}}
|
|
</style>
|