崔方鑫 2026-06-18 16:30:03 +08:00
parent d311f4631f
commit b582e22a35
19 changed files with 3550 additions and 30 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
public/imgs/mainBg.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -7,6 +7,7 @@ import App from './App.vue'
import router from './router'
import { globalScale } from './utils/scale'
import './styles/global.scss'
import './styles/el-fomat.scss'
const app = createApp(App)
@ -20,4 +21,4 @@ app.use(ElementPlus)
app.mount('#app')
globalScale()
// globalScale()

48
src/styles/el-fomat.scss Normal file
View File

@ -0,0 +1,48 @@
.traffic-filter-popper.el-popper {
border: 1px solid rgba(40, 177, 255, 0.55) !important;
background: rgba(7, 31, 78, 0.96) !important;
}
.traffic-filter-popper.el-popper .el-popper__arrow::before {
border-color: rgba(40, 177, 255, 0.55) !important;
background: rgba(7, 31, 78, 0.96) !important;
}
.traffic-filter-popper .el-select-dropdown__item {
color: #dcecff;
font-size: 16px;
}
.traffic-filter-popper .el-select-dropdown__item.is-hovering,
.traffic-filter-popper .el-select-dropdown__item.hover,
.traffic-filter-popper .el-select-dropdown__item.is-selected {
color: #ffffff;
background: rgba(22, 119, 214, 0.68);
}
.traffic-month-popper .el-picker-panel,
.traffic-month-popper .el-date-picker,
.traffic-month-popper .el-date-picker__header,
.traffic-month-popper .el-picker-panel__content {
background: transparent !important;
}
.traffic-month-popper .el-date-picker__header-label,
.traffic-month-popper .el-picker-panel__icon-btn {
color: #dcecff;
font-size: 16px;
font-weight: 700;
}
.traffic-month-popper .el-date-table-cell__text {
color: #dcecff !important;
}
.traffic-month-popper .el-month-table td.current:not(.disabled) .el-date-table-cell__text {
color: #ffffff;
background: rgba(22, 119, 214, 0.68);
}

View File

