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

468 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">对比日{{ compareDateLabel }}</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'
import server from '@/utils/service'
import { processYunyingInfo } from '@/utils/yunying'
import { pointGlowSymbolBase64 } from '../assets/4-item.base64.js'
const hours = ['5', '7', '9', '11', '13', '15', '17', '19', '21', '23']
const pointGlowSymbol = `image://${pointGlowSymbolBase64}`
const lineCards = ref([])
const compareDate = ref('')
const compareDateLabel = computed(() => {
return compareDate.value ? `(${compareDate.value})` : ''
})
const fetchLinePassengerFlow = async () => {
try {
const response = await server.getLinePassengerFlow()
if (response.data.success) {
const processed = processYunyingInfo(response.data.data)
compareDate.value = processed[0]?.compareDate || ''
lineCards.value = processed.map(item => ({
lineId: item.lineId,
total: String(Math.floor(item.totalSum || 0)),
actual: item.actual,
compare: item.compare,
history: item.history
}))
}
} catch (error) {
console.error('获取线路客流信息失败:', error)
}
}
const chartCards = computed(() => {
return lineCards.value.map(item => ({
...item,
option: createChartOption(item)
}))
})
const targetTotals = computed(() => lineCards.value.map(item => String(item.total || '0').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 fetchLinePassengerFlow()
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 => {
const maxValue = Math.max(...item.actual, ...item.compare, ...item.history, 0)
const yAxisMax = Math.ceil(maxValue / 10000) * 10000 || 10000
return {
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: yAxisMax,
interval: yAxisMax / 5,
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: 'rgba(226, 239, 255, 0.96)',
fontSize: 13,
fontWeight: 600,
margin: 10,
formatter: value => (value / 10000).toFixed(0)
},
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: compareDateLabel.value || '对比日',
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);
&::after {
content: '';
position: absolute;
left: 44%;
bottom: 37px;
width: 459px;
height: 50px;
background: url('/imgs/traffic/3-item1.png') no-repeat center center;
background-size: 459px 50px;
transform: translateX(-50%);
pointer-events: none;
z-index: 0;
}
}
.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;
z-index: 1;
}
}
</style>