Merge branch 'main' of https://git.deploy.top/wyh/yunsan-pc
|
|
@ -1,3 +1,3 @@
|
|||
# 开发环境配置
|
||||
# 使用相对路径,通过 vite proxy 代理到目标服务器
|
||||
# 留空则通过 vite proxy 代理到 http://101.133.172.2:8082
|
||||
VITE_API_BASE_URL=
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
# 生产环境配置
|
||||
VITE_API_BASE_URL=http://10.104.10.151:8888
|
||||
# 生产环境配置 目前是配置代理的方式
|
||||
VITE_API_BASE_URL=/juntech-ysdp
|
||||
|
|
|
|||
|
|
@ -1,2 +1,53 @@
|
|||
# 依赖
|
||||
node_modules
|
||||
|
||||
# 构建产物
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# 项目文档(本地维护,不纳入版本库)
|
||||
doc/
|
||||
|
||||
# 环境变量(保留 .env.development / .env.production,忽略本地覆盖)
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# 日志
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# 编辑器 / IDE
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# 操作系统
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# 测试 / 覆盖率
|
||||
coverage
|
||||
*.lcov
|
||||
.nyc_output
|
||||
|
||||
# 缓存 / 临时文件
|
||||
.cache
|
||||
.temp
|
||||
.tmp
|
||||
*.tsbuildinfo
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 618 B |
|
After Width: | Height: | Size: 621 B |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 279 B |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 307 KiB |
|
|
@ -15,7 +15,7 @@ const props = defineProps({
|
|||
// 光晕图片路径
|
||||
glowImage: {
|
||||
type: String,
|
||||
default: '/imgs/guangyun.png'
|
||||
default: './imgs/guangyun.png'
|
||||
},
|
||||
// 是否显示虚线光圈
|
||||
showRing: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
import { ref, watch, onMounted } from 'vue'
|
||||
import server from '@/utils/service'
|
||||
import metroMenuData from '@/utils/lineStationList.json'
|
||||
|
||||
const localLineStationList = metroMenuData.lineStationList || []
|
||||
|
||||
function mapLocalLines() {
|
||||
return localLineStationList.map(item => ({
|
||||
label: item.nameCn,
|
||||
value: item.lineId
|
||||
}))
|
||||
}
|
||||
|
||||
function mapLocalStations(lineId) {
|
||||
const currentLine = localLineStationList.find(item => item.lineId === lineId)
|
||||
return (currentLine?.stationList || []).map(item => ({
|
||||
label: item.nameCn,
|
||||
value: item.statId
|
||||
}))
|
||||
}
|
||||
|
||||
/** 客运页线路/站点下拉:优先接口,空数据时回退本地 JSON */
|
||||
export function useTrafficLineStation(getLine, getStation, emit) {
|
||||
const lineOptions = ref([])
|
||||
const stationOptions = ref([])
|
||||
|
||||
const loadLines = async () => {
|
||||
try {
|
||||
const response = await server.getLineList()
|
||||
if (response.data.success && response.data.data?.length) {
|
||||
lineOptions.value = response.data.data.map(item => ({
|
||||
label: item.name,
|
||||
value: String(item.lineId)
|
||||
}))
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取线路列表失败:', error)
|
||||
}
|
||||
lineOptions.value = mapLocalLines()
|
||||
}
|
||||
|
||||
const loadStations = async lineId => {
|
||||
if (!lineId) {
|
||||
stationOptions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await server.getStationsByLine(lineId)
|
||||
if (response.data.success && response.data.data?.length) {
|
||||
stationOptions.value = response.data.data.map(item => ({
|
||||
label: item.stationName || item.name || item.nameCn,
|
||||
value: item.stationId || item.statId
|
||||
}))
|
||||
syncStationSelection()
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取站点列表失败:', error)
|
||||
}
|
||||
|
||||
stationOptions.value = mapLocalStations(lineId)
|
||||
syncStationSelection()
|
||||
}
|
||||
|
||||
const syncStationSelection = () => {
|
||||
const options = stationOptions.value
|
||||
if (!options.length) {
|
||||
emit('update:station', '')
|
||||
return
|
||||
}
|
||||
const currentStation = getStation()
|
||||
const hasCurrent = options.some(item => item.value === currentStation)
|
||||
if (!hasCurrent) {
|
||||
emit('update:station', options[0].value)
|
||||
}
|
||||
}
|
||||
|
||||
const initLineSelection = () => {
|
||||
if (!lineOptions.value.length) return
|
||||
const currentLine = getLine()
|
||||
const hasCurrent = lineOptions.value.some(item => item.value === currentLine)
|
||||
if (!hasCurrent) {
|
||||
emit('update:line', lineOptions.value[0].value)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => getLine(),
|
||||
lineId => loadStations(lineId),
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadLines()
|
||||
initLineSelection()
|
||||
await loadStations(getLine())
|
||||
})
|
||||
|
||||
return { lineOptions, stationOptions }
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { request } from './request'
|
||||
import { encrypt } from './decrypt'
|
||||
import axios from 'axios'
|
||||
import { ysdpPost } from './ysdpRequest'
|
||||
|
||||
var ipRoot = "http://140.206.138.190:8900"
|
||||
var ip = "http://140.206.138.190:8905"
|
||||
var ysdpmIp = import.meta.env.VITE_API_BASE_URL || "http://192.168.16.193:8888"
|
||||
|
||||
var getToken = "/app/getToken"
|
||||
var getTblMetroInout = ipRoot + '/api/xckf/getTblMetroInout/v1'
|
||||
|
|
@ -22,14 +21,6 @@ var getBusInfo = ipRoot + '/api/xckf/getBusInfo/v1'
|
|||
var updayeBusEntrance = ipRoot + '/api/xckf/updayeBusEntrance/v1'
|
||||
var getHotEventList = ipRoot + '/api/xckf/getHotEventList/v1'
|
||||
var updateHotEvent = ipRoot + '/api/xckf/updateHotEvent/v1'
|
||||
var getOperateInfo = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getOperateInfo/v1'
|
||||
var getCommonMonitor = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getCommonMonitor/v1'
|
||||
var getDutyList = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getDutyList/v1'
|
||||
var getStationMaxList = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getStationMaxList/v1'
|
||||
var getIndicator = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getIndicator/v1'
|
||||
var getYunyingInfo = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getYunyingInfo/v1'
|
||||
var getQxjGdybHour = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getQxjGdybHour/v1'
|
||||
var getTaskNotice = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getTaskNotice/v1'
|
||||
var xcImg = ip + '/1734.jpg'
|
||||
var xcImgGd = ip + '/lineCrossImg/1734.jpg'
|
||||
|
||||
|
|
@ -142,35 +133,75 @@ var server = {
|
|||
},
|
||||
|
||||
getOperateInfo() {
|
||||
return axios.post(getOperateInfo, {})
|
||||
return ysdpPost('/api/getOperateInfo/v1')
|
||||
},
|
||||
|
||||
getCommonMonitor() {
|
||||
return axios.post(getCommonMonitor, {})
|
||||
return ysdpPost('/api/getCommonMonitor/v1')
|
||||
},
|
||||
|
||||
getDutyList() {
|
||||
return axios.post(getDutyList, {})
|
||||
return ysdpPost('/api/getDutyList/v1')
|
||||
},
|
||||
|
||||
getStationMaxList() {
|
||||
return axios.post(getStationMaxList, {})
|
||||
return ysdpPost('/api/getStationMaxList/v1')
|
||||
},
|
||||
|
||||
getIndicator() {
|
||||
return axios.post(getIndicator, {})
|
||||
return ysdpPost('/api/getIndicator/v1')
|
||||
},
|
||||
|
||||
getQxjGdybHour() {
|
||||
return axios.post(getQxjGdybHour, {})
|
||||
return ysdpPost('/api/getQxjGdybHour/v1')
|
||||
},
|
||||
|
||||
getYunyingInfo() {
|
||||
return axios.post(getYunyingInfo, {})
|
||||
return ysdpPost('/api/getYunyingInfo/v1')
|
||||
},
|
||||
|
||||
getTaskNotice() {
|
||||
return axios.post(getTaskNotice, {})
|
||||
return ysdpPost('/api/getTaskNotice/v1')
|
||||
},
|
||||
|
||||
getInOutStationRank() {
|
||||
return ysdpPost('/api/traffic/getInOutStationRank/v1')
|
||||
},
|
||||
|
||||
getTransferStationRank() {
|
||||
return ysdpPost('/api/traffic/getTransferStationRank/v1')
|
||||
},
|
||||
|
||||
getLineList() {
|
||||
return ysdpPost('/api/traffic/getLineList/v1')
|
||||
},
|
||||
|
||||
getLinePassengerFlow() {
|
||||
return ysdpPost('/api/traffic/getLinePassengerFlow/v1')
|
||||
},
|
||||
|
||||
getStationPassengerFlow(stationId) {
|
||||
return ysdpPost('/api/traffic/getStationPassengerFlow/v1', { stationId })
|
||||
},
|
||||
|
||||
getStationsByLine(lineId) {
|
||||
return ysdpPost('/api/traffic/getStationsByLine/v1', { lineId })
|
||||
},
|
||||
|
||||
getBigActivityList({ metroLine, metroStation, activityMonth }) {
|
||||
return ysdpPost('/api/traffic/getBigActivityList/v1', {
|
||||
metroLine,
|
||||
metroStation,
|
||||
activityMonth
|
||||
})
|
||||
},
|
||||
|
||||
cleanTrafficCache() {
|
||||
return ysdpPost('/api/traffic/cleanCache')
|
||||
},
|
||||
|
||||
cleanApiCache() {
|
||||
return ysdpPost('/api/cleanApiCache')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
const TIME_KEYS = ['time1', 'time2', 'time3', 'time4', 'time5', 'time6', 'time7', 'time8', 'time9', 'time10']
|
||||
|
||||
/** 从 time1~time10 对象提取时序数值 */
|
||||
export function extractTimeValues(dataObj) {
|
||||
return TIME_KEYS.map(key => {
|
||||
const value = dataObj?.[key]
|
||||
return value ? parseFloat(value) : 0
|
||||
})
|
||||
}
|
||||
|
||||
/** 将单站客流接口数据转为 Traffic4 图表结构 */
|
||||
export function processStationPassengerFlow(data) {
|
||||
if (!data) return null
|
||||
|
||||
return {
|
||||
compareDate: data.compareDate || '',
|
||||
inOut: {
|
||||
actual: extractTimeValues(data.inOut?.today),
|
||||
compare: extractTimeValues(data.inOut?.compareDay)
|
||||
},
|
||||
transfer: {
|
||||
actual: extractTimeValues(data.transfer?.today),
|
||||
compare: extractTimeValues(data.transfer?.compareDay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 计算图表 Y 轴最大值(按万取整) */
|
||||
export function calcChartYAxisMax(...seriesList) {
|
||||
const maxValue = Math.max(...seriesList.flat(), 0)
|
||||
return Math.ceil(maxValue / 10000) * 10000 || 10000
|
||||
}
|
||||
|
||||
/** 将接口 lineId(如 07、04,07)转为页面展示格式(7、4,7) */
|
||||
export function normalizeLineIds(lineId) {
|
||||
if (!lineId) return ''
|
||||
return String(lineId)
|
||||
.split(',')
|
||||
.map(id => String(parseInt(id.trim(), 10)))
|
||||
.filter(id => id && id !== 'NaN')
|
||||
.join(',')
|
||||
}
|
||||
|
||||
/** 客流数值转万人(保留两位小数) */
|
||||
export function toWanRen(value) {
|
||||
const num = parseFloat(value) || 0
|
||||
return (num / 10000).toFixed(2)
|
||||
}
|
||||
|
||||
const ACTIVITY_STATUS_TYPE_MAP = {
|
||||
进行中: 'active',
|
||||
已结束: 'ended',
|
||||
未开始: 'pending'
|
||||
}
|
||||
|
||||
/** 活动月份格式化为 YYYY-MM */
|
||||
export function formatActivityMonth(month) {
|
||||
if (!month) return ''
|
||||
const [year, monthValue] = String(month).split('-')
|
||||
if (!year || !monthValue) return ''
|
||||
return `${year}-${String(monthValue).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/** 将重大活动清单接口数据映射为 Traffic7 所需结构 */
|
||||
export function processBigActivityList(data) {
|
||||
if (!Array.isArray(data)) return []
|
||||
|
||||
return data.map(item => {
|
||||
const startDate = item.startDate ? item.startDate.replace(/-/g, '/') : ''
|
||||
const endDate = item.endDate ? item.endDate.replace(/-/g, '/') : ''
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.activityName || '',
|
||||
time: startDate && endDate ? `${startDate}--${endDate}` : startDate || endDate,
|
||||
venue: item.venue || '',
|
||||
status: item.status || '',
|
||||
statusType: ACTIVITY_STATUS_TYPE_MAP[item.status] || 'pending'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 将站点排名接口数据映射为 TrafficRankRow 所需结构 */
|
||||
export function mapStationRankList(data, type = '进出站') {
|
||||
if (!Array.isArray(data) || !data.length) return []
|
||||
|
||||
const maxTotal = Math.max(...data.map(item => parseFloat(item.total) || 0))
|
||||
|
||||
return data.map((item, index) => {
|
||||
const total = parseFloat(item.total) || 0
|
||||
const compare = parseFloat(item.compareTotal) || 0
|
||||
|
||||
return {
|
||||
rank: index + 1,
|
||||
lines: normalizeLineIds(item.lineId),
|
||||
station: item.stationName || '',
|
||||
type,
|
||||
today: toWanRen(total),
|
||||
compare: toWanRen(compare),
|
||||
percent: maxTotal > 0 ? Math.round((total / maxTotal) * 100) : 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import axios from 'axios'
|
||||
|
||||
// 开发环境留空时默认 /juntech-ysdp(走 vite proxy);生产环境 VITE_API_BASE_URL 已含此前缀,不再重复拼接
|
||||
const ysdpRequest = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/juntech-ysdp',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
/** POST 请求,参数以 application/x-www-form-urlencoded 提交 */
|
||||
export function ysdpPost(path, data = {}) {
|
||||
const body = new URLSearchParams()
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
body.append(key, value)
|
||||
}
|
||||
})
|
||||
return ysdpRequest.post(path, body, {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
})
|
||||
}
|
||||
|
||||
export default ysdpRequest
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
const TIME_KEYS = ['time1', 'time2', 'time3', 'time4', 'time5', 'time6', 'time7', 'time8', 'time9', 'time10']
|
||||
|
||||
/** 根据当前时刻计算对应的 time 索引(time1~time10) */
|
||||
export function getCurrentTimeIndex() {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 5) return 0
|
||||
return Math.min(Math.ceil((hour - 5) / 2), 9)
|
||||
}
|
||||
|
||||
function getTimeValues(dataObj) {
|
||||
return TIME_KEYS.map(key => {
|
||||
const value = dataObj?.[key]
|
||||
return value ? parseFloat(value) : 0
|
||||
})
|
||||
}
|
||||
|
||||
/** 将 getYunyingInfo 接口数据转为图表组件所需结构 */
|
||||
export function processYunyingInfo(dataList) {
|
||||
if (!Array.isArray(dataList)) return []
|
||||
|
||||
const currentTimeIndex = getCurrentTimeIndex()
|
||||
|
||||
return dataList.map(item => {
|
||||
const barData = getTimeValues(item.today)
|
||||
const yellowData = getTimeValues(item.compareDay)
|
||||
const redData = getTimeValues(item.maxDay)
|
||||
|
||||
const maxValue = redData[currentTimeIndex]
|
||||
const compareMaxValue = yellowData[currentTimeIndex]
|
||||
const totalSum = item.today?.totalSum ? parseFloat(item.today.totalSum) : 0
|
||||
const total = barData.reduce((sum, val) => sum + val, 0)
|
||||
const totalStr = Math.floor(total).toString()
|
||||
|
||||
return {
|
||||
lineId: parseInt(item.line, 10),
|
||||
line: parseInt(item.line, 10),
|
||||
total: totalStr,
|
||||
actual: barData,
|
||||
compare: yellowData,
|
||||
history: redData,
|
||||
barData,
|
||||
yellowData,
|
||||
redData,
|
||||
digits: totalStr.split('').map(Number),
|
||||
compareDate: item.compareDate || '',
|
||||
maxDate: item.maxDay?.maxDate || '',
|
||||
maxValue,
|
||||
compareMaxValue,
|
||||
totalSum
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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: '1′50″',
|
||||
unit: '',
|
||||
secondaryLabel: '中心城区',
|
||||
secondaryValue: '1′50″',
|
||||
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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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', {
|
||||
|
|
@ -134,18 +129,64 @@ onUnmounted(() => {
|
|||
<style lang="scss" scoped>
|
||||
|
||||
.dashboard-header {
|
||||
width: 4800px;
|
||||
height: 106px;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
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: cover;
|
||||
background-size: 100% 100%;
|
||||
// border-bottom: 2px solid rgba(0, 212, 255, 0.5);
|
||||
|
||||
.layout-menu {
|
||||
position: absolute;
|
||||
top: 82px;
|
||||
left: 42px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
.title-img {
|
||||
margin-top: 18px;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
@ -154,7 +195,7 @@ onUnmounted(() => {
|
|||
.header-time {
|
||||
position: absolute;
|
||||
// right: 40px;
|
||||
top: 110px;
|
||||
top: 148px;
|
||||
left: 50%;
|
||||
font-family: BlackOpsOne, sans-serif;
|
||||
font-size: 44px;
|
||||
|
|
@ -224,28 +265,28 @@ onUnmounted(() => {
|
|||
}
|
||||
|
||||
.weather-info {
|
||||
position: absolute;
|
||||
left: 1000px;
|
||||
top: 70%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
||||
.weather-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
line-height: 45px;
|
||||
|
||||
.label {
|
||||
font-size: 36px;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 36px;
|
||||
height: 58px;
|
||||
margin-left: 46px;
|
||||
gap: 14px;
|
||||
white-space: nowrap;
|
||||
color: #fff;
|
||||
|
||||
.weather-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #82f6ff;
|
||||
font-size: 40px;
|
||||
filter: drop-shadow(0 0 6px rgba(70, 224, 255, 0.75));
|
||||
}
|
||||
|
||||
.weather-temperature,
|
||||
.weather-name {
|
||||
font-size: 30px;
|
||||
font-weight: 500;
|
||||
line-height: 58px;
|
||||
text-shadow: 0px 0px 10px rgba(30, 198, 255, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
<!-- <div class="module-title">模块一</div> -->
|
||||
<div class="module-body">
|
||||
<div class="pyramid-3d-wrapper">
|
||||
<Pyramid3D :show-glow="true" :show-ring="true" glow-image="/imgs/guangyun.png" color="#00d4ff" :scale="1.77"
|
||||
<Pyramid3D :show-glow="true" :show-ring="true" glow-image="./imgs/guangyun.png" color="#00d4ff" :scale="1.77"
|
||||
:rotation-speed="0.2" :auto-rotate="true" :width="252" :height="166" @loaded="onPyramidLoaded"
|
||||
@error="onPyramidError" />
|
||||
</div>
|
||||
<div class="pyramid-3d-wrapper2">
|
||||
<Pyramid3D :show-glow="true" :show-ring="true" glow-image="/imgs/guangyun.png" color="#00d4ff" :scale="1.77"
|
||||
<Pyramid3D :show-glow="true" :show-ring="true" glow-image="./imgs/guangyun.png" color="#00d4ff" :scale="1.77"
|
||||
:rotation-speed="0.2" :auto-rotate="true" :width="252" :height="166" @loaded="onPyramidLoaded"
|
||||
@error="onPyramidError" />
|
||||
</div>
|
||||
|
|
@ -48,7 +48,8 @@ const fetchOperateInfo = async () => {
|
|||
const response = await server.getOperateInfo()
|
||||
if (response.data.success) {
|
||||
const { level: levelData, begindate } = response.data.data
|
||||
level.value = levelData
|
||||
const levelMatch = String(levelData).match(/\d+/)
|
||||
level.value = levelMatch ? levelMatch[0] : levelData
|
||||
|
||||
const now = new Date()
|
||||
const begin = new Date(begindate)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import server from '@/utils/service'
|
||||
import { processYunyingInfo } from '@/utils/yunying'
|
||||
|
||||
const chartRefs = ref([])
|
||||
|
||||
|
|
@ -55,8 +56,6 @@ const charts = ref(chartDataList.value)
|
|||
|
||||
const chartInstances = ref([])
|
||||
|
||||
const timeKeys = ['time1', 'time2', 'time3', 'time4', 'time5', 'time6', 'time7', 'time8', 'time9', 'time10']
|
||||
|
||||
const getNumberBoxColor = (number) => {
|
||||
const colors = {
|
||||
3: '#FBD61D',
|
||||
|
|
@ -99,60 +98,18 @@ const getBaifenbiColorStyle = (chart) => {
|
|||
}
|
||||
}
|
||||
const processData = (dataList) => {
|
||||
// 根据当前系统时间计算对应的 time 索引
|
||||
const getCurrentTimeIndex = () => {
|
||||
const now = new Date()
|
||||
const hour = now.getHours()
|
||||
const minutes = now.getMinutes()
|
||||
// 5 点和 6 点对应 time1,7 点和 8 点对应 time2,以此类推
|
||||
// time1: 5-6, time2: 7-8, time3: 9-10, time4: 11-12, time5: 13-14
|
||||
// time6: 15-16, time7: 17-18, time8: 19-20, time9: 21-22, time10: 23-24
|
||||
if (hour < 5) return 0
|
||||
// 如果当前小时是偶数且分钟大于 0,或者当前小时是奇数,都算下一个时间段
|
||||
// 例如:10:30,10 是偶数,但已经过了 10 点,应该算 time4
|
||||
// 11:00-11:59 也算 time4,12:00-12:59 也算 time4
|
||||
const timeIndex = Math.ceil((hour - 5) / 2)
|
||||
const result = Math.min(timeIndex, 9) // 最大到 time10 (索引 9)
|
||||
console.log(`当前时间:${hour}:${minutes},计算得到的 time 索引:${result}, 对应 ${timeKeys[result]}`)
|
||||
return result
|
||||
}
|
||||
|
||||
const currentTimeIndex = getCurrentTimeIndex()
|
||||
|
||||
return dataList.map(item => {
|
||||
const getValues = (dataObj) => {
|
||||
return timeKeys.map(key => {
|
||||
const value = dataObj[key]
|
||||
return value ? parseFloat(value) : 0
|
||||
})
|
||||
}
|
||||
|
||||
const barData = getValues(item.today)
|
||||
const yellowData = getValues(item.compareDay)
|
||||
const redData = getValues(item.maxDay)
|
||||
|
||||
// 根据当前时间获取对应的 maxDay 值和 compareDay 值
|
||||
const maxValue = redData[currentTimeIndex]
|
||||
const compareMaxValue = yellowData[currentTimeIndex]
|
||||
const totalSum = item.today?.totalSum ? parseFloat(item.today.totalSum) : 0
|
||||
|
||||
const total = barData.reduce((sum, val) => sum + val, 0)
|
||||
const totalStr = Math.floor(total).toString()
|
||||
const digits = totalStr.split('').map(Number)
|
||||
|
||||
return {
|
||||
barData,
|
||||
yellowData,
|
||||
redData,
|
||||
number: parseInt(item.line),
|
||||
digits: digits.length > 0 ? digits : [0],
|
||||
return processYunyingInfo(dataList).map(item => ({
|
||||
barData: item.barData,
|
||||
yellowData: item.yellowData,
|
||||
redData: item.redData,
|
||||
number: item.line,
|
||||
digits: item.digits.length > 0 ? item.digits : [0],
|
||||
compareDate: item.compareDate,
|
||||
maxDate: item.maxDay?.maxDate || '',
|
||||
maxValue: maxValue,
|
||||
compareMaxValue: compareMaxValue,
|
||||
totalSum: totalSum
|
||||
}
|
||||
})
|
||||
maxDate: item.maxDate,
|
||||
maxValue: item.maxValue,
|
||||
compareMaxValue: item.compareMaxValue,
|
||||
totalSum: item.totalSum
|
||||
}))
|
||||
}
|
||||
|
||||
const fetchYunyingInfo = async () => {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ import DashboardHeader from '@/views/home/components/DashboardHeader.vue'
|
|||
.route-content {
|
||||
width: 4800px;
|
||||
height: 1514px;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -18,10 +18,9 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, watch } from 'vue'
|
||||
import metroMenuData from '@/utils/lineStationList.json'
|
||||
import { computed } from 'vue'
|
||||
import { METRO_LINES } from '@/utils/const'
|
||||
|
||||
import { useTrafficLineStation } from '@/composables/useTrafficLineStation'
|
||||
|
||||
const props = defineProps({
|
||||
line: {
|
||||
|
|
@ -31,12 +30,10 @@ const props = defineProps({
|
|||
station: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:line', 'update:station'])
|
||||
const lineStationList = metroMenuData.lineStationList || []
|
||||
|
||||
const selectedLine = computed({
|
||||
get: () => props.line,
|
||||
|
|
@ -48,25 +45,11 @@ const selectedStation = computed({
|
|||
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 { lineOptions, stationOptions } = useTrafficLineStation(
|
||||
() => props.line,
|
||||
() => props.station,
|
||||
emit
|
||||
)
|
||||
|
||||
const lineDisplayText = computed(() => {
|
||||
const currentLine = lineOptions.value.find(item => item.value === props.line)
|
||||
|
|
@ -78,8 +61,6 @@ const stationDisplayText = computed(() => {
|
|||
return String(currentStation?.label || stationOptions.value[0]?.label || '')
|
||||
})
|
||||
|
||||
|
||||
|
||||
const lineTheme = computed(() => {
|
||||
return METRO_LINES[Number(selectedLine.value)] || {
|
||||
bg: '#ED6E00',
|
||||
|
|
@ -89,7 +70,7 @@ const lineTheme = computed(() => {
|
|||
|
||||
const themeStyle = computed(() => ({
|
||||
'--metro-bg': lineTheme.value.bg,
|
||||
'--metro-text': lineTheme.value.color,
|
||||
'--metro-text': lineTheme.value.color
|
||||
}))
|
||||
|
||||
const getFilterItemStyle = (text, type) => {
|
||||
|
|
@ -105,18 +86,6 @@ const getFilterItemStyle = (text, type) => {
|
|||
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>
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, watch } from 'vue'
|
||||
import { computed } 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 { useTrafficLineStation } from '@/composables/useTrafficLineStation'
|
||||
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
|
|
@ -53,7 +53,6 @@ const props = defineProps({
|
|||
})
|
||||
|
||||
const emit = defineEmits(['update:line', 'update:station', 'update:month'])
|
||||
const lineStationList = metroMenuData.lineStationList || []
|
||||
|
||||
const selectedLine = computed({
|
||||
get: () => props.line,
|
||||
|
|
@ -70,23 +69,11 @@ const selectedMonth = computed({
|
|||
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 { lineOptions, stationOptions } = useTrafficLineStation(
|
||||
() => props.line,
|
||||
() => props.station,
|
||||
emit
|
||||
)
|
||||
|
||||
const lineDisplayText = computed(() => {
|
||||
const currentLine = lineOptions.value.find(item => item.value === props.line)
|
||||
|
|
@ -109,7 +96,7 @@ const lineTheme = computed(() => {
|
|||
|
||||
const themeStyle = computed(() => ({
|
||||
'--metro-bg': lineTheme.value.bg,
|
||||
'--metro-text': lineTheme.value.color,
|
||||
'--metro-text': lineTheme.value.color
|
||||
}))
|
||||
|
||||
const getFilterItemStyle = (text, type) => {
|
||||
|
|
@ -125,18 +112,6 @@ const getFilterItemStyle = (text, type) => {
|
|||
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>
|
||||
|
|
|
|||
|
|
@ -7,21 +7,27 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import TrafficRankRow from './TrafficRankRow.vue'
|
||||
import server from '@/utils/service'
|
||||
import { mapStationRankList } from '@/utils/traffic'
|
||||
|
||||
const rankList = ref([
|
||||
{ rank: 1, lines: '1,3,7,15', station: '上海火车站', type: '进出站', today: '8.40', compare: '6.20', percent: 92 },
|
||||
{ rank: 2, lines: '1,3,7,15', station: '上海体育场', type: '进出站', today: '8.20', compare: '5.20', percent: 80 },
|
||||
{ rank: 3, lines: '1,3,7,15', station: '延安西路', type: '进出站', today: '6.40', compare: '3.20', percent: 70 },
|
||||
{ rank: 4, lines: '1,3,7,15', station: '上大路', type: '进出站', today: '6.20', compare: '6.20', percent: 50 },
|
||||
{ rank: 5, lines: '1,3,7,15', station: '上海体育馆', type: '进出站', today: '6.20', compare: '6.20', percent: 40 },
|
||||
{ rank: 6, lines: '1,3,7,15', station: '上海南站', type: '进出站', today: '6.20', compare: '6.20', percent: 38 },
|
||||
{ rank: 7, lines: '1,3,7,15', station: '大渡河路', type: '进出站', today: '6.20', compare: '6.20', percent: 38 },
|
||||
{ rank: 8, lines: '1,3,7,15', station: '大渡河路', type: '进出站', today: '6.20', compare: '6.20', percent: 38 },
|
||||
{ rank: 9, lines: '1,3,7,15', station: '红宝石路', type: '进出站', today: '6.20', compare: '6.20', percent: 38 },
|
||||
{ rank: 10, lines: '1,3,7,15', station: '吴中路', type: '进出站', today: '6.20', compare: '6.20', percent: 10 }
|
||||
])
|
||||
const rankList = ref([])
|
||||
|
||||
const fetchRankList = async () => {
|
||||
try {
|
||||
const response = await server.getInOutStationRank()
|
||||
if (response.data.success) {
|
||||
rankList.value = mapStationRankList(response.data.data, '进出站')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取进出站客流排名失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRankList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -6,21 +6,27 @@
|
|||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import TrafficRankRow from './TrafficRankRow.vue'
|
||||
import server from '@/utils/service'
|
||||
import { mapStationRankList } from '@/utils/traffic'
|
||||
|
||||
const rankList = ref([
|
||||
{ rank: 1, lines: '1,3,7,15', station: '上海火车站', type: '进出站', today: '18.40', compare: '6.20', percent: 92 },
|
||||
{ rank: 2, lines: '3,7', station: '上海体育场', type: '进出站', today: '18.20', compare: '5.20', percent: 80 },
|
||||
{ rank: 3, lines: '1,3,7,15', station: '延安西路', type: '进出站', today: '16.40', compare: '3.20', percent: 70 },
|
||||
{ rank: 4, lines: '1,3,7', station: '上大路', type: '进出站', today: '6.20', compare: '6.20', percent: 50 },
|
||||
{ rank: 5, lines: '1,15', station: '上海体育馆', type: '进出站', today: '6.20', compare: '6.20', percent: 40 },
|
||||
{ rank: 6, lines: '1,3,7,15', station: '上海南站', type: '进出站', today: '6.20', compare: '6.20', percent: 38 },
|
||||
{ rank: 7, lines: '1,3,15', station: '大渡河路', type: '进出站', today: '6.20', compare: '6.20', percent: 38 },
|
||||
{ rank: 8, lines: '1,3,7,15', station: '大渡河路', type: '进出站', today: '6.20', compare: '6.20', percent: 38 },
|
||||
{ rank: 9, lines: '1,3', station: '红宝石路', type: '进出站', today: '6.20', compare: '6.20', percent: 38 },
|
||||
{ rank: 10, lines: '15', station: '吴中路', type: '进出站', today: '6.20', compare: '6.20', percent: 10 }
|
||||
])
|
||||
const rankList = ref([])
|
||||
|
||||
const fetchRankList = async () => {
|
||||
try {
|
||||
const response = await server.getTransferStationRank()
|
||||
if (response.data.success) {
|
||||
rankList.value = mapStationRankList(response.data.data, '换乘站')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取换乘站客流排名失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRankList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<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--compare">对比日{{ compareDateLabel }}</span>
|
||||
<span class="legend-item legend-item--history">历史峰值</span>
|
||||
<span class="legend-unit">单位:万人次</span>
|
||||
</div>
|
||||
|
|
@ -35,45 +35,38 @@ 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:///imgs/traffic/4-item.png'
|
||||
const pointGlowSymbol = `image://${pointGlowSymbolBase64}`
|
||||
|
||||
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]
|
||||
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)
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
lineCards.value[0].total = '8846081'
|
||||
}, 8000);
|
||||
|
||||
const chartCards = computed(() => {
|
||||
return lineCards.value.map(item => ({
|
||||
|
|
@ -82,7 +75,7 @@ const chartCards = computed(() => {
|
|||
}))
|
||||
})
|
||||
|
||||
const targetTotals = computed(() => lineCards.value.map(item => item.total.split('')))
|
||||
const targetTotals = computed(() => lineCards.value.map(item => String(item.total || '0').split('')))
|
||||
const digitEls = []
|
||||
const odometerInstances = []
|
||||
|
||||
|
|
@ -132,6 +125,7 @@ watch(targetTotals, async () => {
|
|||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchLinePassengerFlow()
|
||||
await nextTick()
|
||||
initOdometers()
|
||||
requestAnimationFrame(updateOdometers)
|
||||
|
|
@ -149,7 +143,11 @@ const getLineStyle = lineId => {
|
|||
}
|
||||
}
|
||||
|
||||
const createChartOption = item => ({
|
||||
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: {
|
||||
|
|
@ -184,8 +182,8 @@ const createChartOption = item => ({
|
|||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 5000,
|
||||
interval: 1000,
|
||||
max: yAxisMax,
|
||||
interval: yAxisMax / 5,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
|
|
@ -196,7 +194,8 @@ const createChartOption = item => ({
|
|||
color: 'rgba(226, 239, 255, 0.96)',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
margin: 10
|
||||
margin: 10,
|
||||
formatter: value => (value / 10000).toFixed(0)
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
|
|
@ -248,7 +247,7 @@ const createChartOption = item => ({
|
|||
}
|
||||
},
|
||||
{
|
||||
name: '对比日(2026-03-05)',
|
||||
name: compareDateLabel.value || '对比日',
|
||||
type: 'line',
|
||||
data: item.compare,
|
||||
smooth: false,
|
||||
|
|
@ -281,7 +280,8 @@ const createChartOption = item => ({
|
|||
fontSize: 12
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<section class="traffic-4">
|
||||
<StationFilter class="traffic-4__filter" v-model:line="selectedLine" v-model:station="selectedStation"
|
||||
/>
|
||||
<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>
|
||||
|
|
@ -9,31 +8,56 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed ,ref} from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import StationFilter from './StationFilter.vue'
|
||||
import VChart from 'vue-echarts'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
|
||||
import server from '@/utils/service'
|
||||
import { processStationPassengerFlow, calcChartYAxisMax } from '@/utils/traffic'
|
||||
import { pointGlowSymbolBase64 } from '../assets/4-item.base64.js'
|
||||
import { pointGlowSymbol1Base64 } from '../assets/4-item1.base64.js'
|
||||
|
||||
const selectedLine = ref('3')
|
||||
const selectedStation = ref('0313')
|
||||
|
||||
|
||||
|
||||
const hours = ['5', '7', '9', '11', '13', '15', '17', '19', '21', '23']
|
||||
const pointGlowSymbol = `image://${pointGlowSymbolBase64}`
|
||||
const pointGlowSymbol1 = `image://${pointGlowSymbol1Base64}`
|
||||
|
||||
const topActual = [800, 900, 2100, 4050, 1500, 1450, 1200, 2300, 900, 900]
|
||||
const topCompare = [1400, 1800, 3400, 4500, 2950, 2600, 2800, 4300, 4300, 4850]
|
||||
const topActual = ref([])
|
||||
const topCompare = ref([])
|
||||
const bottomActual = ref([])
|
||||
const bottomCompare = ref([])
|
||||
|
||||
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 fetchStationFlow = async () => {
|
||||
if (!selectedStation.value) return
|
||||
|
||||
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]])
|
||||
try {
|
||||
const response = await server.getStationPassengerFlow(selectedStation.value)
|
||||
if (response.data.success) {
|
||||
const processed = processStationPassengerFlow(response.data.data)
|
||||
if (!processed) return
|
||||
topActual.value = processed.inOut.actual
|
||||
topCompare.value = processed.inOut.compare
|
||||
bottomActual.value = processed.transfer.actual
|
||||
bottomCompare.value = processed.transfer.compare
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取车站客流详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedStation, () => {
|
||||
fetchStationFlow()
|
||||
}, { immediate: true })
|
||||
|
||||
const topPointGlow = computed(() => hours.map((hour, index) => [hour, topActual.value[index] || 0]))
|
||||
const bottomPointGlow = computed(() => hours.map((hour, index) => [hour, bottomActual.value[index] || 0]))
|
||||
|
||||
const yAxisMaxTop = computed(() => calcChartYAxisMax(topActual.value, topCompare.value))
|
||||
const yAxisMaxBottom = computed(() => calcChartYAxisMax(bottomActual.value, bottomCompare.value))
|
||||
const yAxisIntervalTop = computed(() => yAxisMaxTop.value / 5)
|
||||
const yAxisIntervalBottom = computed(() => yAxisMaxBottom.value / 5)
|
||||
|
||||
const chartOption = computed(() => ({
|
||||
backgroundColor: 'transparent',
|
||||
|
|
@ -63,21 +87,15 @@ const chartOption = computed(() => ({
|
|||
boundaryGap: false,
|
||||
data: hours,
|
||||
gridIndex: 0,
|
||||
axisLine: {
|
||||
show: true
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLine: { show: true },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(226, 239, 255, 0.95)',
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
margin: 12
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
}
|
||||
splitLine: { show: false }
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
|
|
@ -85,35 +103,26 @@ const chartOption = computed(() => ({
|
|||
data: hours,
|
||||
gridIndex: 1,
|
||||
position: 'top',
|
||||
axisLine: {
|
||||
show: true
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
}
|
||||
axisLine: { show: true },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { show: false },
|
||||
splitLine: { show: false }
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
gridIndex: 0,
|
||||
interval: 1000,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
min: 0,
|
||||
max: yAxisMaxTop.value,
|
||||
interval: yAxisIntervalTop.value,
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(222, 236, 255, 0.9)',
|
||||
fontSize: 12,
|
||||
margin: 10
|
||||
margin: 10,
|
||||
formatter: value => (value / 10000).toFixed(0)
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
|
|
@ -127,18 +136,17 @@ const chartOption = computed(() => ({
|
|||
{
|
||||
type: 'value',
|
||||
gridIndex: 1,
|
||||
interval: 1000,
|
||||
min: 0,
|
||||
max: yAxisMaxBottom.value,
|
||||
interval: yAxisIntervalBottom.value,
|
||||
inverse: true,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: {
|
||||
color: 'rgba(222, 236, 255, 0.9)',
|
||||
fontSize: 12,
|
||||
margin: 10
|
||||
margin: 10,
|
||||
formatter: value => (value / 10000).toFixed(0)
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
|
|
@ -155,38 +163,34 @@ const chartOption = computed(() => ({
|
|||
type: 'scatter',
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
data: topPointGlow,
|
||||
data: topPointGlow.value,
|
||||
symbol: pointGlowSymbol,
|
||||
symbolSize: [47, 109],
|
||||
symbolOffset: [4, 0],
|
||||
symbolKeepAspect: false,
|
||||
silent: false,
|
||||
z: 4,
|
||||
tooltip: {
|
||||
show: false
|
||||
}
|
||||
tooltip: { show: false }
|
||||
},
|
||||
{
|
||||
type: 'scatter',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
data: bottomPointGlow,
|
||||
data: bottomPointGlow.value,
|
||||
symbol: pointGlowSymbol1,
|
||||
symbolSize: [47, 109],
|
||||
symbolOffset: [4, 0],
|
||||
symbolKeepAspect: true,
|
||||
silent: true,
|
||||
z: 4,
|
||||
tooltip: {
|
||||
show: false
|
||||
}
|
||||
tooltip: { show: false }
|
||||
},
|
||||
{
|
||||
name: '进出站',
|
||||
type: 'line',
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
data: topActual,
|
||||
data: topActual.value,
|
||||
smooth: false,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
|
|
@ -219,7 +223,7 @@ const chartOption = computed(() => ({
|
|||
type: 'line',
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
data: topCompare,
|
||||
data: topCompare.value,
|
||||
smooth: false,
|
||||
symbol: 'none',
|
||||
z: 4,
|
||||
|
|
@ -235,7 +239,7 @@ const chartOption = computed(() => ({
|
|||
type: 'line',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
data: bottomActual,
|
||||
data: bottomActual.value,
|
||||
smooth: false,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
|
|
@ -268,7 +272,7 @@ const chartOption = computed(() => ({
|
|||
type: 'line',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
data: bottomCompare,
|
||||
data: bottomCompare.value,
|
||||
smooth: false,
|
||||
symbol: 'none',
|
||||
z: 4,
|
||||
|
|
@ -289,7 +293,7 @@ const chartOption = computed(() => ({
|
|||
},
|
||||
formatter: params => {
|
||||
const { name, seriesName, value } = params[0]
|
||||
return `${name}时<br>${seriesName}: ${value}`
|
||||
return `${name}时<br>${seriesName}: ${(value / 10000).toFixed(2)}万`
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
|
@ -305,8 +309,6 @@ const chartOption = computed(() => ({
|
|||
background: url('/imgs/traffic/4-bg.png') no-repeat center center;
|
||||
background-size: cover;
|
||||
|
||||
|
||||
|
||||
.traffic-4__filter {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@
|
|||
/>
|
||||
|
||||
<div class="activity-list">
|
||||
|
||||
<article v-for="activity in activityList" :key="activity.name" class="activity-card">
|
||||
<article v-for="activity in activityList" :key="activity.id" class="activity-card">
|
||||
<div class="activity-status" :class="`activity-status--${activity.statusType}`">
|
||||
{{ activity.status }}
|
||||
</div>
|
||||
|
|
@ -31,58 +30,62 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import StationTimeFilter from './StationTimeFilter.vue'
|
||||
import server from '@/utils/service'
|
||||
import { formatActivityMonth, processBigActivityList } from '@/utils/traffic'
|
||||
|
||||
const selectedLine = ref('7')
|
||||
const selectedStation = ref('0753')
|
||||
const selectedMonth = ref('2026-5')
|
||||
const selectedMonth = ref(dayjs().format('YYYY-M'))
|
||||
const activityList = ref([])
|
||||
const lineApiIdMap = ref({})
|
||||
|
||||
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'
|
||||
const loadLineApiIdMap = async () => {
|
||||
try {
|
||||
const response = await server.getLineList()
|
||||
if (response.data.success && response.data.data?.length) {
|
||||
lineApiIdMap.value = Object.fromEntries(
|
||||
response.data.data.map(item => [String(item.lineId), String(item.id)])
|
||||
)
|
||||
}
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('获取线路列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchActivityList = async () => {
|
||||
const metroLine = lineApiIdMap.value[selectedLine.value]
|
||||
if (!metroLine || !selectedStation.value || !selectedMonth.value) {
|
||||
activityList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await server.getBigActivityList({
|
||||
metroLine,
|
||||
metroStation: selectedStation.value,
|
||||
activityMonth: formatActivityMonth(selectedMonth.value)
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
activityList.value = processBigActivityList(response.data.data)
|
||||
} else {
|
||||
activityList.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取重大活动清单失败:', error)
|
||||
activityList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
watch([selectedLine, selectedStation, selectedMonth], fetchActivityList)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadLineApiIdMap()
|
||||
await fetchActivityList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -58,8 +58,7 @@ const rankingList = [
|
|||
{ rank: 1, lineId: 3, count: 20 },
|
||||
{ rank: 2, lineId: 4, count: 2 },
|
||||
{ rank: 3, lineId: 7, count: 11 },
|
||||
{ rank: 4, lineId: 41, count: 11 },
|
||||
{ rank: 5, lineId: 51, count: 11 }
|
||||
{ rank: 4, lineId: 11, count: 11 }
|
||||
]
|
||||
|
||||
const getLineTheme = lineId => {
|
||||
|
|
|
|||