@ -14,6 +14,18 @@
font-style: normal;
font-display: swap;
}
@font-face {
font-family: BlackOpsOne;
src: url('/fonts/BlackOpsOne-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: YouSheBiaoTiHei;
src: url('/fonts/YouSheBiaoTiHei.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
* {
margin: 0;
@ -25,6 +37,7 @@ html, body, #app {
width: 100%;
height: 100%;
overflow: hidden;
overflow: scroll;
font-family: 'Alibaba PuHuiTi 3.0', 'Microsoft YaHei', 'PingFang SC', sans-serif;
background: var(--bg-dark);
color: var(--text-primary);

24
src/utils/const.js Normal file
View File

@ -0,0 +1,24 @@
/** @format */
export const METRO_LINES = {
1: { bg: "#E20930", color: "#ffffff", name: "1号线" },
2: { bg: "#80BD2A", color: "#000000", name: "2号线" },
3: { bg: "#F9D600", color: "#000000", name: "3号线" },
4: { bg: "#482284", color: "#ffffff", name: "4号线" },
5: { bg: "#944D9A", color: "#ffffff", name: "5号线" },
6: { bg: "#E10066", color: "#ffffff", name: "6号线" },
7: { bg: "#ED6E00", color: "#000000", name: "7号线" },
8: { bg: "#0094D6", color: "#ffffff", name: "8号线" },
9: { bg: "#85C8EC", color: "#000000", name: "9号线" },
10: { bg: "#C6ADD2", color: "#000000", name: "10号线" },
11: { bg: "#86202E", color: "#ffffff", name: "11号线" },
12: { bg: "#0C775F", color: "#ffffff", name: "12号线" },
13: { bg: "#E698BF", color: "#000000", name: "13号线" },
14: { bg: "#616025", color: "#ffffff", name: "14号线" },
15: { bg: "#C9B38E", color: "#000000", name: "15号线" },
16: { bg: "#97D0BF", color: "#000000", name: "16号线" },
17: { bg: "#B97970", color: "#ffffff", name: "17号线" },
18: { bg: "#C29750", color: "#000000", name: "18号线" },
41: { bg: "#B4B5B4", color: "#FFFFFF", name: "浦江线" },
51: { bg: "#898989", color: "#FFFFFF", name: "市域机场线" },
};

File diff suppressed because it is too large Load Diff

View File

@ -113,12 +113,6 @@ onUnmounted(() => {
</script>
<style lang="scss" scoped>
@font-face {
font-family: BlackOpsOne;
src: url('/fonts/BlackOpsOne-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
.dashboard-header {
width: 4800px;

View File

@ -16,7 +16,7 @@ import DashboardHeader from '@/views/home/components/DashboardHeader.vue'
width: 4800px;
height: 1620px;
position: relative;
background: url('/imgs/mainBg.png') no-repeat center center;
background: url('/imgs/mainBg.webp') no-repeat center center;
background-size: cover;
overflow: hidden;
}

View File

@ -0,0 +1,203 @@
<template>
<div class="station-filter" :style="themeStyle">
<div class="filter-item " :style="getFilterItemStyle(lineDisplayText, 'line')">
<el-select v-model="selectedLine" popper-class="traffic-filter-popper" :teleported="true">
<el-option v-for="item in lineOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<span class="filter-arrow" aria-hidden="true"></span>
</div>
<div class="filter-item" :style="getFilterItemStyle(stationDisplayText, 'station')">
<el-select v-model="selectedStation" popper-class="traffic-filter-popper" :teleported="true">
<el-option v-for="item in stationOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<span class="filter-arrow" aria-hidden="true"></span>
</div>
</div>
</template>
<script setup>
import { computed, watch } from 'vue'
import metroMenuData from '@/utils/lineStationList.json'
import { METRO_LINES } from '@/utils/const'
const props = defineProps({
line: {
type: String,
default: ''
},
station: {
type: String,
default: ''
},
})
const emit = defineEmits(['update:line', 'update:station'])
const lineStationList = metroMenuData.lineStationList || []
const selectedLine = computed({
get: () => props.line,
set: value => emit('update:line', value)
})
const selectedStation = computed({
get: () => props.station,
set: value => emit('update:station', value)
})
const lineOptions = computed(() => {
return lineStationList.map(item => ({
label: item.nameCn,
value: item.lineId
}))
})
const stationOptions = computed(() => {
const currentLine = lineStationList.find(item => item.lineId === selectedLine.value)
const stationList = currentLine?.stationList || []
return stationList.map(item => ({
label: item.nameCn,
value: item.statId,
line: item.line
}))
})
const lineDisplayText = computed(() => {
const currentLine = lineOptions.value.find(item => item.value === props.line)
return String(currentLine?.label || lineOptions.value[0]?.label || '')
})
const stationDisplayText = computed(() => {
const currentStation = stationOptions.value.find(item => item.value === props.station)
return String(currentStation?.label || stationOptions.value[0]?.label || '')
})
const lineTheme = computed(() => {
return METRO_LINES[Number(selectedLine.value)] || {
bg: '#ED6E00',
color: '#000000'
}
})
const themeStyle = computed(() => ({
'--metro-bg': lineTheme.value.bg,
'--metro-text': lineTheme.value.color,
}))
const getFilterItemStyle = (text, type) => {
const contentLength = Array.from(String(text || '')).length
const rules = {
line: { padding: 18, charWidth: 20 },
station: { padding: 18, charWidth: 20 },
month: { padding: 44, charWidth: 12 }
}
const rule = rules[type]
return {
width: `${Math.max(rule.padding + contentLength * rule.charWidth)}px`
}
}
watch(stationOptions, options => {
if (!options.length) {
emit('update:station', '')
return
}
const hasCurrentStation = options.some(item => item.value === props.station)
if (!hasCurrentStation) {
emit('update:station', options[0].value)
}
}, { immediate: true })
</script>
<style lang="scss" scoped>
.station-filter {
display: flex;
align-items: center;
gap: 26px;
height: 33px;
}
.filter-item {
position: relative;
display: flex;
align-items: center;
flex-shrink: 0;
height: 33px;
width: max-content;
margin-right: 10px;
:deep(.el-select__wrapper) {
box-sizing: border-box;
border: 0;
border-radius: 6px;
box-shadow: none;
padding: 0 0 0 5px;
background: var(--metro-bg);
}
:deep(.el-input__wrapper) {
box-sizing: border-box;
border: 0;
box-shadow: none;
background: transparent !important;
}
:deep(.el-input__inner) {
font-size: 22px;
font-family: BlackOpsOne;
color: #c5dfff !important;
font-weight: 400;
text-shadow: 2px 2px 2px #0051ec !important;
}
:deep(.el-select__placeholder),
:deep(.el-select__selected-item) {
color: var(--metro-text);
display: block;
width: 100%;
text-align: center;
font-size: 20px;
font-weight: 500;
text-shadow: none;
overflow: visible;
}
:deep(.el-select__caret),
:deep(.el-input__suffix),
:deep(.el-input__prefix),
:deep(.el-input__suffix-inner),
:deep(.el-icon-circle-close) {
display: none !important;
}
}
.filter-arrow {
position: absolute;
top: 50%;
right: -20px;
width: 14px;
height: 9px;
background: url('/imgs/traffic/7-item.png') no-repeat center center;
background-size: 14px 9px;
transform: translateY(-50%);
pointer-events: none;
}
</style>

View File

@ -0,0 +1,232 @@
<template>
<div class="station-time-filter" :style="themeStyle">
<div class="filter-item " :style="getFilterItemStyle(lineDisplayText, 'line')">
<el-select v-model="selectedLine" popper-class="traffic-filter-popper" :teleported="true">
<el-option v-for="item in lineOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<span class="filter-arrow" aria-hidden="true"></span>
</div>
<div class="filter-item" :style="getFilterItemStyle(stationDisplayText, 'station')">
<el-select v-model="selectedStation" popper-class="traffic-filter-popper" :teleported="true">
<el-option v-for="item in stationOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<span class="filter-arrow" aria-hidden="true"></span>
</div>
<div class="filter-item item-month" :style="getFilterItemStyle(monthDisplayText, 'month')">
<el-config-provider :locale="zhCn">
<el-date-picker v-model="selectedMonth" type="month" format="YYYY-M" value-format="YYYY-M"
popper-class="traffic-filter-popper traffic-month-popper" :clearable="false" :editable="false"
:teleported="true" :prefix-icon="Calendar" />
</el-config-provider>
<span class="filter-arrow" aria-hidden="true"></span>
</div>
</div>
</template>
<script setup>
import { computed, watch } from 'vue'
import { Calendar } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import metroMenuData from '@/utils/lineStationList.json'
import { METRO_LINES } from '@/utils/const'
import 'dayjs/locale/zh-cn'
dayjs.locale('zh-cn')
const props = defineProps({
line: {
type: String,
default: ''
},
station: {
type: String,
default: ''
},
month: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:line', 'update:station', 'update:month'])
const lineStationList = metroMenuData.lineStationList || []
const selectedLine = computed({
get: () => props.line,
set: value => emit('update:line', value)
})
const selectedStation = computed({
get: () => props.station,
set: value => emit('update:station', value)
})
const selectedMonth = computed({
get: () => props.month,
set: value => emit('update:month', value)
})
const lineOptions = computed(() => {
return lineStationList.map(item => ({
label: item.nameCn,
value: item.lineId
}))
})
const stationOptions = computed(() => {
const currentLine = lineStationList.find(item => item.lineId === selectedLine.value)
const stationList = currentLine?.stationList || []
return stationList.map(item => ({
label: item.nameCn,
value: item.statId,
line: item.line
}))
})
const lineDisplayText = computed(() => {
const currentLine = lineOptions.value.find(item => item.value === props.line)
return String(currentLine?.label || lineOptions.value[0]?.label || '')
})
const stationDisplayText = computed(() => {
const currentStation = stationOptions.value.find(item => item.value === props.station)
return String(currentStation?.label || stationOptions.value[0]?.label || '')
})
const monthDisplayText = computed(() => String(props.month || ''))
const lineTheme = computed(() => {
return METRO_LINES[Number(selectedLine.value)] || {
bg: '#ED6E00',
color: '#000000'
}
})
const themeStyle = computed(() => ({
'--metro-bg': lineTheme.value.bg,
'--metro-text': lineTheme.value.color,
}))
const getFilterItemStyle = (text, type) => {
const contentLength = Array.from(String(text || '')).length
const rules = {
line: { padding: 18, charWidth: 20 },
station: { padding: 18, charWidth: 20 },
month: { padding: 44, charWidth: 12 }
}
const rule = rules[type]
return {
width: `${Math.max(rule.padding + contentLength * rule.charWidth)}px`
}
}
watch(stationOptions, options => {
if (!options.length) {
emit('update:station', '')
return
}
const hasCurrentStation = options.some(item => item.value === props.station)
if (!hasCurrentStation) {
emit('update:station', options[0].value)
}
}, { immediate: true })
</script>
<style lang="scss" scoped>
.station-time-filter {
display: flex;
align-items: center;
gap: 26px;
height: 33px;
}
.filter-item {
position: relative;
display: flex;
align-items: center;
flex-shrink: 0;
height: 33px;
width: max-content;
margin-right: 10px;
:deep(.el-select__wrapper) {
box-sizing: border-box;
border: 0;
border-radius: 6px;
box-shadow: none;
padding: 0 0 0 5px;
background: var(--metro-bg);
}
:deep(.el-input__wrapper) {
box-sizing: border-box;
border: 0;
box-shadow: none;
background: transparent !important;
}
:deep(.el-input__inner) {
font-size: 22px;
font-family: BlackOpsOne;
color: #c5dfff !important;
font-weight: 400;
text-shadow: 2px 2px 2px #0051ec !important;
}
:deep(.el-select__placeholder),
:deep(.el-select__selected-item) {
color: var(--metro-text);
display: block;
width: 100%;
text-align: center;
font-size: 20px;
font-weight: 500;
text-shadow: none;
overflow: visible;
}
:deep(.el-select__caret),
:deep(.el-input__suffix),
:deep(.el-input__prefix),
:deep(.el-input__suffix-inner),
:deep(.el-icon-circle-close) {
display: none !important;
}
}
.item-month {
margin-right: 0;
margin-left: -10px;
.filter-arrow {
margin-right: 15px;
}
}
.filter-arrow {
position: absolute;
top: 50%;
right: -20px;
width: 14px;
height: 9px;
background: url('/imgs/traffic/7-item.png') no-repeat center center;
background-size: 14px 9px;
transform: translateY(-50%);
pointer-events: none;
}
</style>

View File

@ -1,5 +1,7 @@
<template>
<section class="traffic-4">
<StationFilter class="traffic-4__filter" v-model:line="selectedLine" v-model:station="selectedStation"
/>
<div class="chart-shell">
<v-chart class="traffic-4__chart" :option="chartOption" autoresize />
</div>
@ -7,10 +9,18 @@
</template>
<script setup>
import { computed } from 'vue'
import { computed ,ref} from 'vue'
import StationFilter from './StationFilter.vue'
import VChart from 'vue-echarts'
import * as echarts from 'echarts'
const selectedLine = ref('3')
const selectedStation = ref('0313')
const hours = ['5', '7', '9', '11', '13', '15', '17', '19', '21', '23']
const topActual = [800, 900, 2100, 4050, 1500, 1450, 1200, 2300, 900, 900]
@ -19,6 +29,11 @@ const topCompare = [1400, 1800, 3400, 4500, 2950, 2600, 2800, 4300, 4300, 4850]
const bottomActual = [4800, 4400, 3000, 1600, 3150, 3500, 3350, 1850, 1450, 1650]
const bottomCompare = [5400, 5300, 4900, 2200, 4700, 5000, 3850, 4700, 5350, 5350 ]
const pointGlowSymbol = 'image:///imgs/traffic/4-item.png'
const pointGlowSymbol1 = 'image:///imgs/traffic/4-item1.png'
const topPointGlow = hours.map((hour, index) => [hour, topActual[index]])
const bottomPointGlow = hours.map((hour, index) => [hour, bottomActual[index]])
const chartOption = computed(() => ({
backgroundColor: 'transparent',
@ -136,6 +151,36 @@ const chartOption = computed(() => ({
}
],
series: [
{
type: 'scatter',
xAxisIndex: 0,
yAxisIndex: 0,
data: topPointGlow,
symbol: pointGlowSymbol,
symbolSize: [47, 109],
symbolOffset: [4, 0],
symbolKeepAspect: true,
silent: true,
z: 4,
tooltip: {
show: false
}
},
{
type: 'scatter',
xAxisIndex: 1,
yAxisIndex: 1,
data: bottomPointGlow,
symbol: pointGlowSymbol1,
symbolSize: [47, 109],
symbolOffset: [4,0],
symbolKeepAspect: true,
silent: true,
z: 4,
tooltip: {
show: false
}
},
{
name: '进出站',
type: 'line',
@ -251,27 +296,35 @@ const chartOption = computed(() => ({
</script>
<style lang="scss" scoped>
.traffic-4 {
position: absolute;
left: 2948px;
top: 128px;
width: 892px;
height: 564.59px;
background: url('/imgs/traffic/4-bg.png') no-repeat center center;
background-size: cover;
.chart-shell {
.traffic-4 {
position: absolute;
left: 56px;
right: 8px;
top: 66px;
bottom: 4px;
background: #041a4b;
}
left: 2948px;
top: 128px;
width: 892px;
height: 564.59px;
background: url('/imgs/traffic/4-bg.png') no-repeat center center;
background-size: cover;
&__chart {
width: 100%;
height: 100%;
.traffic-4__filter {
position: absolute;
top: 12px;
right: 30px;
z-index: 2;
}
.chart-shell {
position: absolute;
left: 56px;
right: 8px;
top: 66px;
bottom: 4px;
}
&__chart {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@ -1,7 +1,90 @@
<template>
<section class="traffic-7"></section>
<section class="traffic-7">
<StationTimeFilter
class="traffic-7__filter"
v-model:line="selectedLine"
v-model:station="selectedStation"
v-model:month="selectedMonth"
/>
<div class="activity-list">
<article v-for="activity in activityList" :key="activity.name" class="activity-card">
<div class="activity-status" :class="`activity-status--${activity.statusType}`">
{{ activity.status }}
</div>
<h3 class="activity-name">{{ activity.name }}</h3>
<div class="activity-meta">
<span class="activity-label">时间:</span>
<span>{{ activity.time }}</span>
</div>
<div v-if="activity.venue" class="activity-meta activity-meta--venue">
<span class="activity-label">举办场馆:</span>
<span>{{ activity.venue }}</span>
</div>
</article>
</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
import StationTimeFilter from './StationTimeFilter.vue'
const selectedLine = ref('7')
const selectedStation = ref('0753')
const selectedMonth = ref('2026-5')
const activityList = [
{
name: 'CBE中国美容博览会',
time: '2026/05/12--2026/05/14',
venue: '新国际博览中心',
status: '进行中',
statusType: 'active'
},
{
name: '上海插画艺术节',
time: '2026/05/01--2026/05/03',
status: '已结束',
statusType: 'ended'
},
{
name: '中国国际自行车展览会/中国国际电动车及零配件展览会/中国国际摩托车及零配件展览会/上海国际户外骑行装备展览会',
time: '2026/05/05--2026/05/08',
status: '已结束',
statusType: 'ended'
},
{
name: 'SIAL 西雅国际食品展览会(上海)',
time: '2026/05/18--2026/05/20',
status: '未开始',
statusType: 'pending'
},
{
name: '亚洲国际有机产品博览会',
time: '2026/05/18--2026/05/20',
status: '未开始',
statusType: 'pending'
},
{
name: '上海网络安全博览会暨高峰论坛',
time: '2026/05/26--2026/05/28',
status: '未开始',
statusType: 'pending'
},
{
name: '中国国际厨房、卫浴设施展览会',
time: '2026/05/26--2026/05/29',
status: '未开始',
statusType: 'pending'
}
]
</script>
<style lang="scss" scoped>
.traffic-7 {
position: absolute;
@ -11,5 +94,130 @@
height: 880px;
background: url('/imgs/traffic/7-bg.png') no-repeat center center;
background-size: cover;
.traffic-7__filter {
position: absolute;
top: 12px;
right: 30px;
z-index: 2;
}
.activity-list {
position: absolute;
top: 75px;
right: 12px;
bottom: 20px;
left: 20px;
display: flex;
flex-direction: column;
gap: 15px;
overflow-x: hidden;
overflow-y: scroll;
padding-right: 6px;
}
.activity-card {
position: relative;
min-height: 89px;
box-sizing: border-box;
padding: 12px 104px 10px 31px;
overflow: hidden;
border-radius: 5px;
letter-spacing: 0.03em;
flex-shrink: 0;
background: rgba(44, 121, 236, 0.35);
}
.activity-empty {
height: 100%;
:deep(.el-empty__description p) {
color: rgba(220, 236, 255, 0.78);
font-size: 18px;
font-weight: 700;
}
}
.activity-name {
margin: 0;
color: #FFFFFF;
font-size: 22px;
font-weight: 500;
line-height: 32px;
letter-spacing: 0.03em;
}
.activity-meta {
display: flex;
align-items: center;
// gap: 37px;
margin-top: 7px;
color: rgba(255, 255, 255, 0.8);
font-size: 20px;
font-weight: 400;
line-height: 28px;
white-space: nowrap;
&--venue {
margin-top: 7px;
}
}
.activity-label {
color: rgba(228, 243, 255, 0.74);
flex-shrink: 0;
width: 94px;
}
.activity-status {
position: absolute;
top: 0;
right: 0;
z-index: 1;
width: 91px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 500;
line-height: 1;
text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
&::before {
content: '';
position: absolute;
top: 0;
left: -20px;
width: 0;
height: 0;
border-top: 35px solid var(--status-bg-start);
border-left: 20px solid transparent;
}
&--active {
--status-bg-start: #078b66;
--status-bg-end: #08b279;
color: #02FF5F;
background: linear-gradient(90deg, var(--status-bg-start) 0%, var(--status-bg-end) 100%);
}
&--ended {
--status-bg-start: #9b7f7f;
--status-bg-end: #bba1a1;
color: #cfcfcf;
background: linear-gradient(90deg, var(--status-bg-start) 0%, var(--status-bg-end) 100%);
}
&--pending {
--status-bg-start: #546174;
--status-bg-end: #6f7c91;
color: #f0f0f0;
background: linear-gradient(90deg, var(--status-bg-start) 0%, var(--status-bg-end) 100%);
}
}
}
</style>