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=
|
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
|
node_modules
|
||||||
|
|
||||||
|
# 构建产物
|
||||||
dist
|
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: {
|
glowImage: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '/imgs/guangyun.png'
|
default: './imgs/guangyun.png'
|
||||||
},
|
},
|
||||||
// 是否显示虚线光圈
|
// 是否显示虚线光圈
|
||||||
showRing: {
|
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',
|
name: 'Traffic',
|
||||||
component: () => import('@/views/traffic/index.vue'),
|
component: () => import('@/views/traffic/index.vue'),
|
||||||
meta: { title: '客运', requiresAuth: true }
|
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 }
|
meta: { requiresAuth: true }
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { request } from './request'
|
import { request } from './request'
|
||||||
import { encrypt } from './decrypt'
|
import { encrypt } from './decrypt'
|
||||||
import axios from 'axios'
|
import { ysdpPost } from './ysdpRequest'
|
||||||
|
|
||||||
var ipRoot = "http://140.206.138.190:8900"
|
var ipRoot = "http://140.206.138.190:8900"
|
||||||
var ip = "http://140.206.138.190:8905"
|
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 getToken = "/app/getToken"
|
||||||
var getTblMetroInout = ipRoot + '/api/xckf/getTblMetroInout/v1'
|
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 updayeBusEntrance = ipRoot + '/api/xckf/updayeBusEntrance/v1'
|
||||||
var getHotEventList = ipRoot + '/api/xckf/getHotEventList/v1'
|
var getHotEventList = ipRoot + '/api/xckf/getHotEventList/v1'
|
||||||
var updateHotEvent = ipRoot + '/api/xckf/updateHotEvent/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 xcImg = ip + '/1734.jpg'
|
||||||
var xcImgGd = ip + '/lineCrossImg/1734.jpg'
|
var xcImgGd = ip + '/lineCrossImg/1734.jpg'
|
||||||
|
|
||||||
|
|
@ -142,35 +133,75 @@ var server = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getOperateInfo() {
|
getOperateInfo() {
|
||||||
return axios.post(getOperateInfo, {})
|
return ysdpPost('/api/getOperateInfo/v1')
|
||||||
},
|
},
|
||||||
|
|
||||||
getCommonMonitor() {
|
getCommonMonitor() {
|
||||||
return axios.post(getCommonMonitor, {})
|
return ysdpPost('/api/getCommonMonitor/v1')
|
||||||
},
|
},
|
||||||
|
|
||||||
getDutyList() {
|
getDutyList() {
|
||||||
return axios.post(getDutyList, {})
|
return ysdpPost('/api/getDutyList/v1')
|
||||||
},
|
},
|
||||||
|
|
||||||
getStationMaxList() {
|
getStationMaxList() {
|
||||||
return axios.post(getStationMaxList, {})
|
return ysdpPost('/api/getStationMaxList/v1')
|
||||||
},
|
},
|
||||||
|
|
||||||
getIndicator() {
|
getIndicator() {
|
||||||
return axios.post(getIndicator, {})
|
return ysdpPost('/api/getIndicator/v1')
|
||||||
},
|
},
|
||||||
|
|
||||||
getQxjGdybHour() {
|
getQxjGdybHour() {
|
||||||
return axios.post(getQxjGdybHour, {})
|
return ysdpPost('/api/getQxjGdybHour/v1')
|
||||||
},
|
},
|
||||||
|
|
||||||
getYunyingInfo() {
|
getYunyingInfo() {
|
||||||
return axios.post(getYunyingInfo, {})
|
return ysdpPost('/api/getYunyingInfo/v1')
|
||||||
},
|
},
|
||||||
|
|
||||||
getTaskNotice() {
|
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>
|
<template>
|
||||||
<div class="dashboard-header">
|
<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">
|
<div class="header-title">
|
||||||
<img src="/imgs/title.png" alt="title" class="title-img" />
|
<img src="/imgs/title.png" alt="title" class="title-img" />
|
||||||
</div>
|
</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="header-time">{{ currentTime }}</div>
|
||||||
<div class="notice-marquee" v-if="noticeList.length > 0">
|
<div class="notice-marquee" v-if="noticeList.length > 0">
|
||||||
<div class="marquee-content" :class="{ 'marquee-scroll': noticeList.length > 1 }">
|
<div class="marquee-content" :class="{ 'marquee-scroll': noticeList.length > 1 }">
|
||||||
|
|
@ -46,6 +26,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||||
|
import { Pouring } from '@element-plus/icons-vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import server from '@/utils/service'
|
import server from '@/utils/service'
|
||||||
|
|
||||||
|
|
@ -66,11 +47,25 @@ const currentNoticeIndex = ref(0)
|
||||||
let noticeTimer = null
|
let noticeTimer = null
|
||||||
let timeTimer = null
|
let timeTimer = null
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ label: '常规', to: '/home' },
|
||||||
|
{ label: '客运', to: '/traffic' },
|
||||||
|
{ label: '行车', to: '/driving' },
|
||||||
|
{ label: '施工', to: '/construction' }
|
||||||
|
]
|
||||||
|
|
||||||
const displayNoticeList = computed(() => {
|
const displayNoticeList = computed(() => {
|
||||||
if (noticeList.value.length === 0) return []
|
if (noticeList.value.length === 0) return []
|
||||||
return [noticeList.value[currentNoticeIndex.value]]
|
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 updateTime = () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
currentTime.value = now.toLocaleString('zh-CN', {
|
currentTime.value = now.toLocaleString('zh-CN', {
|
||||||
|
|
@ -133,19 +128,65 @@ onUnmounted(() => {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.dashboard-header {
|
.dashboard-header {
|
||||||
width: 4800px;
|
position: absolute;
|
||||||
height: 106px;
|
left: -2px;
|
||||||
position: relative;
|
top: -36px;
|
||||||
|
width: 4802px;
|
||||||
|
height: 150px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: url('/imgs/top.png') no-repeat center 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);
|
// 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 {
|
.header-title {
|
||||||
.title-img {
|
.title-img {
|
||||||
|
margin-top: 18px;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +195,7 @@ onUnmounted(() => {
|
||||||
.header-time {
|
.header-time {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
// right: 40px;
|
// right: 40px;
|
||||||
top: 110px;
|
top: 148px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
font-family: BlackOpsOne, sans-serif;
|
font-family: BlackOpsOne, sans-serif;
|
||||||
font-size: 44px;
|
font-size: 44px;
|
||||||
|
|
@ -224,29 +265,29 @@ onUnmounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.weather-info {
|
.weather-info {
|
||||||
position: absolute;
|
|
||||||
left: 1000px;
|
|
||||||
top: 70%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
|
|
||||||
.weather-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
height: 58px;
|
||||||
line-height: 45px;
|
margin-left: 46px;
|
||||||
|
gap: 14px;
|
||||||
.label {
|
white-space: nowrap;
|
||||||
font-size: 36px;
|
|
||||||
color: #FFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
font-size: 36px;
|
|
||||||
color: #fff;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
<!-- <div class="module-title">模块一</div> -->
|
<!-- <div class="module-title">模块一</div> -->
|
||||||
<div class="module-body">
|
<div class="module-body">
|
||||||
<div class="pyramid-3d-wrapper">
|
<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"
|
:rotation-speed="0.2" :auto-rotate="true" :width="252" :height="166" @loaded="onPyramidLoaded"
|
||||||
@error="onPyramidError" />
|
@error="onPyramidError" />
|
||||||
</div>
|
</div>
|
||||||
<div class="pyramid-3d-wrapper2">
|
<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"
|
:rotation-speed="0.2" :auto-rotate="true" :width="252" :height="166" @loaded="onPyramidLoaded"
|
||||||
@error="onPyramidError" />
|
@error="onPyramidError" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -48,7 +48,8 @@ const fetchOperateInfo = async () => {
|
||||||
const response = await server.getOperateInfo()
|
const response = await server.getOperateInfo()
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
const { level: levelData, begindate } = response.data.data
|
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 now = new Date()
|
||||||
const begin = new Date(begindate)
|
const begin = new Date(begindate)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
import { ref, onMounted, nextTick } from 'vue'
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
import server from '@/utils/service'
|
import server from '@/utils/service'
|
||||||
|
import { processYunyingInfo } from '@/utils/yunying'
|
||||||
|
|
||||||
const chartRefs = ref([])
|
const chartRefs = ref([])
|
||||||
|
|
||||||
|
|
@ -55,8 +56,6 @@ const charts = ref(chartDataList.value)
|
||||||
|
|
||||||
const chartInstances = ref([])
|
const chartInstances = ref([])
|
||||||
|
|
||||||
const timeKeys = ['time1', 'time2', 'time3', 'time4', 'time5', 'time6', 'time7', 'time8', 'time9', 'time10']
|
|
||||||
|
|
||||||
const getNumberBoxColor = (number) => {
|
const getNumberBoxColor = (number) => {
|
||||||
const colors = {
|
const colors = {
|
||||||
3: '#FBD61D',
|
3: '#FBD61D',
|
||||||
|
|
@ -99,60 +98,18 @@ const getBaifenbiColorStyle = (chart) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const processData = (dataList) => {
|
const processData = (dataList) => {
|
||||||
// 根据当前系统时间计算对应的 time 索引
|
return processYunyingInfo(dataList).map(item => ({
|
||||||
const getCurrentTimeIndex = () => {
|
barData: item.barData,
|
||||||
const now = new Date()
|
yellowData: item.yellowData,
|
||||||
const hour = now.getHours()
|
redData: item.redData,
|
||||||
const minutes = now.getMinutes()
|
number: item.line,
|
||||||
// 5 点和 6 点对应 time1,7 点和 8 点对应 time2,以此类推
|
digits: item.digits.length > 0 ? item.digits : [0],
|
||||||
// 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],
|
|
||||||
compareDate: item.compareDate,
|
compareDate: item.compareDate,
|
||||||
maxDate: item.maxDay?.maxDate || '',
|
maxDate: item.maxDate,
|
||||||
maxValue: maxValue,
|
maxValue: item.maxValue,
|
||||||
compareMaxValue: compareMaxValue,
|
compareMaxValue: item.compareMaxValue,
|
||||||
totalSum: totalSum
|
totalSum: item.totalSum
|
||||||
}
|
}))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchYunyingInfo = async () => {
|
const fetchYunyingInfo = async () => {
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,21 @@ import DashboardHeader from '@/views/home/components/DashboardHeader.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.layout-container {
|
.layout-container {
|
||||||
width: 4800px;
|
width: 4800px;
|
||||||
height: 1620px;
|
height: 1620px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: url('/imgs/mainBg.webp') no-repeat center center;
|
background: url('/imgs/mainBg.webp') no-repeat center center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.route-content {
|
.route-content {
|
||||||
width: 4800px;
|
width: 4800px;
|
||||||
height: 1514px;
|
height: 1514px;
|
||||||
position: relative;
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, watch } from 'vue'
|
import { computed } from 'vue'
|
||||||
import metroMenuData from '@/utils/lineStationList.json'
|
|
||||||
import { METRO_LINES } from '@/utils/const'
|
import { METRO_LINES } from '@/utils/const'
|
||||||
|
import { useTrafficLineStation } from '@/composables/useTrafficLineStation'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
line: {
|
line: {
|
||||||
|
|
@ -31,12 +30,10 @@ const props = defineProps({
|
||||||
station: {
|
station: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:line', 'update:station'])
|
const emit = defineEmits(['update:line', 'update:station'])
|
||||||
const lineStationList = metroMenuData.lineStationList || []
|
|
||||||
|
|
||||||
const selectedLine = computed({
|
const selectedLine = computed({
|
||||||
get: () => props.line,
|
get: () => props.line,
|
||||||
|
|
@ -48,25 +45,11 @@ const selectedStation = computed({
|
||||||
set: value => emit('update:station', value)
|
set: value => emit('update:station', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { lineOptions, stationOptions } = useTrafficLineStation(
|
||||||
|
() => props.line,
|
||||||
const lineOptions = computed(() => {
|
() => props.station,
|
||||||
return lineStationList.map(item => ({
|
emit
|
||||||
label: item.nameCn,
|
)
|
||||||
value: item.lineId
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
const stationOptions = computed(() => {
|
|
||||||
const currentLine = lineStationList.find(item => item.lineId === selectedLine.value)
|
|
||||||
const stationList = currentLine?.stationList || []
|
|
||||||
|
|
||||||
return stationList.map(item => ({
|
|
||||||
label: item.nameCn,
|
|
||||||
value: item.statId,
|
|
||||||
line: item.line
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
const lineDisplayText = computed(() => {
|
const lineDisplayText = computed(() => {
|
||||||
const currentLine = lineOptions.value.find(item => item.value === props.line)
|
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 || '')
|
return String(currentStation?.label || stationOptions.value[0]?.label || '')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const lineTheme = computed(() => {
|
const lineTheme = computed(() => {
|
||||||
return METRO_LINES[Number(selectedLine.value)] || {
|
return METRO_LINES[Number(selectedLine.value)] || {
|
||||||
bg: '#ED6E00',
|
bg: '#ED6E00',
|
||||||
|
|
@ -89,7 +70,7 @@ const lineTheme = computed(() => {
|
||||||
|
|
||||||
const themeStyle = computed(() => ({
|
const themeStyle = computed(() => ({
|
||||||
'--metro-bg': lineTheme.value.bg,
|
'--metro-bg': lineTheme.value.bg,
|
||||||
'--metro-text': lineTheme.value.color,
|
'--metro-text': lineTheme.value.color
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const getFilterItemStyle = (text, type) => {
|
const getFilterItemStyle = (text, type) => {
|
||||||
|
|
@ -105,18 +86,6 @@ const getFilterItemStyle = (text, type) => {
|
||||||
width: `${Math.max(rule.padding + contentLength * rule.charWidth)}px`
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, watch } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { Calendar } from '@element-plus/icons-vue'
|
import { Calendar } from '@element-plus/icons-vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
import metroMenuData from '@/utils/lineStationList.json'
|
|
||||||
import { METRO_LINES } from '@/utils/const'
|
import { METRO_LINES } from '@/utils/const'
|
||||||
|
import { useTrafficLineStation } from '@/composables/useTrafficLineStation'
|
||||||
|
|
||||||
import 'dayjs/locale/zh-cn'
|
import 'dayjs/locale/zh-cn'
|
||||||
|
|
||||||
|
|
@ -53,7 +53,6 @@ const props = defineProps({
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:line', 'update:station', 'update:month'])
|
const emit = defineEmits(['update:line', 'update:station', 'update:month'])
|
||||||
const lineStationList = metroMenuData.lineStationList || []
|
|
||||||
|
|
||||||
const selectedLine = computed({
|
const selectedLine = computed({
|
||||||
get: () => props.line,
|
get: () => props.line,
|
||||||
|
|
@ -70,23 +69,11 @@ const selectedMonth = computed({
|
||||||
set: value => emit('update:month', value)
|
set: value => emit('update:month', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const lineOptions = computed(() => {
|
const { lineOptions, stationOptions } = useTrafficLineStation(
|
||||||
return lineStationList.map(item => ({
|
() => props.line,
|
||||||
label: item.nameCn,
|
() => props.station,
|
||||||
value: item.lineId
|
emit
|
||||||
}))
|
)
|
||||||
})
|
|
||||||
|
|
||||||
const stationOptions = computed(() => {
|
|
||||||
const currentLine = lineStationList.find(item => item.lineId === selectedLine.value)
|
|
||||||
const stationList = currentLine?.stationList || []
|
|
||||||
|
|
||||||
return stationList.map(item => ({
|
|
||||||
label: item.nameCn,
|
|
||||||
value: item.statId,
|
|
||||||
line: item.line
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
const lineDisplayText = computed(() => {
|
const lineDisplayText = computed(() => {
|
||||||
const currentLine = lineOptions.value.find(item => item.value === props.line)
|
const currentLine = lineOptions.value.find(item => item.value === props.line)
|
||||||
|
|
@ -109,7 +96,7 @@ const lineTheme = computed(() => {
|
||||||
|
|
||||||
const themeStyle = computed(() => ({
|
const themeStyle = computed(() => ({
|
||||||
'--metro-bg': lineTheme.value.bg,
|
'--metro-bg': lineTheme.value.bg,
|
||||||
'--metro-text': lineTheme.value.color,
|
'--metro-text': lineTheme.value.color
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const getFilterItemStyle = (text, type) => {
|
const getFilterItemStyle = (text, type) => {
|
||||||
|
|
@ -125,18 +112,6 @@ const getFilterItemStyle = (text, type) => {
|
||||||
width: `${Math.max(rule.padding + contentLength * rule.charWidth)}px`
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,27 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import TrafficRankRow from './TrafficRankRow.vue'
|
import TrafficRankRow from './TrafficRankRow.vue'
|
||||||
|
import server from '@/utils/service'
|
||||||
|
import { mapStationRankList } from '@/utils/traffic'
|
||||||
|
|
||||||
const rankList = ref([
|
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 },
|
const fetchRankList = async () => {
|
||||||
{ rank: 3, lines: '1,3,7,15', station: '延安西路', type: '进出站', today: '6.40', compare: '3.20', percent: 70 },
|
try {
|
||||||
{ rank: 4, lines: '1,3,7,15', station: '上大路', type: '进出站', today: '6.20', compare: '6.20', percent: 50 },
|
const response = await server.getInOutStationRank()
|
||||||
{ rank: 5, lines: '1,3,7,15', station: '上海体育馆', type: '进出站', today: '6.20', compare: '6.20', percent: 40 },
|
if (response.data.success) {
|
||||||
{ rank: 6, lines: '1,3,7,15', station: '上海南站', type: '进出站', today: '6.20', compare: '6.20', percent: 38 },
|
rankList.value = mapStationRankList(response.data.data, '进出站')
|
||||||
{ 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 },
|
} catch (error) {
|
||||||
{ rank: 9, lines: '1,3,7,15', station: '红宝石路', type: '进出站', today: '6.20', compare: '6.20', percent: 38 },
|
console.error('获取进出站客流排名失败:', error)
|
||||||
{ rank: 10, lines: '1,3,7,15', station: '吴中路', type: '进出站', today: '6.20', compare: '6.20', percent: 10 }
|
}
|
||||||
])
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchRankList()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,27 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import TrafficRankRow from './TrafficRankRow.vue'
|
import TrafficRankRow from './TrafficRankRow.vue'
|
||||||
|
import server from '@/utils/service'
|
||||||
|
import { mapStationRankList } from '@/utils/traffic'
|
||||||
|
|
||||||
const rankList = ref([
|
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 },
|
const fetchRankList = async () => {
|
||||||
{ rank: 3, lines: '1,3,7,15', station: '延安西路', type: '进出站', today: '16.40', compare: '3.20', percent: 70 },
|
try {
|
||||||
{ rank: 4, lines: '1,3,7', station: '上大路', type: '进出站', today: '6.20', compare: '6.20', percent: 50 },
|
const response = await server.getTransferStationRank()
|
||||||
{ rank: 5, lines: '1,15', station: '上海体育馆', type: '进出站', today: '6.20', compare: '6.20', percent: 40 },
|
if (response.data.success) {
|
||||||
{ rank: 6, lines: '1,3,7,15', station: '上海南站', type: '进出站', today: '6.20', compare: '6.20', percent: 38 },
|
rankList.value = mapStationRankList(response.data.data, '换乘站')
|
||||||
{ 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 },
|
} catch (error) {
|
||||||
{ rank: 9, lines: '1,3', station: '红宝石路', type: '进出站', today: '6.20', compare: '6.20', percent: 38 },
|
console.error('获取换乘站客流排名失败:', error)
|
||||||
{ rank: 10, lines: '15', station: '吴中路', type: '进出站', today: '6.20', compare: '6.20', percent: 10 }
|
}
|
||||||
])
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchRankList()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<section class="traffic-3">
|
<section class="traffic-3">
|
||||||
<div class="chart-legend">
|
<div class="chart-legend">
|
||||||
<span class="legend-item legend-item--actual">实际</span>
|
<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-item legend-item--history">历史峰值</span>
|
||||||
<span class="legend-unit">单位:万人次</span>
|
<span class="legend-unit">单位:万人次</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -35,45 +35,38 @@ import * as echarts from 'echarts'
|
||||||
import Odometer from 'odometer'
|
import Odometer from 'odometer'
|
||||||
import 'odometer/themes/odometer-theme-default.css'
|
import 'odometer/themes/odometer-theme-default.css'
|
||||||
import { METRO_LINES } from '@/utils/const'
|
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 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([
|
const lineCards = ref([])
|
||||||
{
|
const compareDate = ref('')
|
||||||
lineId: 3,
|
|
||||||
total: '2746782',
|
const compareDateLabel = computed(() => {
|
||||||
actual: [2600, 4700, 2400, 3900, 2300, 4100, 2600, 3300, 1700, 2200],
|
return compareDate.value ? `(${compareDate.value})` : ''
|
||||||
compare: [4850, 4550, 4650, 4300, 4300, 4550, 4700, 3900, 3900, 3000],
|
})
|
||||||
history: [5000, 4600, 4700, 4200, 4200, 4450, 4600, 4300, 3900, 3600]
|
|
||||||
},
|
const fetchLinePassengerFlow = async () => {
|
||||||
{
|
try {
|
||||||
lineId: 4,
|
const response = await server.getLinePassengerFlow()
|
||||||
total: '2746782',
|
if (response.data.success) {
|
||||||
actual: [700, 950, 1550, 4200, 1700, 1350, 1600, 1550, 2500, 1400],
|
const processed = processYunyingInfo(response.data.data)
|
||||||
compare: [1150, 1450, 2200, 3900, 2500, 1550, 1700, 1650, 2950, 2050],
|
compareDate.value = processed[0]?.compareDate || ''
|
||||||
history: [1350, 2200, 3100, 4450, 3000, 2650, 2550, 2750, 4200, 4550]
|
lineCards.value = processed.map(item => ({
|
||||||
},
|
lineId: item.lineId,
|
||||||
{
|
total: String(Math.floor(item.totalSum || 0)),
|
||||||
lineId: 7,
|
actual: item.actual,
|
||||||
total: '2746782',
|
compare: item.compare,
|
||||||
actual: [2900, 3300, 5000, 2300, 2000, 1900, 2600, 4050, 1700, 1700],
|
history: item.history
|
||||||
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]
|
|
||||||
}
|
}
|
||||||
])
|
} catch (error) {
|
||||||
|
console.error('获取线路客流信息失败:', error)
|
||||||
|
}
|
||||||
setTimeout(() => {
|
}
|
||||||
lineCards.value[0].total = '8846081'
|
|
||||||
}, 8000);
|
|
||||||
|
|
||||||
const chartCards = computed(() => {
|
const chartCards = computed(() => {
|
||||||
return lineCards.value.map(item => ({
|
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 digitEls = []
|
||||||
const odometerInstances = []
|
const odometerInstances = []
|
||||||
|
|
||||||
|
|
@ -132,6 +125,7 @@ watch(targetTotals, async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await fetchLinePassengerFlow()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
initOdometers()
|
initOdometers()
|
||||||
requestAnimationFrame(updateOdometers)
|
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',
|
backgroundColor: 'transparent',
|
||||||
animationDuration: 900,
|
animationDuration: 900,
|
||||||
grid: {
|
grid: {
|
||||||
|
|
@ -184,8 +182,8 @@ const createChartOption = item => ({
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 5000,
|
max: yAxisMax,
|
||||||
interval: 1000,
|
interval: yAxisMax / 5,
|
||||||
axisLine: {
|
axisLine: {
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
|
|
@ -196,7 +194,8 @@ const createChartOption = item => ({
|
||||||
color: 'rgba(226, 239, 255, 0.96)',
|
color: 'rgba(226, 239, 255, 0.96)',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
margin: 10
|
margin: 10,
|
||||||
|
formatter: value => (value / 10000).toFixed(0)
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
|
|
@ -248,7 +247,7 @@ const createChartOption = item => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '对比日(2026-03-05)',
|
name: compareDateLabel.value || '对比日',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: item.compare,
|
data: item.compare,
|
||||||
smooth: false,
|
smooth: false,
|
||||||
|
|
@ -281,7 +280,8 @@ const createChartOption = item => ({
|
||||||
fontSize: 12
|
fontSize: 12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="traffic-4">
|
<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">
|
<div class="chart-shell">
|
||||||
<v-chart class="traffic-4__chart" :option="chartOption" autoresize />
|
<v-chart class="traffic-4__chart" :option="chartOption" autoresize />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -9,31 +8,56 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed ,ref} from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import StationFilter from './StationFilter.vue'
|
import StationFilter from './StationFilter.vue'
|
||||||
import VChart from 'vue-echarts'
|
import VChart from 'vue-echarts'
|
||||||
import * as echarts from '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 selectedLine = ref('3')
|
||||||
const selectedStation = ref('0313')
|
const selectedStation = ref('0313')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const hours = ['5', '7', '9', '11', '13', '15', '17', '19', '21', '23']
|
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 topActual = ref([])
|
||||||
const topCompare = [1400, 1800, 3400, 4500, 2950, 2600, 2800, 4300, 4300, 4850]
|
const topCompare = ref([])
|
||||||
|
const bottomActual = ref([])
|
||||||
|
const bottomCompare = ref([])
|
||||||
|
|
||||||
const bottomActual = [4800, 4400, 3000, 1600, 3150, 3500, 3350, 1850, 1450, 1650]
|
const fetchStationFlow = async () => {
|
||||||
const bottomCompare = [5400, 5300, 4900, 2200, 4700, 5000, 3850, 4700, 5350, 5350 ]
|
if (!selectedStation.value) return
|
||||||
|
|
||||||
const pointGlowSymbol = 'image:///imgs/traffic/4-item.png'
|
try {
|
||||||
const pointGlowSymbol1 = 'image:///imgs/traffic/4-item1.png'
|
const response = await server.getStationPassengerFlow(selectedStation.value)
|
||||||
const topPointGlow = hours.map((hour, index) => [hour, topActual[index]])
|
if (response.data.success) {
|
||||||
const bottomPointGlow = hours.map((hour, index) => [hour, bottomActual[index]])
|
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(() => ({
|
const chartOption = computed(() => ({
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
|
|
@ -63,21 +87,15 @@ const chartOption = computed(() => ({
|
||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
data: hours,
|
data: hours,
|
||||||
gridIndex: 0,
|
gridIndex: 0,
|
||||||
axisLine: {
|
axisLine: { show: true },
|
||||||
show: true
|
axisTick: { show: false },
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: 'rgba(226, 239, 255, 0.95)',
|
color: 'rgba(226, 239, 255, 0.95)',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
margin: 12
|
margin: 12
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: { show: false }
|
||||||
show: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
|
|
@ -85,35 +103,26 @@ const chartOption = computed(() => ({
|
||||||
data: hours,
|
data: hours,
|
||||||
gridIndex: 1,
|
gridIndex: 1,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
axisLine: {
|
axisLine: { show: true },
|
||||||
show: true
|
axisTick: { show: false },
|
||||||
},
|
axisLabel: { show: false },
|
||||||
axisTick: {
|
splitLine: { show: false }
|
||||||
show: false
|
|
||||||
},
|
|
||||||
axisLabel: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
gridIndex: 0,
|
gridIndex: 0,
|
||||||
interval: 1000,
|
min: 0,
|
||||||
axisLine: {
|
max: yAxisMaxTop.value,
|
||||||
show: false
|
interval: yAxisIntervalTop.value,
|
||||||
},
|
axisLine: { show: false },
|
||||||
axisTick: {
|
axisTick: { show: false },
|
||||||
show: false
|
|
||||||
},
|
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: 'rgba(222, 236, 255, 0.9)',
|
color: 'rgba(222, 236, 255, 0.9)',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
margin: 10
|
margin: 10,
|
||||||
|
formatter: value => (value / 10000).toFixed(0)
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
|
|
@ -127,18 +136,17 @@ const chartOption = computed(() => ({
|
||||||
{
|
{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
gridIndex: 1,
|
gridIndex: 1,
|
||||||
interval: 1000,
|
min: 0,
|
||||||
|
max: yAxisMaxBottom.value,
|
||||||
|
interval: yAxisIntervalBottom.value,
|
||||||
inverse: true,
|
inverse: true,
|
||||||
axisLine: {
|
axisLine: { show: false },
|
||||||
show: false
|
axisTick: { show: false },
|
||||||
},
|
|
||||||
axisTick: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: 'rgba(222, 236, 255, 0.9)',
|
color: 'rgba(222, 236, 255, 0.9)',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
margin: 10
|
margin: 10,
|
||||||
|
formatter: value => (value / 10000).toFixed(0)
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
|
|
@ -155,38 +163,34 @@ const chartOption = computed(() => ({
|
||||||
type: 'scatter',
|
type: 'scatter',
|
||||||
xAxisIndex: 0,
|
xAxisIndex: 0,
|
||||||
yAxisIndex: 0,
|
yAxisIndex: 0,
|
||||||
data: topPointGlow,
|
data: topPointGlow.value,
|
||||||
symbol: pointGlowSymbol,
|
symbol: pointGlowSymbol,
|
||||||
symbolSize: [47, 109],
|
symbolSize: [47, 109],
|
||||||
symbolOffset: [4, 0],
|
symbolOffset: [4, 0],
|
||||||
symbolKeepAspect: false,
|
symbolKeepAspect: false,
|
||||||
silent: false,
|
silent: false,
|
||||||
z: 4,
|
z: 4,
|
||||||
tooltip: {
|
tooltip: { show: false }
|
||||||
show: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'scatter',
|
type: 'scatter',
|
||||||
xAxisIndex: 1,
|
xAxisIndex: 1,
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
data: bottomPointGlow,
|
data: bottomPointGlow.value,
|
||||||
symbol: pointGlowSymbol1,
|
symbol: pointGlowSymbol1,
|
||||||
symbolSize: [47, 109],
|
symbolSize: [47, 109],
|
||||||
symbolOffset: [4,0],
|
symbolOffset: [4, 0],
|
||||||
symbolKeepAspect: true,
|
symbolKeepAspect: true,
|
||||||
silent: true,
|
silent: true,
|
||||||
z: 4,
|
z: 4,
|
||||||
tooltip: {
|
tooltip: { show: false }
|
||||||
show: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '进出站',
|
name: '进出站',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
xAxisIndex: 0,
|
xAxisIndex: 0,
|
||||||
yAxisIndex: 0,
|
yAxisIndex: 0,
|
||||||
data: topActual,
|
data: topActual.value,
|
||||||
smooth: false,
|
smooth: false,
|
||||||
symbol: 'circle',
|
symbol: 'circle',
|
||||||
symbolSize: 6,
|
symbolSize: 6,
|
||||||
|
|
@ -219,7 +223,7 @@ const chartOption = computed(() => ({
|
||||||
type: 'line',
|
type: 'line',
|
||||||
xAxisIndex: 0,
|
xAxisIndex: 0,
|
||||||
yAxisIndex: 0,
|
yAxisIndex: 0,
|
||||||
data: topCompare,
|
data: topCompare.value,
|
||||||
smooth: false,
|
smooth: false,
|
||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
z: 4,
|
z: 4,
|
||||||
|
|
@ -235,7 +239,7 @@ const chartOption = computed(() => ({
|
||||||
type: 'line',
|
type: 'line',
|
||||||
xAxisIndex: 1,
|
xAxisIndex: 1,
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
data: bottomActual,
|
data: bottomActual.value,
|
||||||
smooth: false,
|
smooth: false,
|
||||||
symbol: 'circle',
|
symbol: 'circle',
|
||||||
symbolSize: 6,
|
symbolSize: 6,
|
||||||
|
|
@ -268,7 +272,7 @@ const chartOption = computed(() => ({
|
||||||
type: 'line',
|
type: 'line',
|
||||||
xAxisIndex: 1,
|
xAxisIndex: 1,
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
data: bottomCompare,
|
data: bottomCompare.value,
|
||||||
smooth: false,
|
smooth: false,
|
||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
z: 4,
|
z: 4,
|
||||||
|
|
@ -289,7 +293,7 @@ const chartOption = computed(() => ({
|
||||||
},
|
},
|
||||||
formatter: params => {
|
formatter: params => {
|
||||||
const { name, seriesName, value } = params[0]
|
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: url('/imgs/traffic/4-bg.png') no-repeat center center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.traffic-4__filter {
|
.traffic-4__filter {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="activity-list">
|
<div class="activity-list">
|
||||||
|
<article v-for="activity in activityList" :key="activity.id" class="activity-card">
|
||||||
<article v-for="activity in activityList" :key="activity.name" class="activity-card">
|
|
||||||
<div class="activity-status" :class="`activity-status--${activity.statusType}`">
|
<div class="activity-status" :class="`activity-status--${activity.statusType}`">
|
||||||
{{ activity.status }}
|
{{ activity.status }}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -31,58 +30,62 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
import StationTimeFilter from './StationTimeFilter.vue'
|
import StationTimeFilter from './StationTimeFilter.vue'
|
||||||
|
import server from '@/utils/service'
|
||||||
|
import { formatActivityMonth, processBigActivityList } from '@/utils/traffic'
|
||||||
|
|
||||||
const selectedLine = ref('7')
|
const selectedLine = ref('7')
|
||||||
const selectedStation = ref('0753')
|
const selectedStation = ref('0753')
|
||||||
const selectedMonth = ref('2026-5')
|
const selectedMonth = ref(dayjs().format('YYYY-M'))
|
||||||
|
const activityList = ref([])
|
||||||
|
const lineApiIdMap = ref({})
|
||||||
|
|
||||||
const activityList = [
|
const loadLineApiIdMap = async () => {
|
||||||
{
|
try {
|
||||||
name: 'CBE中国美容博览会',
|
const response = await server.getLineList()
|
||||||
time: '2026/05/12--2026/05/14',
|
if (response.data.success && response.data.data?.length) {
|
||||||
venue: '新国际博览中心',
|
lineApiIdMap.value = Object.fromEntries(
|
||||||
status: '进行中',
|
response.data.data.map(item => [String(item.lineId), String(item.id)])
|
||||||
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'
|
|
||||||
}
|
}
|
||||||
]
|
} 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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,7 @@ const rankingList = [
|
||||||
{ rank: 1, lineId: 3, count: 20 },
|
{ rank: 1, lineId: 3, count: 20 },
|
||||||
{ rank: 2, lineId: 4, count: 2 },
|
{ rank: 2, lineId: 4, count: 2 },
|
||||||
{ rank: 3, lineId: 7, count: 11 },
|
{ rank: 3, lineId: 7, count: 11 },
|
||||||
{ rank: 4, lineId: 41, count: 11 },
|
{ rank: 4, lineId: 11, count: 11 }
|
||||||
{ rank: 5, lineId: 51, count: 11 }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const getLineTheme = lineId => {
|
const getLineTheme = lineId => {
|
||||||
|
|
|
||||||