Compare commits

...

2 Commits

Author SHA1 Message Date
崔方鑫 763956d7ed Merge branch 'main' of https://git.deploy.top/wyh/yunsan-pc 2026-07-27 15:59:37 +08:00
崔方鑫 becf5a8190 添加行车静态页面;添加菜单 2026-07-27 15:53:36 +08:00
33 changed files with 1922 additions and 112 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 307 KiB

View File

@ -33,6 +33,18 @@ const routes = [
name: 'Traffic',
component: () => import('@/views/traffic/index.vue'),
meta: { title: '客运', requiresAuth: true }
},
{
path: 'driving',
name: 'Driving',
component: () => import('@/views/driving/index.vue'),
meta: { title: '行车', requiresAuth: true }
},
{
path: 'construction',
name: 'Construction',
component: () => import('@/views/construction/index.vue'),
meta: { title: '施工', requiresAuth: true }
}
],
meta: { requiresAuth: true }

View File

@ -0,0 +1,12 @@
<template>
<div class="construction-container" />
</template>
<style lang="scss" scoped>
.construction-container {
width: 4800px;
height: 1514px;
position: relative;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,312 @@
<template>
<section class="driving-1">
<div class="column-select">
<MetroLineSelect v-model="activeLine" />
</div>
<div class="vehicle-table">
<div class="table-header table-grid">
<span>车站</span>
<span>图号</span>
<span>用车数</span>
<span>当前计划用车数</span>
<span>行车间隔</span>
</div>
<el-scrollbar class="table-body">
<div class="table-body-inner">
<div v-for="item in vehicles" :key="item.id" class="table-row table-grid">
<span class="station">{{ item.station }}</span>
<span class="diagram">{{ item.diagram }}</span>
<span class="vehicles">{{ item.vehicles }}</span>
<span class="plan">
{{ item.plan }}<b :class="item.change > 0 ? 'rise' : 'fall'">{{ formatChange(item.change) }}</b>
</span>
<el-popover placement="top" trigger="hover" :width="560" :offset="34" :fallback-placements="[]"
:teleported="false" :show-after="150" :hide-after="80" popper-class="driving-row-popover">
<template #reference>
<div class="intervals">
<span class="interval-route">{{ item.interval.section }}</span>
<div class="interval-directions">
<div class="interval-direction">
<img src="/imgs/driving/1-item-1.png" alt="上行" />
<span>{{ item.interval.up }}</span>
</div>
<div class="interval-direction">
<img src="/imgs/driving/1-item-2.png" alt="下行" />
<span>{{ item.interval.down }}</span>
</div>
</div>
</div>
</template>
<div class="row-popover-content">
<div class="popover-overview">{{ intervalPopover.overview }}</div>
<div v-for="row in intervalPopover.rows" :key="row.section" class="popover-interval">
<span class="popover-section">{{ row.section }}</span>
<span class="popover-time"><img src="/imgs/driving/1-item-1.png" alt="上行" />{{ row.up }}</span>
<span class="popover-time"><img src="/imgs/driving/1-item-2.png" alt="下行" />{{ row.down }}</span>
</div>
</div>
</el-popover>
</div>
</div>
</el-scrollbar>
</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
import MetroLineSelect from './MetroLineSelect.vue'
const activeLine = ref(7)
const vehicles = [
{ id: 1, station: '上海南站', diagram: '1706-2', vehicles: 54, plan: 4, change: 2, interval: { section: '东川路站~奉贤新城站', up: '3分45秒', down: '2分13秒' } },
{ id: 2, station: '石龙路', diagram: '1706-2', vehicles: 54, plan: 4, change: 1, interval: { section: '嘉定新城站~南翔站', up: '3分45秒', down: '2分13秒' } },
{ id: 3, station: '龙漕路', diagram: '1706-2', vehicles: 54, plan: 4, change: -1, interval: { section: '三林站~罗山路站', up: '3分45秒', down: '2分13秒' } },
{ id: 4, station: '漕溪路', diagram: '1706-2', vehicles: 54, plan: 4, change: 2, interval: { section: '花桥站~嘉定新城站', up: '3分45秒', down: '2分13秒' } },
{ id: 5, station: '龙漕路', diagram: '1706-2', vehicles: 54, plan: 4, change: -1, interval: { section: '嘉定北站~嘉定新城站', up: '3分45秒', down: '2分13秒' } },
{ id: 6, station: '漕溪路', diagram: '1706-2', vehicles: 54, plan: 4, change: 2, interval: { section: '罗山路站~迪士尼站', up: '3分45秒', down: '2分13秒' } },
{ id: 7, station: '龙漕路', diagram: '1706-2', vehicles: 54, plan: 4, change: -1, interval: { section: '东川路站~闵行开发区站', up: '7分30秒', down: '5分20秒' } },
{ id: 8, station: '漕溪路', diagram: '1706-2', vehicles: 54, plan: 4, change: 2, interval: { section: '嘉定新城站~南翔站', up: '3分45秒', down: '2分13秒' } },
{ id: 9, station: '漕溪路', diagram: '1706-2', vehicles: 54, plan: 4, change: 2, interval: { section: '嘉定新城站~南翔站', up: '3分45秒', down: '2分13秒' } },
{ id: 10, station: '漕溪路', diagram: '1706-2', vehicles: 54, plan: 4, change: 2, interval: { section: '嘉定新城站~南翔站', up: '3分45秒', down: '2分13秒' } }
]
const intervalPopover = {
overview: '东川路站~闵行开发区站平均7分30秒(3分~5分)',
rows: [
{ section: '嘉定新城站~南翔站:', up: '平均3分45秒', down: '平均2分13秒' },
{ section: '三林站~罗山路站:', up: '平均3分45秒', down: '平均2分13秒' },
{ section: '花桥站~嘉定新城站:', up: '平均3分45秒', down: '平均2分13秒' },
{ section: '嘉定北~嘉定新城站:\n罗山路站~迪士尼站:', up: '平均3分45秒', down: '平均2分13秒' }
]
}
const formatChange = value => `${value > 0 ? '+' : '-'}${Math.abs(value)}`
</script>
<style lang="scss" scoped>
.driving-1 {
position: absolute;
left: 11px;
top: 123.99px;
width: 1255.52px;
height: 895.21px;
color: #d9ebff;
background: url('/imgs/driving/1-bg.png') no-repeat center center;
background-size: cover;
overflow: hidden;
}
.column-select {
position: absolute;
top: 61px;
right: 27px;
z-index: 2;
}
.vehicle-table {
position: absolute;
top: 115px;
right: 17px;
bottom: 27px;
left: 17px;
display: flex;
min-height: 0;
flex-direction: column;
}
.table-grid {
display: grid;
grid-template-columns: 220px 145px 115px 145px minmax(0, 1fr);
align-items: center;
}
.table-header {
height: 46px;
padding: 0 15px;
color: #FFFFFF;
font-size: 24px;
text-align: center;
background: linear-gradient(90deg, #0094ff00 -7%, #0085ff4d 50%, #0047ff00 99%), rgba(0, 87, 255, 0.05);
span {
text-align: center;
white-space: nowrap;
}
}
.table-body {
min-height: 0;
flex: 1;
}
.table-body-inner {
display: flex;
flex-direction: column;
gap: 16px;
padding-top: 16px;
padding-right: 4px;
}
.table-row {
flex: 0 0 74px;
height: 66px;
line-height: 66px;
padding: 0 15px;
font-size: 20px;
text-align: center;
cursor: default;
}
.station {
background: linear-gradient(114deg, #3485ff33 4%, #3e8bff20 44%, #2eadff10 104%);
}
.diagram {
font-size: 23px;
color: #19CFD6;
background: linear-gradient(130deg, #3485ff32 3%, #3e8bff26 47%, #2eadff1d 112%);
}
.vehicles {
font-size: 25px;
font-weight: normal;
background: linear-gradient(131deg, #3485ff32 3%, #3e8bff26 47%, #2eadff1d 113%);
}
.plan {
font-size: 25px;
background: linear-gradient(131deg, #3485ff32 3%, #3e8bff26 47%, #2eadff1d 113%);
}
.rise,
.fall {
color: #FF0000;
}
.intervals {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) 170px;
min-width: 0;
align-items: center;
padding: 0 12px 0 16px;
cursor: default;
text-align: left;
background: linear-gradient(99deg, #3485ff32 5%, #3e8bff26 43%, #2eadff0e 101%);
&:hover {
background: linear-gradient(90deg, rgba(35, 113, 211, .96), rgba(24, 91, 178, .88));
box-shadow: inset 0 0 20px rgba(58, 174, 255, .22);
}
}
.interval-route {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.interval-directions {
min-width: 0;
}
.interval-direction {
display: flex;
align-items: center;
height: 32px;
white-space: nowrap;
img {
width: 24px;
height: 24px;
margin-right: 10px;
object-fit: contain;
}
}
</style>
<style lang="scss">
.el-popper.driving-row-popover {
padding: 13px 31px 15px;
border: 1px solid #087bce;
border-radius: 9px;
color: #dce9fa;
background: rgba(2, 22, 61, .98);
box-shadow: inset 0 0 18px rgba(0, 106, 213, .1), 0 5px 14px rgba(0, 8, 32, .32);
.el-popper__arrow {
display: none;
}
&[data-popper-placement^='top']::before,
&[data-popper-placement^='top']::after {
position: absolute;
left: 77px;
content: '';
clip-path: polygon(0 0, 100% 0, 0 100%);
}
&[data-popper-placement^='top']::before {
bottom: -40px;
width: 103px;
height: 41px;
background: #087bce;
}
&[data-popper-placement^='top']::after {
bottom: -37px;
left: 78px;
width: 99px;
height: 38px;
background: #02163d;
}
}
.row-popover-content {
font-size: 17px;
line-height: 24px;
}
.popover-overview {
height: 31px;
white-space: nowrap;
}
.popover-interval {
display: grid;
grid-template-columns: 200px 149px 149px;
min-height: 43px;
align-items: center;
}
.popover-section {
padding-right: 10px;
white-space: pre-line;
}
.popover-time {
display: flex;
align-items: center;
white-space: nowrap;
img {
flex: 0 0 24px;
width: 24px;
height: 24px;
margin-right: 9px;
object-fit: contain;
}
}
</style>

View File

@ -0,0 +1,138 @@
<template>
<section class="driving-2">
<div class="column-select">
<MetroLineSelect v-model="activeLine" />
</div>
<div class="station-table">
<div class="table-header table-grid">
<span>车站</span>
<span>供电</span>
<span>车辆</span>
<span>车队</span>
<span>通号</span>
<span>保护区</span>
<span>轨行区</span>
</div>
<el-scrollbar class="table-body">
<div class="table-body-inner">
<div v-for="item in stationRanks" :key="item.id" class="table-row table-grid">
<span class="station">{{ item.station }}</span>
<span>{{ item.power }}</span>
<span>{{ item.vehicle }}</span>
<span>{{ item.fleet }}</span>
<span>{{ item.signal }}</span>
<span>{{ item.protection }}</span>
<span>{{ item.track }}</span>
</div>
</div>
</el-scrollbar>
</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
import MetroLineSelect from './MetroLineSelect.vue'
const activeLine = ref(7)
const stationRanks = [
{ id: 1, station: '上海南站', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 },
{ id: 2, station: '石龙路', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 },
{ id: 3, station: '龙漕路', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 },
{ id: 4, station: '漕溪路', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 },
{ id: 5, station: '龙漕路', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 },
{ id: 6, station: '虹桥火车站', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 },
{ id: 7, station: '龙漕路', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 },
{ id: 8, station: '龙漕路', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 },
{ id: 9, station: '龙漕路', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 },
{ id: 10, station: '龙漕路', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 },
{ id: 11, station: '龙漕路', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 },
{ id: 12, station: '龙漕路', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 },
{ id: 13, station: '龙漕路', power: 23, vehicle: 56, fleet: 45, signal: 7, protection: 46, track: 5 }
]
</script>
<style lang="scss" scoped>
.driving-2 {
position: absolute;
left: 1277.92px;
top: 123.99px;
width: 865.4px;
height: 894px;
color: #d9ebff;
background: url('/imgs/driving/2-bg.png') no-repeat center center;
background-size: cover;
overflow: hidden;
}
.column-select {
position: absolute;
top: 61px;
right: 27px;
z-index: 2;
}
.station-table {
position: absolute;
top: 115px;
right: 13px;
bottom: 22px;
left: 13px;
display: flex;
min-height: 0;
flex-direction: column;
}
.table-grid {
display: grid;
grid-template-columns: 130px repeat(6, minmax(0, 1fr));
align-items: center;
}
.table-header {
flex: 0 0 49px;
padding: 0 11px;
color: #FFFFFF;
font-size: 24px;
text-align: center;
background: linear-gradient(90deg, #0094ff00 -7%, #0085ff4d 50%, #0047ff00 99%), rgba(0, 87, 255, 0.05);
span:first-child {
text-align: left;
}
}
.table-body {
min-height: 0;
flex: 1;
}
.table-body-inner {
display: flex;
flex-direction: column;
gap: 16px;
padding-top: 16px;
}
.table-row {
flex: 0 0 48px;
height: 48px;
padding: 0 11px;
border-bottom: 1px solid rgba(0, 138, 255, .72);
font-size: 20px;
text-align: center;
background: linear-gradient(90deg, #0094ff00 -7%, #0085ff4d 50%, #0047ff00 99%), rgba(0, 87, 255, 0.05);
}
.station {
overflow: hidden;
font-weight: 700;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<section class="driving-3">
<img class="rotating-visual" src="/imgs/driving/3-item-1.png" alt="" />
<span v-for="item in smallBallCounts" :key="item.name" class="small-ball-count"
:style="{ left: `${item.left}px`, top: `${item.top}px`, fontSize: `${item.fontSize}px` }">{{
constructionStats[item.key] }}</span>
<div class="total-count">
<span>总施工量</span>
<strong>{{ formatNumber(constructionStats.total) }}</strong>
</div>
</section>
</template>
<script setup>
import { reactive } from 'vue'
const constructionStats = reactive({
protection: 6,
database: 6,
signal: 183,
generalAffairs: 19,
power: 82,
vehicle: 1,
track: 20,
fleet: 30,
total: 63935
})
const smallBallCounts = [
{ name: '保护区', key: 'protection', left: 125, top: 105, fontSize: 30 },
{ name: '数据库', key: 'database', left: 354, top: 121, fontSize: 38 },
{ name: '通号', key: 'signal', left: 541, top: 158, fontSize: 34 },
{ name: '总务', key: 'generalAffairs', left: 787, top: 108, fontSize: 30 },
{ name: '供电', key: 'power', left: 190, top: 278, fontSize: 38 },
{ name: '车辆', key: 'vehicle', left: 700, top: 256, fontSize: 38 },
{ name: '轨行区', key: 'track', left: 117, top: 486, fontSize: 34 },
{ name: '车队', key: 'fleet', left: 765, top: 480, fontSize: 36 }
]
const formatNumber = value => Number(value ?? 0).toLocaleString('en-US')
</script>
<style lang="scss" scoped>
.driving-3 {
position: absolute;
left: 2155.13px;
top: 123.99px;
width: 866.87px;
height: 926.8px;
opacity: 1;
display: flex;
flex-direction: column;
background: url('/imgs/driving/3-bg.png') no-repeat center center;
background-size: cover;
}
.rotating-visual {
position: absolute;
top: 328px;
left: 250px;
width: 380px;
height: 379px;
object-fit: contain;
transform-origin: center;
animation: driving-3-rotate 18s linear infinite;
pointer-events: none;
}
.small-ball-count {
position: absolute;
z-index: 2;
color: #fff;
font-weight: 700;
line-height: 1;
text-align: center;
text-shadow: 0 2px 4px rgba(0, 38, 118, .7);
transform: translateX(-50%);
white-space: nowrap;
pointer-events: none;
}
.total-count {
position: absolute;
top: 451px;
left: 250px;
z-index: 2;
display: flex;
width: 380px;
flex-direction: column;
align-items: center;
color: #fff;
text-align: center;
pointer-events: none;
span {
font-size: 42px;
line-height: 52px;
text-shadow: 0px 0px 10px rgba(30, 121, 255, 0.8);
}
strong {
margin-top: 18px;
font-size: 50px;
font-weight: 600;
line-height: 62px;
text-shadow: 0px 1px 4px rgba(7, 44, 137, 0.5);
}
}
@keyframes driving-3-rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@ -0,0 +1,20 @@
<template>
<DrivingChartPanel
v-model:line="line"
v-model:date="date"
:chart-data="chartData"
/>
</template>
<script setup>
import { ref } from 'vue'
import DrivingChartPanel from './DrivingChartPanel.vue'
const line = ref(7)
const date = ref('2026-06-30')
const chartData = {
dates: ['01/21', '01/22', '01/23', '01/24', '01/25', '01/26'],
current: [98.8, 100.15, 101.3, 99.95, 100.42, 99.35],
previous: [98.55, 99.7, 100.08, 99.68, 100.88, 99.32]
}
</script>

View File

@ -0,0 +1,203 @@
<template>
<section class="driving-5">
<div class="metric-cards">
<article v-for="card in metricCards" :key="card.key" class="metric-card"
:class="`metric-card--${card.type}`">
<h3>{{ card.title }}</h3>
<div class="primary-value">
<strong>{{ card.value }}</strong>
<span v-if="card.unit">{{ card.unit }}</span>
</div>
<div class="secondary-value">
<span>{{ card.secondaryLabel }}</span>
<strong>{{ card.secondaryValue }}</strong>
<small v-if="card.secondaryUnit">{{ card.secondaryUnit }}</small>
</div>
</article>
</div>
</section>
</template>
<script setup>
import { reactive } from 'vue'
const metricCards = reactive([
{
key: 'assignedMileage',
type: 'basic',
title: '配属车辆',
value: '774',
unit: '公里',
secondaryLabel: '车站数',
secondaryValue: '460',
secondaryUnit: '座'
},
{
key: 'assignedVehicles',
type: 'basic',
title: '配属车辆',
value: '7154',
unit: '辆',
secondaryLabel: '列车数',
secondaryValue: '1116',
secondaryUnit: '座'
},
{
key: 'headway',
type: 'basic',
title: '行车间隔',
value: '150″',
unit: '',
secondaryLabel: '中心城区',
secondaryValue: '150″',
secondaryUnit: ''
},
{
key: 'dailyPassengerFlow',
type: 'gauge',
title: '日均客流',
value: '965.5',
unit: '万人',
secondaryLabel: '目标值',
secondaryValue: '915',
secondaryUnit: '万人'
},
{
key: 'fiveMinuteDelay',
type: 'gauge',
title: "5晚点",
value: '940',
unit: '万车公里',
secondaryLabel: '目标值',
secondaryValue: '750',
secondaryUnit: ''
},
{
key: 'punctualityRate',
type: 'gauge',
title: '正点率',
value: '99.9',
unit: '%',
secondaryLabel: '目标值',
secondaryValue: '99.7',
secondaryUnit: '%'
}
])
</script>
<style lang="scss" scoped>
.driving-5 {
position: absolute;
left: 3856.58px;
top: 128px;
width: 920.56px;
height: 717.6px;
color: #fff;
font-family: 'AlibabaPuHuiTi', 'Microsoft YaHei', sans-serif;
background: url('/imgs/driving/5-bg.png') no-repeat center center;
background-size: cover;
overflow: hidden;
}
.metric-cards {
position: absolute;
top: 128px;
left: 46px;
display: grid;
grid-template-columns: repeat(3, 234px);
grid-auto-rows: 229px;
gap: 54px 63px;
}
.metric-card {
position: relative;
width: 234px;
height: 229px;
text-align: center;
background-repeat: no-repeat;
background-position: center;
background-size: 100% 100%;
&--basic {
background-image: url('/imgs/driving/5-item-1.png');
}
&.metric-card--gauge {
background-image: url('/imgs/driving/5-item-2.png');
h3 {
top: 14px;
}
.primary-value {
top: 118px;
}
.secondary-value {
top: 170px;
}
}
h3 {
position: absolute;
top: 17px;
left: 0;
width: 100%;
margin: 0;
font-size: 28px;
line-height: normal;
letter-spacing: 0;
}
}
.primary-value {
position: absolute;
top: 91px;
left: 0;
display: flex;
width: 100%;
align-items: baseline;
justify-content: center;
white-space: nowrap;
strong {
font-size: 42px;
font-weight: 700;
line-height: 48px;
letter-spacing: 0;
text-shadow: 0 3px 5px rgba(0, 22, 79, .68);
}
span {
margin-left: 3px;
font-size: 20px;
}
}
.secondary-value {
position: absolute;
top: 158px;
left: 0;
display: flex;
width: 100%;
align-items: baseline;
justify-content: center;
font-size: 24px;
line-height: normal;
white-space: nowrap;
strong {
margin-left: 13px;
font-size: 24px;
font-weight: 700;
}
small {
margin-left: 3px;
font-size: 14px;
}
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<section class="driving-6">
</section>
</template>
<script setup>
import { ref } from 'vue'
</script>
<style lang="scss" scoped>
.driving-6 {
position: absolute;
left: 16px;
top: 1052px;
width: 3009px;
height: 535px;
opacity: 1;
display: flex;
flex-direction: column;
background: url('/imgs/driving/6-bg.png') no-repeat center center;
background-size: cover;
backdrop-filter: blur(10px);
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<section class="driving-7">
<DrivingChartPanel
class="driving-7-panel"
v-model:line="line"
v-model:date="date"
:chart-data="chartData"
/>
</section>
</template>
<script setup>
import { ref } from 'vue'
import DrivingChartPanel from './DrivingChartPanel.vue'
const line = ref(7)
const date = ref('2026-06-30')
const chartData = {
dates: ['01/21', '01/22', '01/23', '01/24', '01/25', '01/26'],
current: [98.8, 100.15, 101.3, 99.95, 100.42, 99.35],
previous: [98.55, 99.7, 100.08, 99.68, 100.88, 99.32]
}
</script>
<style lang="scss" scoped>
.driving-7 {
position: absolute;
left: 3035px;
top: 860.63px;
width: 805px;
height: 726.37px;
opacity: 1;
display: flex;
flex-direction: column;
background: url('/imgs/driving/7-bg.png') no-repeat center center;
background-size: cover;
}
.driving-7-panel {
position: absolute !important;
inset: 0 !important;
width: 100% !important;
height: 100% !important;
background: transparent !important;
}
</style>

View File

@ -0,0 +1,252 @@
<template>
<section class="driving-8">
<header class="toolbar"><span>对比日期:</span><select v-model="state.date">
<option>{{ state.date }}</option>
</select><select v-model="state.period">
<option>{{ state.period }}</option>
</select></header>
<div class="summary">
<div class="row"><b>最大满载区间</b><span>{{ state.maxSection }} <i>{{ state.loadRate }}%</i></span></div>
<div class="row"><b>运行图图号</b><span>实际 <i>{{ state.actualTrips }}</i><em>|</em> 计划 <strong>{{
state.planTrips }}</strong></span></div>
<div class="row"><b>备车情况</b><span>{{ state.spare }}</span></div>
<div class="row"><b>行车间隔</b><span>高峰: <span style="margin-right: 20px;">{{ state.peakInterval }}min
</span>平峰: <span>{{
state.offPeakInterval }}min</span></span></div>
</div>
<div class="route">
<div class="route-line">
<label>西岑</label>
<span class="station mid">淀山湖大道</span>
<span class="station end">虹桥火车站</span>
<small class="time t1">6分钟</small>
<small class="time t2">3分钟</small>
</div>
<div class="flow f1">{{ state.flowLeft }}/小时</div>
<div class="flow f2">{{ state.flowRight }}/小时</div>
</div>
</section>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({ date: '20251113', period: '早高峰时段', maxSection: '虹桥火车站-国家会展中心', loadRate: 110, actualTrips: 23, planTrips: 24, spare: '东方绿洲/淀山湖', peakInterval: 2.5, offPeakInterval: 4, flowLeft: 225600, flowRight: 225600 })
</script>
<style lang="scss" scoped>
$panel-bg: rgba(4, 124, 235, 0.1);
$panel-radius: 6px;
$route-color: #00e9ed;
$tag-bg: rgba(34, 71, 122, 0.92);
$tag-border: #8d7c4e;
@mixin flex($align: center, $justify: flex-start, $direction: row) {
display: flex;
flex-direction: $direction;
align-items: $align;
justify-content: $justify;
}
@mixin glass-panel {
background: $panel-bg;
backdrop-filter: blur(10px);
border-radius: $panel-radius;
}
@mixin route-tag($top) {
position: absolute;
top: $top;
z-index: 2;
padding: 4px 16px;
color: inherit;
white-space: nowrap;
background: linear-gradient(130deg, #f5ff3442 2%, #ffb23e31 36%, #ff932e27 116%);
border: 1px solid;
border-image: linear-gradient(90deg, #ffd343b3 -3%, #ffc85b 51%, #ffcd43b3 102%) 1;
border-radius: 3px;
}
@mixin time-tag($top) {
position: absolute;
top: $top;
z-index: 2;
padding: 4px 16px;
color: inherit;
white-space: nowrap;
background: linear-gradient(122deg, #28426a 4%, #0d4061 108%);
border: 1px solid;
border-image: linear-gradient(90deg, #43aaffb3 -3%, #5bb5ff 51%, #43aaffb3 102%) 1;
border-radius: 3px;
}
.driving-8 {
@include flex(stretch, flex-start, column);
position: absolute;
top: 860.63px;
left: 3856.58px;
width: 920.56px;
height: 726.37px;
padding: 8px 32px 0;
color: #FFFFFF;
font: 22px;
background: url('/imgs/driving/8-bg.png') no-repeat center / cover;
.toolbar {
@include flex(center, flex-end);
height: 33px;
margin-top: 70px;
gap: 12px;
color: #dcecff;
font-size: 16px;
select {
height: 26px;
padding: 0 24px 0 10px;
appearance: none;
color: #e4f2ff;
font-size: 17px;
background-color: #2166a8;
background-image: linear-gradient(45deg, transparent 50%, #b8d9f7 50%),
linear-gradient(135deg, #b8d9f7 50%, transparent 50%);
background-repeat: no-repeat;
background-position: calc(100% - 13px) 10px, calc(100% - 8px) 10px;
background-size: 5px 5px;
border: 1px solid #4b9ee5;
border-radius: 2px;
}
}
.summary {
@include glass-panel;
@include flex(stretch, space-evenly, column);
height: 280px;
padding: 0 18px;
.row {
@include flex(center, space-between);
height: 53px;
padding: 0 18px;
margin-bottom: 1px;
background: linear-gradient(90deg, #0085ff00 0%, #00a3ff48 40%, #00a2ff48 70%, #0075ff0a 100%);
b {
font-size: 22px;
font-weight: 500;
letter-spacing: 0;
}
span {
// font-weight: 600;
font-size: 22px;
}
i,
strong {
padding: 3px 10px;
margin-left: 10px;
color: white;
font-style: normal;
background: #D72929;
border-radius: 4px;
font-weight: 500;
}
strong {
background: #008CFF;
font-weight: 500;
}
em {
margin: 0 10px;
color: #d6e6fa;
font-style: normal;
}
}
}
.route {
@include glass-panel;
position: relative;
height: 290px;
margin-top: 24px;
.route-line {
position: absolute;
top: 30px;
left: 44px;
width: 760px;
height: 187px;
border: 3px solid $route-color;
border-radius: 100px;
&::after {
position: absolute;
top: 45px;
left: 238px;
width: 512px;
height: 85px;
content: '';
border: 3px solid $route-color;
border-radius: inherit;
}
label {
@include route-tag(73px);
left: 40px;
}
.station {
@include route-tag(73px);
&.mid,
&.end {
width: 125px;
text-align: center;
}
&.mid {
left: 270px;
}
&.end {
right: 30px;
}
}
.time {
@include time-tag(169px);
padding: 3px 20px;
&.t1 {
left: 190px;
}
&.t2 {
left: 487px;
}
}
}
.flow {
@include time-tag(242px);
font-size: 18px;
&.f1 {
left: 198px;
}
&.f2 {
left: 492px;
}
}
}
}
</style>

View File

@ -0,0 +1,413 @@
<template>
<section class="driving-4">
<div class="filters">
<MetroLineSelect v-model="activeLine" />
<div class="date-select-shell">
<el-config-provider :locale="zhCn">
<el-date-picker
v-model="selectedDate"
class="date-select"
popper-class="driving-date-popper"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:clearable="false"
:editable="false"
/>
</el-config-provider>
</div>
</div>
<span class="chart-unit">单位100%</span>
<img
class="current-highlight"
src="/imgs/driving/4-item-1.png"
alt=""
aria-hidden="true"
:style="highlightStyle"
/>
<div class="current-value-labels" :style="highlightValueStyle">
<strong class="current-value-labels__current">{{ chartData.currentCount }}</strong>
<strong class="current-value-labels__previous">{{ chartData.previousCount }}</strong>
</div>
<v-chart class="punctuality-chart" :option="chartOption" autoresize @click="handleChartClick" />
<div class="chart-legend" aria-hidden="true">
<span class="legend-item legend-item--current">当前数据</span>
<span class="legend-item legend-item--previous">去年同期</span>
</div>
</section>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import VChart from 'vue-echarts'
import * as echarts from 'echarts'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import MetroLineSelect from './MetroLineSelect.vue'
const props = defineProps({
line: { type: Number, default: 7 },
date: { type: String, default: '2026-06-30' },
chartData: { type: Object, default: () => ({}) }
})
const emit = defineEmits(['update:line', 'update:date'])
const activeLine = computed({ get: () => props.line, set: value => emit('update:line', value) })
const selectedDate = computed({ get: () => props.date, set: value => emit('update:date', value) })
const currentIndex = ref(2)
const chartData = reactive({
dates: ['01/21', '01/22', '01/23', '01/24', '01/25', '01/26'],
current: [98.8, 100.15, 101.3, 99.95, 100.42, 99.35],
previous: [98.55, 99.7, 100.08, 99.68, 100.88, 99.32],
currentCount: 418,
previousCount: 101
})
Object.assign(chartData, props.chartData)
const summary = reactive({
current: '99.46',
month: '99.86',
year: '99.86',
yearTotal: '94.86'
})
const handleChartClick = params => {
if (!['series', 'xAxis'].includes(params.componentType)) return
const index = typeof params.dataIndex === 'number'
? params.dataIndex
: chartData.dates.indexOf(params.value)
if (index >= 0) currentIndex.value = index
}
const highlightStyle = computed(() => ({
left: `${74 + currentIndex.value * 135 - 28.5}px`
}))
const highlightValueStyle = computed(() => ({
left: `${74 + currentIndex.value * 135 - 36}px`
}))
const chartOption = computed(() => ({
animationDuration: 900,
backgroundColor: 'transparent',
grid: {
top: 78,
right: 56,
bottom: 82,
left: 74
},
tooltip: {
show: true,
showContent: false,
trigger: 'axis',
axisPointer: {
type: 'line',
lineStyle: { color: 'rgba(39, 220, 255, .75)', width: 1 }
}
},
xAxis: {
type: 'category',
boundaryGap: false,
triggerEvent: true,
data: chartData.dates,
axisLine: {
lineStyle: { color: '#d4e8ff', width: 1.5 }
},
axisTick: { show: false },
axisLabel: {
interval: 0,
margin: 18,
color: '#fff',
fontSize: 19,
fontWeight: 600,
formatter: (value, index) => index === currentIndex.value ? `{active|${value}}\n{current|当前}` : value,
rich: {
active: { color: '#158dff', fontSize: 19, fontWeight: 700, lineHeight: 26 },
current: { color: '#158dff', fontSize: 18, fontWeight: 600, lineHeight: 28 }
}
}
},
yAxis: {
type: 'value',
min: 97,
max: 101.5,
interval: 1,
axisLine: {
show: true,
symbol: ['none', 'arrow'],
symbolSize: [8, 10],
lineStyle: { color: '#d4e8ff', width: 1.5 }
},
axisTick: { show: false },
axisLabel: {
color: '#fff',
fontSize: 18,
fontWeight: 600,
margin: 16,
formatter: value => Number.isInteger(value) ? value : ''
},
splitLine: {
show: true,
lineStyle: { color: 'rgba(190, 218, 255, .32)', width: 1.5 }
}
},
series: [
{
name: '当前数据',
type: 'line',
data: chartData.current,
smooth: 0.45,
showSymbol: false,
z: 5,
lineStyle: {
color: '#28bfff',
width: 4,
shadowBlur: 8,
shadowColor: 'rgba(40, 191, 255, .5)'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(33, 184, 255, .58)' },
{ offset: 1, color: 'rgba(20, 107, 203, .08)' }
])
},
markPoint: {
silent: true,
symbol: 'none',
z: 20,
data: [
{
coord: [currentIndex.value, 101.22],
value: chartData.currentCount,
label: { color: '#00caff', fontSize: 34, fontWeight: 700, offset: [-36, -28] }
},
{
coord: [currentIndex.value, 99.2],
value: chartData.previousCount,
label: { color: '#ffd20b', fontSize: 34, fontWeight: 700, offset: [-36, 0] }
}
]
}
},
{
name: '去年同期',
type: 'line',
data: chartData.previous,
smooth: 0.45,
showSymbol: false,
z: 4,
lineStyle: { color: '#f2cb08', width: 4 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(238, 202, 15, .42)' },
{ offset: 1, color: 'rgba(238, 202, 15, .06)' }
])
}
},
{
name: '日期选择区域',
type: 'bar',
data: chartData.dates.map(() => 101.5),
barWidth: '100%',
silent: false,
z: 10,
tooltip: { show: false },
itemStyle: { color: 'transparent' },
emphasis: { itemStyle: { color: 'transparent' } }
}
]
}))
</script>
<style lang="scss" scoped>
.driving-4 {
position: absolute;
left: 3035px;
top: 123.99px;
width: 805px;
height: 717.6px;
color: #fff;
background: url('/imgs/driving/4-bg.png') no-repeat center center;
background-size: cover;
overflow: hidden;
}
.filters {
position: absolute;
top: 68px;
right: 24px;
z-index: 4;
display: flex;
align-items: center;
gap: 20px;
}
.date-select-shell {
position: relative;
width: 200px;
height: 43px;
border-radius: 4px;
background: linear-gradient(90deg, rgba(51, 99, 164, .9), rgba(9, 38, 92, .66));
--el-input-bg-color: transparent;
--el-fill-color-blank: transparent;
&::after {
position: absolute;
top: 50%;
right: 13px;
width: 16px;
height: 16px;
background: url('/imgs/driving/arrow.png') no-repeat center / contain;
content: '';
pointer-events: none;
transform: translateY(-50%);
}
}
:global(.driving-date-popper.el-picker__popper) {
border: 1px solid rgba(42, 154, 239, .7) !important;
border-radius: 4px;
background: rgba(4, 27, 72, .98) !important;
box-shadow: 0 10px 28px rgba(0, 8, 30, .55);
}
:global(.driving-date-popper .el-picker-panel),
:global(.driving-date-popper .el-date-picker__header),
:global(.driving-date-popper .el-picker-panel__content) {
background: transparent !important;
}
:global(.driving-date-popper .el-date-picker__header-label),
:global(.driving-date-popper .el-picker-panel__icon-btn) {
color: #e8f4ff !important;
font-size: 16px;
font-weight: 700;
}
:global(.driving-date-popper .el-date-table th) {
color: #75b9ed !important;
border-bottom-color: rgba(117, 185, 237, .2) !important;
}
:global(.driving-date-popper .el-date-table-cell__text) {
color: #dcecff !important;
}
:global(.driving-date-popper .el-date-table td.available:hover .el-date-table-cell__text),
:global(.driving-date-popper .el-date-table td.current:not(.disabled) .el-date-table-cell__text) {
color: #fff !important;
background: #1677d6 !important;
}
:global(.driving-date-popper .el-date-table td.today .el-date-table-cell__text) {
color: #39c5ff !important;
font-weight: 700;
}
// Element Plus applies the date editor outline on the outer input wrapper.
:global(.driving-4 .date-select.el-date-editor),
:global(.driving-4 .date-select.el-date-editor .el-input__wrapper),
:global(.driving-4 .date-select.el-date-editor.is-focus .el-input__wrapper),
:global(.driving-4 .date-select.el-date-editor:hover .el-input__wrapper) {
border: 0 !important;
box-shadow: none !important;
}
:global(.driving-4 .date-select .el-input__prefix) {
display: none !important;
}
:global(.driving-4 .date-select-shell .el-input),
:global(.driving-4 .date-select-shell .el-input__wrapper),
:global(.driving-4 .date-select-shell .el-input__inner) {
background-color: transparent !important;
background-image: none !important;
height: 44px !important;
font-size: 18px !important;
}
.chart-unit {
position: absolute;
top: 147px;
left: 33px;
z-index: 3;
font-size: 18px;
}
.punctuality-chart {
position: absolute;
top: 128px;
left: 0;
width: 100%;
height: 530px;
}
.current-highlight {
position: absolute;
top: 219px;
z-index: 2;
width: 57px;
height: 364px;
pointer-events: none;
}
.current-value-labels {
position: absolute;
top: 182px;
z-index: 6;
display: flex;
flex-direction: column;
align-items: center;
gap: 123px;
width: 72px;
pointer-events: none;
}
.current-value-labels strong { font-size: 34px; line-height: 1; }
.current-value-labels__current { color: #12bfff; }
.current-value-labels__previous { color: #ffd20b; }
.chart-legend {
position: absolute;
bottom: 38px;
left: 0;
display: flex;
width: 100%;
justify-content: center;
gap: 38px;
font-size: 16px;
font-weight: 600;
}
.legend-item {
position: relative;
padding-left: 37px;
&::before {
position: absolute;
top: 50%;
left: 0;
width: 29px;
border-top: 4px solid;
content: '';
transform: translateY(-50%);
}
&--current {
color: #28bfff;
}
&--previous {
color: #f2cb08;
&::before {
border-top-style: dotted;
}
}
}
</style>

View File

@ -0,0 +1,167 @@
<template>
<div ref="root" class="metro-line-select">
<button class="select-trigger" type="button" aria-haspopup="listbox" :aria-expanded="open" @click="open = !open"
@keydown.down.prevent="moveSelection(1)" @keydown.up.prevent="moveSelection(-1)" @keydown.esc="open = false">
<span class="selected-line" :style="lineStyle(selectedLine)">
<strong v-if="isNumberedLine(selectedLine)">{{ selectedLine.id }}</strong>
<span>{{ isNumberedLine(selectedLine) ? '号线' : selectedLine.name }}</span>
</span>
<img class="arrow" :class="{ open }" src="/imgs/driving/arrow.png" alt="" aria-hidden="true" />
</button>
<Transition name="line-options">
<ul v-if="open" class="options" role="listbox">
<li v-for="line in lineOptions" :key="line.id" :class="{ active: line.id === modelValue }" role="option"
:aria-selected="line.id === modelValue" @click="selectLine(line.id)">
<span class="line-swatch" :style="lineStyle(line)">{{ line.name }}</span>
</li>
</ul>
</Transition>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { METRO_LINES } from '@/utils/const.js'
const props = defineProps({
modelValue: { type: Number, default: 7 }
})
const emit = defineEmits(['update:modelValue', 'change'])
const root = ref(null)
const open = ref(false)
const lineOptions = Object.entries(METRO_LINES).map(([id, theme]) => ({
id: Number(id),
...theme
}))
const selectedLine = computed(() => (
lineOptions.find(line => line.id === props.modelValue) || lineOptions[0]
))
const lineStyle = line => ({ backgroundColor: line.bg, color: line.color })
const isNumberedLine = line => line.id >= 1 && line.id <= 18
const selectLine = id => {
emit('update:modelValue', id)
emit('change', id)
open.value = false
}
const moveSelection = offset => {
open.value = true
const currentIndex = lineOptions.findIndex(line => line.id === props.modelValue)
const nextIndex = (currentIndex + offset + lineOptions.length) % lineOptions.length
selectLine(lineOptions[nextIndex].id)
}
const closeOnOutsideClick = event => {
if (!root.value?.contains(event.target)) open.value = false
}
onMounted(() => document.addEventListener('pointerdown', closeOnOutsideClick))
onBeforeUnmount(() => document.removeEventListener('pointerdown', closeOnOutsideClick))
</script>
<style lang="scss" scoped>
.metro-line-select {
position: relative;
width: 154px;
}
.select-trigger {
display: flex;
align-items: center;
width: 100%;
height: 43px;
padding: 0 13px 0 18px;
border: 0;
border-radius: 4px;
background: linear-gradient(90deg, rgba(51, 99, 164, .9), rgba(9, 38, 92, .66));
cursor: pointer;
}
.selected-line {
display: inline-flex;
// align-items: center;
justify-content: center;
gap: 4px;
min-width: 78px;
height: 33px;
padding: 0 9px;
border-radius: 6px;
font-size: 13px;
line-height: 1.6;
white-space: nowrap;
strong {
font-size: 30px;
line-height: 1;
}
}
.arrow {
width: 16px;
height: 16px;
margin-left: 12px;
object-fit: contain;
transition: transform .18s ease;
&.open {
transform: rotate(180deg);
}
}
.options {
position: absolute;
top: 47px;
right: 0;
z-index: 20;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
width: 238px;
max-height: 330px;
margin: 0;
padding: 9px;
overflow-y: auto;
border: 1px solid rgba(47, 156, 255, .7);
border-radius: 4px;
list-style: none;
background: rgba(3, 24, 68, .98);
box-shadow: 0 8px 24px rgba(0, 8, 30, .55);
}
.options li {
padding: 2px;
border: 1px solid transparent;
border-radius: 3px;
cursor: pointer;
&:hover,
&.active {
border-color: #60d9ff;
}
}
.line-swatch {
display: block;
height: 30px;
border-radius: 3px;
font-size: 14px;
line-height: 30px;
text-align: center;
white-space: nowrap;
}
.line-options-enter-active,
.line-options-leave-active {
transition: opacity .15s ease, transform .15s ease;
}
.line-options-enter-from,
.line-options-leave-to {
opacity: 0;
transform: translateY(-5px);
}
</style>

View File

@ -0,0 +1,37 @@
<script setup lang="ts">
import Driving1 from './components/Driving1.vue';
import Driving2 from './components/Driving2.vue';
import Driving3 from './components/Driving3.vue';
import Driving4 from './components/Driving4.vue';
import Driving5 from './components/Driving5.vue';
import Driving6 from './components/Driving6.vue';
import Driving7 from './components/Driving7.vue';
import Driving8 from './components/Driving8.vue';
</script>
<template>
<div class="driving-container">
<Driving1 />
<Driving2 />
<Driving3 />
<Driving4 />
<Driving5 />
<Driving6 />
<Driving7 />
<Driving8 />
</div>
</template>
<style lang="scss" scoped>
.driving-container {
width: 4800px;
height: 1620px;
position: absolute;
left: 0;
bottom: 0;
overflow: hidden;
}
</style>

View File

@ -1,38 +1,18 @@
<template>
<div class="dashboard-header">
<nav class="layout-menu" aria-label="">
<RouterLink v-for="item in menuItems" :key="item.to" :to="item.to" class="layout-menu__item">
<span>{{ item.label }}</span>
</RouterLink>
<div class="weather-info">
<el-icon class="weather-icon"><Pouring /></el-icon>
<span class="weather-temperature">{{ temperatureRange }}</span>
<span class="weather-name">{{ weatherInfo.weather || '--' }}</span>
</div>
</nav>
<div class="header-title">
<img src="/imgs/title.png" alt="title" class="title-img" />
</div>
<div class="function-switch">
<button class="switch-btn" @click="goHome">
<img src="/imgs/Group607.png" alt="home" />
</button>
<button class="switch-btn" @click="goTraffic">
<img src="/imgs/Group608.png" alt="traffic" />
</button>
</div>
<div class="weather-info">
<div class="weather-item">
<span class="label">天气</span>
<span class="value">{{ weatherInfo.weather || '--' }}</span>
</div>
<div class="weather-item">
<span class="label">温度</span>
<span class="value">{{ weatherInfo.tem || '--' }}</span>
</div>
<!-- <div class="weather-item">
<span class="label">风速</span>
<span class="value">{{ weatherInfo.wind_speed || '--' }}m/s</span>
</div>
<div class="weather-item">
<span class="label">湿度</span>
<span class="value">{{ weatherInfo.rh || '--' }}%</span>
</div>
<div class="weather-item">
<span class="label">能见度</span>
<span class="value">{{ weatherInfo.vis || '--' }}km</span>
</div> -->
</div>
<div class="header-time">{{ currentTime }}</div>
<div class="notice-marquee" v-if="noticeList.length > 0">
<div class="marquee-content" :class="{ 'marquee-scroll': noticeList.length > 1 }">
@ -46,6 +26,7 @@
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { Pouring } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import server from '@/utils/service'
@ -66,11 +47,25 @@ const currentNoticeIndex = ref(0)
let noticeTimer = null
let timeTimer = null
const menuItems = [
{ label: '常规', to: '/home' },
{ label: '客运', to: '/traffic' },
{ label: '行车', to: '/driving' },
{ label: '施工', to: '/construction' }
]
const displayNoticeList = computed(() => {
if (noticeList.value.length === 0) return []
return [noticeList.value[currentNoticeIndex.value]]
})
const temperatureRange = computed(() => {
const min = weatherInfo.value.tem_min ?? weatherInfo.value.min_tem ?? weatherInfo.value.temp_min ?? 21
const max = weatherInfo.value.tem_max ?? weatherInfo.value.max_tem ?? weatherInfo.value.temp_max
?? (weatherInfo.value.tem ? Math.round(Number(weatherInfo.value.tem)) : 27)
return `${min}°~${max}°`
})
const updateTime = () => {
const now = new Date()
currentTime.value = now.toLocaleString('zh-CN', {
@ -133,63 +128,109 @@ onUnmounted(() => {
<style lang="scss" scoped>
.dashboard-header {
width: 4800px;
height: 106px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: url('/imgs/top.png') no-repeat center center;
background-size: cover;
// border-bottom: 2px solid rgba(0, 212, 255, 0.5);
.header-title {
.title-img {
max-height: 100%;
max-width: 100%;
}
}
.header-time {
.dashboard-header {
position: absolute;
// right: 40px;
top: 110px;
left: 50%;
font-family: BlackOpsOne, sans-serif;
font-size: 44px;
color: #19EBFF;
text-align: center;
//
transform: translateX(-50%);
}
left: -2px;
top: -36px;
width: 4802px;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
background: url('/imgs/top.png') no-repeat center center;
background-size: 100% 100%;
// border-bottom: 2px solid rgba(0, 212, 255, 0.5);
.notice-marquee {
position: absolute;
right: 40px;
top: 55px;
width: 1300px;
height: 45px;
overflow: hidden;
.marquee-content {
.layout-menu {
position: absolute;
top: 82px;
left: 42px;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
.notice-item {
font-size: 40px;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
animation: marquee 20s linear infinite;
&__item {
display: flex;
align-items: center;
justify-content: center;
width: 214px;
height: 58px;
padding-bottom: 8px;
background: url('/imgs/layout/menu-bg.png') no-repeat center / 100% 100%;
text-decoration: none;
margin-left: -10px;
span {
color: #fff;
font-size: 36px;
font-weight: 500;
background: linear-gradient(180deg, #ffffff 53%, #8dc2ff 69%, #66b5ff 75%), #FFFFFF;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
&:hover {
filter: brightness(1.15);
}
&.router-link-active {
background-image: url('/imgs/layout/menu-bg-active.png');
color: #eafaff;
}
}
}
}
@keyframes marquee {
0% {
transform: translateX(100%);
.header-title {
.title-img {
margin-top: 18px;
max-height: 100%;
max-width: 100%;
}
}
.header-time {
position: absolute;
// right: 40px;
top: 148px;
left: 50%;
font-family: BlackOpsOne, sans-serif;
font-size: 44px;
color: #19EBFF;
text-align: center;
//
transform: translateX(-50%);
}
.notice-marquee {
position: absolute;
right: 40px;
top: 55px;
width: 1300px;
height: 45px;
overflow: hidden;
.marquee-content {
display: flex;
flex-direction: column;
.notice-item {
font-size: 40px;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
animation: marquee 20s linear infinite;
}
}
}
@keyframes marquee {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
@ -224,29 +265,29 @@ onUnmounted(() => {
}
.weather-info {
position: absolute;
left: 1000px;
top: 70%;
transform: translateY(-50%);
display: flex;
gap: 20px;
align-items: center;
height: 58px;
margin-left: 46px;
gap: 14px;
white-space: nowrap;
color: #fff;
.weather-item {
display: flex;
align-items: center;
gap: 8px;
line-height: 45px;
.weather-icon {
width: 40px;
height: 40px;
color: #82f6ff;
font-size: 40px;
filter: drop-shadow(0 0 6px rgba(70, 224, 255, 0.75));
}
.label {
font-size: 36px;
color: #FFF;
}
.value {
font-size: 36px;
color: #fff;
}
.weather-temperature,
.weather-name {
font-size: 30px;
font-weight: 500;
line-height: 58px;
text-shadow: 0px 0px 10px rgba(30, 198, 255, 0.8);
}
}
}
}
</style>

View File

@ -12,19 +12,21 @@ import DashboardHeader from '@/views/home/components/DashboardHeader.vue'
</script>
<style lang="scss" scoped>
.layout-container {
width: 4800px;
height: 1620px;
position: relative;
background: url('/imgs/mainBg.webp') no-repeat center center;
background-size: cover;
overflow: hidden;
}
.layout-container {
width: 4800px;
height: 1620px;
position: relative;
background: url('/imgs/mainBg.webp') no-repeat center center;
background-size: cover;
overflow: hidden;
}
.route-content {
width: 4800px;
height: 1514px;
position: relative;
overflow: hidden;
}
.route-content {
width: 4800px;
height: 1514px;
position: absolute;
bottom: 0;
left: 0;
overflow: hidden;
}
</style>