对接运三大屏开发环境接口,完善客运页数据展示

统一 ysdp 请求封装并接入首页/客运页接口,新增线路与单站客流对接,补充 gitignore 与开发环境配置说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
main
崔方鑫 2026-08-27 17:20:28 +08:00
parent 763956d7ed
commit 35440a9493
15 changed files with 527 additions and 295 deletions

View File

@ -1,3 +1,3 @@
# 开发环境配置 # 开发环境配置
# 使用相对路径,通过 vite proxy 代理到目标服务器 # 留空则通过 vite proxy 代理到 http://101.133.172.2:8082
VITE_API_BASE_URL= VITE_API_BASE_URL=

53
.gitignore vendored
View File

@ -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-*

View File

@ -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 }
}

View File

@ -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,67 @@ 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 })
},
cleanTrafficCache() {
return ysdpPost('/api/traffic/cleanCache')
},
cleanApiCache() {
return ysdpPost('/api/cleanApiCache')
} }
} }

70
src/utils/traffic.js Normal file
View File

@ -0,0 +1,70 @@
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)
}
/** 将站点排名接口数据映射为 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
}
})
}

21
src/utils/ysdpRequest.js Normal file
View File

@ -0,0 +1,21 @@
import axios from 'axios'
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

52
src/utils/yunying.js Normal file
View File

@ -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
}
})
}

View File

@ -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)

View File

@ -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 time17 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 compareDate: item.compareDate,
// time6: 15-16, time7: 17-18, time8: 19-20, time9: 21-22, time10: 23-24 maxDate: item.maxDate,
if (hour < 5) return 0 maxValue: item.maxValue,
// 0 compareMaxValue: item.compareMaxValue,
// 10:3010 10 time4 totalSum: item.totalSum
// 11:00-11:59 time412: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,
maxDate: item.maxDay?.maxDate || '',
maxValue: maxValue,
compareMaxValue: compareMaxValue,
totalSum: totalSum
}
})
} }
const fetchYunyingInfo = async () => { const fetchYunyingInfo = async () => {

View File

@ -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,15 +70,15 @@ 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) => {
const contentLength = Array.from(String(text || '')).length const contentLength = Array.from(String(text || '')).length
const rules = { const rules = {
line: { padding: 18, charWidth: 20 }, line: { padding: 18, charWidth: 20 },
station: { padding: 18, charWidth: 20 }, station: { padding: 18, charWidth: 20 },
month: { padding: 44, charWidth: 12 } month: { padding: 44, charWidth: 12 }
} }
const rule = rules[type] const rule = rules[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>
@ -200,4 +169,4 @@ watch(stationOptions, options => {
transform: translateY(-50%); transform: translateY(-50%);
pointer-events: none; pointer-events: none;
} }
</style> </style>

View File

@ -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,15 +96,15 @@ 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) => {
const contentLength = Array.from(String(text || '')).length const contentLength = Array.from(String(text || '')).length
const rules = { const rules = {
line: { padding: 18, charWidth: 20 }, line: { padding: 18, charWidth: 20 },
station: { padding: 18, charWidth: 20 }, station: { padding: 18, charWidth: 20 },
month: { padding: 44, charWidth: 12 } month: { padding: 44, charWidth: 12 }
} }
const rule = rules[type] const rule = rules[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>
@ -229,4 +204,4 @@ watch(stationOptions, options => {
transform: translateY(-50%); transform: translateY(-50%);
pointer-events: none; pointer-events: none;
} }
</style> </style>

View File

@ -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>

View File

@ -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>

View File

@ -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,37 @@ 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'
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:///imgs/traffic/4-item.png'
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] }
}, } catch (error) {
{ console.error('获取线路客流信息失败:', error)
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]
} }
]) }
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 +74,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 +124,7 @@ watch(targetTotals, async () => {
}) })
onMounted(async () => { onMounted(async () => {
await fetchLinePassengerFlow()
await nextTick() await nextTick()
initOdometers() initOdometers()
requestAnimationFrame(updateOdometers) requestAnimationFrame(updateOdometers)
@ -149,7 +142,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 +181,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 +193,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 +246,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 +279,8 @@ const createChartOption = item => ({
fontSize: 12 fontSize: 12
} }
} }
}) }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -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,54 @@
</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'
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 topActual = [800, 900, 2100, 4050, 1500, 1450, 1200, 2300, 900, 900]
const topCompare = [1400, 1800, 3400, 4500, 2950, 2600, 2800, 4300, 4300, 4850]
const bottomActual = [4800, 4400, 3000, 1600, 3150, 3500, 3350, 1850, 1450, 1650]
const bottomCompare = [5400, 5300, 4900, 2200, 4700, 5000, 3850, 4700, 5350, 5350 ]
const pointGlowSymbol = 'image:///imgs/traffic/4-item.png' const pointGlowSymbol = 'image:///imgs/traffic/4-item.png'
const pointGlowSymbol1 = 'image:///imgs/traffic/4-item1.png' const pointGlowSymbol1 = 'image:///imgs/traffic/4-item1.png'
const topPointGlow = hours.map((hour, index) => [hour, topActual[index]])
const bottomPointGlow = hours.map((hour, index) => [hour, bottomActual[index]])
const topActual = ref([])
const topCompare = ref([])
const bottomActual = ref([])
const bottomCompare = ref([])
const fetchStationFlow = async () => {
if (!selectedStation.value) return
try {
const response = await server.getStationPassengerFlow(selectedStation.value)
if (response.data.success) {
const processed = processStationPassengerFlow(response.data.data)
if (!processed) return
topActual.value = processed.inOut.actual
topCompare.value = processed.inOut.compare
bottomActual.value = processed.transfer.actual
bottomCompare.value = processed.transfer.compare
}
} catch (error) {
console.error('获取车站客流详情失败:', error)
}
}
watch(selectedStation, () => {
fetchStationFlow()
}, { immediate: true })
const topPointGlow = computed(() => hours.map((hour, index) => [hour, topActual.value[index] || 0]))
const bottomPointGlow = computed(() => hours.map((hour, index) => [hour, bottomActual.value[index] || 0]))
const yAxisMaxTop = computed(() => calcChartYAxisMax(topActual.value, topCompare.value))
const yAxisMaxBottom = computed(() => calcChartYAxisMax(bottomActual.value, bottomCompare.value))
const yAxisIntervalTop = computed(() => yAxisMaxTop.value / 5)
const yAxisIntervalBottom = computed(() => yAxisMaxBottom.value / 5)
const chartOption = computed(() => ({ const chartOption = computed(() => ({
backgroundColor: 'transparent', backgroundColor: 'transparent',
@ -63,21 +85,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 +101,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 +134,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 +161,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 +221,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 +237,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 +270,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 +291,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 +307,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;