yunsan-pc/src/views/traffic/components/Traffic3.vue

454 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>