454 lines
10 KiB
Vue
454 lines
10 KiB
Vue
<template>
|
||
<section class="traffic-3">
|
||
<div class="chart-legend">
|
||
<span class="legend-item legend-item--actual">实际</span>
|
||
<span class="legend-item legend-item--compare">对比日(2026-03-05)</span>
|
||
<span class="legend-item legend-item--history">历史峰值</span>
|
||
<span class="legend-unit">单位:万人次</span>
|
||
</div>
|
||
|
||
<div class="line-chart-grid">
|
||
<article v-for="(item, index) in chartCards" :key="item.lineId" class="line-chart-card">
|
||
<div class="line-chart-header">
|
||
<span class="line-badge" :style="getLineStyle(item.lineId)">{{ item.lineId }}</span>
|
||
<span class="line-title">累计<br>客流</span>
|
||
<div class="total-digits">
|
||
<span v-for="(_, digitIndex) in targetTotals[index]" :key="`${item.lineId}-${digitIndex}`" class="digit-box">
|
||
<span
|
||
:ref="el => setDigitRef(el, index, digitIndex)"
|
||
class="digit-odometer"
|
||
>0</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<v-chart class="line-chart" :option="item.option" autoresize />
|
||
</article>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||
import VChart from 'vue-echarts'
|
||
import * as echarts from 'echarts'
|
||
import Odometer from 'odometer'
|
||
import 'odometer/themes/odometer-theme-default.css'
|
||
import { METRO_LINES } from '@/utils/const'
|
||
|
||
const hours = ['5', '7', '9', '11', '13', '15', '17', '19', '21', '23']
|
||
const pointGlowSymbol = 'image:///imgs/traffic/4-item.png'
|
||
|
||
const lineCards = ref([
|
||
{
|
||
lineId: 3,
|
||
total: '2746782',
|
||
actual: [2600, 4700, 2400, 3900, 2300, 4100, 2600, 3300, 1700, 2200],
|
||
compare: [4850, 4550, 4650, 4300, 4300, 4550, 4700, 3900, 3900, 3000],
|
||
history: [5000, 4600, 4700, 4200, 4200, 4450, 4600, 4300, 3900, 3600]
|
||
},
|
||
{
|
||
lineId: 4,
|
||
total: '2746782',
|
||
actual: [700, 950, 1550, 4200, 1700, 1350, 1600, 1550, 2500, 1400],
|
||
compare: [1150, 1450, 2200, 3900, 2500, 1550, 1700, 1650, 2950, 2050],
|
||
history: [1350, 2200, 3100, 4450, 3000, 2650, 2550, 2750, 4200, 4550]
|
||
},
|
||
{
|
||
lineId: 7,
|
||
total: '2746782',
|
||
actual: [2900, 3300, 5000, 2300, 2000, 1900, 2600, 4050, 1700, 1700],
|
||
compare: [3200, 3700, 5200, 2500, 2400, 2500, 3100, 4700, 2400, 2400],
|
||
history: [3300, 4100, 5300, 2700, 2650, 2900, 3500, 4950, 3300, 2700]
|
||
},
|
||
{
|
||
lineId: 15,
|
||
total: '2746782',
|
||
actual: [3000, 3600, 4550, 2400, 3900, 2200, 3400, 4050, 2800, 3300],
|
||
compare: [4700, 4300, 4500, 4100, 4100, 4300, 4400, 4550, 4000, 3900],
|
||
history: [4850, 4350, 4550, 4100, 4100, 4300, 4450, 4500, 3950, 3600]
|
||
}
|
||
])
|
||
|
||
|
||
setTimeout(() => {
|
||
lineCards.value[0].total = '8846081'
|
||
}, 8000);
|
||
|
||
const chartCards = computed(() => {
|
||
return lineCards.value.map(item => ({
|
||
...item,
|
||
option: createChartOption(item)
|
||
}))
|
||
})
|
||
|
||
const targetTotals = computed(() => lineCards.value.map(item => item.total.split('')))
|
||
const digitEls = []
|
||
const odometerInstances = []
|
||
|
||
const setDigitRef = (el, cardIndex, digitIndex) => {
|
||
if (!el) {
|
||
return
|
||
}
|
||
|
||
digitEls[cardIndex] ||= []
|
||
digitEls[cardIndex][digitIndex] = el
|
||
}
|
||
|
||
const initOdometers = () => {
|
||
targetTotals.value.forEach((digits, cardIndex) => {
|
||
odometerInstances[cardIndex] ||= []
|
||
|
||
digits.forEach((_, digitIndex) => {
|
||
const el = digitEls[cardIndex]?.[digitIndex]
|
||
|
||
if (!el || odometerInstances[cardIndex][digitIndex]) {
|
||
return
|
||
}
|
||
|
||
odometerInstances[cardIndex][digitIndex] = new Odometer({
|
||
el,
|
||
value: 0,
|
||
format: 'd',
|
||
duration: 1200,
|
||
theme: 'default'
|
||
})
|
||
})
|
||
})
|
||
}
|
||
|
||
const updateOdometers = () => {
|
||
targetTotals.value.forEach((digits, cardIndex) => {
|
||
digits.forEach((digit, digitIndex) => {
|
||
odometerInstances[cardIndex]?.[digitIndex]?.update(Number(digit))
|
||
})
|
||
})
|
||
}
|
||
|
||
watch(targetTotals, async () => {
|
||
await nextTick()
|
||
initOdometers()
|
||
updateOdometers()
|
||
})
|
||
|
||
onMounted(async () => {
|
||
await nextTick()
|
||
initOdometers()
|
||
requestAnimationFrame(updateOdometers)
|
||
})
|
||
|
||
const getLineStyle = lineId => {
|
||
const theme = METRO_LINES[lineId] || {
|
||
bg: '#2bd5ff',
|
||
color: '#00194a'
|
||
}
|
||
|
||
return {
|
||
backgroundColor: theme.bg,
|
||
color: theme.color
|
||
}
|
||
}
|
||
|
||
const createChartOption = item => ({
|
||
backgroundColor: 'transparent',
|
||
animationDuration: 900,
|
||
grid: {
|
||
left: 6,
|
||
right: 34,
|
||
top: 16,
|
||
bottom: 14,
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: hours,
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: 'rgba(167, 205, 255, 0.45)'
|
||
}
|
||
},
|
||
axisTick: {
|
||
show: false
|
||
},
|
||
axisLabel: {
|
||
color: 'rgba(226, 239, 255, 0.96)',
|
||
fontSize: 14,
|
||
fontWeight: 600,
|
||
margin: 10
|
||
},
|
||
splitLine: {
|
||
show: false
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
min: 0,
|
||
max: 5000,
|
||
interval: 1000,
|
||
axisLine: {
|
||
show: false
|
||
},
|
||
axisTick: {
|
||
show: false
|
||
},
|
||
axisLabel: {
|
||
color: 'rgba(226, 239, 255, 0.96)',
|
||
fontSize: 13,
|
||
fontWeight: 600,
|
||
margin: 10
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(219, 235, 255, 0.32)',
|
||
type: 'dashed',
|
||
width: 1.5
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
type: 'scatter',
|
||
data: hours.map((hour, index) => [hour, item.actual[index]]),
|
||
symbol: pointGlowSymbol,
|
||
symbolSize: [38, 88],
|
||
symbolOffset: [3, 0],
|
||
symbolKeepAspect: false,
|
||
silent: true,
|
||
z: 3,
|
||
tooltip: {
|
||
show: false
|
||
}
|
||
},
|
||
{
|
||
name: '实际',
|
||
type: 'line',
|
||
data: item.actual,
|
||
smooth: false,
|
||
symbol: 'circle',
|
||
symbolSize: 5,
|
||
z: 5,
|
||
lineStyle: {
|
||
color: '#20cfff',
|
||
width: 3,
|
||
shadowBlur: 8,
|
||
shadowColor: 'rgba(32, 207, 255, 0.55)'
|
||
},
|
||
itemStyle: {
|
||
color: '#dffbff',
|
||
borderColor: '#20cfff',
|
||
borderWidth: 2
|
||
},
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: 'rgba(27, 218, 255, 0.86)' },
|
||
{ offset: 0.58, color: 'rgba(21, 144, 226, 0.48)' },
|
||
{ offset: 1, color: 'rgba(13, 74, 166, 0.16)' }
|
||
])
|
||
}
|
||
},
|
||
{
|
||
name: '对比日(2026-03-05)',
|
||
type: 'line',
|
||
data: item.compare,
|
||
smooth: false,
|
||
symbol: 'none',
|
||
z: 3,
|
||
lineStyle: {
|
||
color: '#f5c400',
|
||
width: 2
|
||
}
|
||
},
|
||
{
|
||
name: '历史峰值',
|
||
type: 'line',
|
||
data: item.history,
|
||
smooth: false,
|
||
symbol: 'none',
|
||
z: 3,
|
||
lineStyle: {
|
||
color: '#ff1d1d',
|
||
width: 2
|
||
}
|
||
}
|
||
],
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(0, 18, 55, 0.92)',
|
||
borderColor: 'rgba(31, 205, 255, 0.45)',
|
||
textStyle: {
|
||
color: '#ffffff',
|
||
fontSize: 12
|
||
}
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.traffic-3 {
|
||
position: absolute;
|
||
left: 1689px;
|
||
top: 128px;
|
||
width: 1250px;
|
||
height: 895px;
|
||
background: url('/imgs/traffic/3-bg.png') no-repeat center center;
|
||
background-size: cover;
|
||
|
||
.chart-legend {
|
||
position: absolute;
|
||
top: 64px;
|
||
left: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
color: #ffffff;
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
line-height: 1;
|
||
}
|
||
|
||
.legend-item {
|
||
position: relative;
|
||
padding-left: 18px;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 0;
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
&--actual::before {
|
||
background: #20d8ff;
|
||
}
|
||
|
||
&--compare::before {
|
||
background: #f5c400;
|
||
}
|
||
|
||
&--history::before {
|
||
background: #ff1d1d;
|
||
}
|
||
}
|
||
|
||
.legend-unit {
|
||
margin-left: -12px;
|
||
}
|
||
|
||
.line-chart-grid {
|
||
position: absolute;
|
||
top: 96px;
|
||
right: 23px;
|
||
bottom: 22px;
|
||
left: 23px;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||
gap: 27px 26px;
|
||
}
|
||
|
||
.line-chart-card {
|
||
position: relative;
|
||
overflow: hidden;
|
||
background: rgba(27, 83, 165, 0.25);
|
||
|
||
}
|
||
|
||
.line-chart-header {
|
||
position: absolute;
|
||
top: 36px;
|
||
left: 20px;
|
||
right: 22px;
|
||
z-index: 2;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.line-badge {
|
||
width: 39px;
|
||
height: 39px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 6px;
|
||
font-size: 30px;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
}
|
||
|
||
.line-title {
|
||
margin-left: 21px;
|
||
color: #ffffff;
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
line-height: normal;
|
||
}
|
||
|
||
.total-digits {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
margin-left: 47px;
|
||
}
|
||
|
||
.digit-box {
|
||
width: 39px;
|
||
height: 44px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
color: #ffffff;
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
background: url('/imgs/traffic/3-item.png') no-repeat center center;
|
||
background-size: contain;
|
||
}
|
||
|
||
.digit-odometer {
|
||
width: 39px;
|
||
height: 44px;
|
||
display: block;
|
||
color: #ffffff;
|
||
font-family: inherit;
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
line-height: 44px;
|
||
text-align: center;
|
||
}
|
||
|
||
.digit-odometer :deep(.odometer-digit),
|
||
.digit-odometer :deep(.odometer-digit-spacer) {
|
||
width: 39px;
|
||
height: 44px;
|
||
line-height: 44px;
|
||
}
|
||
|
||
.digit-odometer :deep(.odometer-digit-inner) {
|
||
width: 39px;
|
||
height: 44px;
|
||
line-height: 44px;
|
||
}
|
||
|
||
.digit-odometer :deep(.odometer-value) {
|
||
width: 39px;
|
||
height: 44px;
|
||
line-height: 44px;
|
||
text-align: center;
|
||
}
|
||
|
||
.line-chart {
|
||
position: absolute;
|
||
right: 0;
|
||
bottom: 14px;
|
||
left: 10px;
|
||
height: 238px;
|
||
}
|
||
}
|
||
</style>
|