diff --git a/.env.development b/.env.development index fd7f64f..eab18b8 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,3 @@ # 开发环境配置 -# 使用相对路径,通过 vite proxy 代理到目标服务器 +# 留空则通过 vite proxy 代理到 http://101.133.172.2:8082 VITE_API_BASE_URL= diff --git a/.gitignore b/.gitignore index 76add87..83e8060 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,53 @@ +# 依赖 node_modules -dist \ No newline at end of file + +# 构建产物 +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-* diff --git a/src/composables/useTrafficLineStation.js b/src/composables/useTrafficLineStation.js new file mode 100644 index 0000000..8cfa4c7 --- /dev/null +++ b/src/composables/useTrafficLineStation.js @@ -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 } +} diff --git a/src/utils/service.js b/src/utils/service.js index 21732d5..3bc572e 100644 --- a/src/utils/service.js +++ b/src/utils/service.js @@ -1,10 +1,9 @@ import { request } from './request' import { encrypt } from './decrypt' -import axios from 'axios' +import { ysdpPost } from './ysdpRequest' var ipRoot = "http://140.206.138.190:8900" var ip = "http://140.206.138.190:8905" -var ysdpmIp = import.meta.env.VITE_API_BASE_URL || "http://192.168.16.193:8888" var getToken = "/app/getToken" var getTblMetroInout = ipRoot + '/api/xckf/getTblMetroInout/v1' @@ -22,14 +21,6 @@ var getBusInfo = ipRoot + '/api/xckf/getBusInfo/v1' var updayeBusEntrance = ipRoot + '/api/xckf/updayeBusEntrance/v1' var getHotEventList = ipRoot + '/api/xckf/getHotEventList/v1' var updateHotEvent = ipRoot + '/api/xckf/updateHotEvent/v1' -var getOperateInfo = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getOperateInfo/v1' -var getCommonMonitor = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getCommonMonitor/v1' -var getDutyList = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getDutyList/v1' -var getStationMaxList = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getStationMaxList/v1' -var getIndicator = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getIndicator/v1' -var getYunyingInfo = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getYunyingInfo/v1' -var getQxjGdybHour = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getQxjGdybHour/v1' -var getTaskNotice = (import.meta.env.DEV ? '' : import.meta.env.VITE_API_BASE_URL) + '/juntech-ysdp/api/getTaskNotice/v1' var xcImg = ip + '/1734.jpg' var xcImgGd = ip + '/lineCrossImg/1734.jpg' @@ -142,35 +133,67 @@ var server = { }, getOperateInfo() { - return axios.post(getOperateInfo, {}) + return ysdpPost('/api/getOperateInfo/v1') }, getCommonMonitor() { - return axios.post(getCommonMonitor, {}) + return ysdpPost('/api/getCommonMonitor/v1') }, getDutyList() { - return axios.post(getDutyList, {}) + return ysdpPost('/api/getDutyList/v1') }, getStationMaxList() { - return axios.post(getStationMaxList, {}) + return ysdpPost('/api/getStationMaxList/v1') }, getIndicator() { - return axios.post(getIndicator, {}) + return ysdpPost('/api/getIndicator/v1') }, getQxjGdybHour() { - return axios.post(getQxjGdybHour, {}) + return ysdpPost('/api/getQxjGdybHour/v1') }, getYunyingInfo() { - return axios.post(getYunyingInfo, {}) + return ysdpPost('/api/getYunyingInfo/v1') }, getTaskNotice() { - return axios.post(getTaskNotice, {}) + return ysdpPost('/api/getTaskNotice/v1') + }, + + getInOutStationRank() { + return ysdpPost('/api/traffic/getInOutStationRank/v1') + }, + + getTransferStationRank() { + return ysdpPost('/api/traffic/getTransferStationRank/v1') + }, + + getLineList() { + return ysdpPost('/api/traffic/getLineList/v1') + }, + + getLinePassengerFlow() { + return ysdpPost('/api/traffic/getLinePassengerFlow/v1') + }, + + getStationPassengerFlow(stationId) { + return ysdpPost('/api/traffic/getStationPassengerFlow/v1', { stationId }) + }, + + getStationsByLine(lineId) { + return ysdpPost('/api/traffic/getStationsByLine/v1', { lineId }) + }, + + cleanTrafficCache() { + return ysdpPost('/api/traffic/cleanCache') + }, + + cleanApiCache() { + return ysdpPost('/api/cleanApiCache') } } diff --git a/src/utils/traffic.js b/src/utils/traffic.js new file mode 100644 index 0000000..548282d --- /dev/null +++ b/src/utils/traffic.js @@ -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 + } + }) +} diff --git a/src/utils/ysdpRequest.js b/src/utils/ysdpRequest.js new file mode 100644 index 0000000..4c0c313 --- /dev/null +++ b/src/utils/ysdpRequest.js @@ -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 diff --git a/src/utils/yunying.js b/src/utils/yunying.js new file mode 100644 index 0000000..d503da9 --- /dev/null +++ b/src/utils/yunying.js @@ -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 + } + }) +} diff --git a/src/views/home/components/ModuleOne.vue b/src/views/home/components/ModuleOne.vue index ed927d1..b0d2fbf 100644 --- a/src/views/home/components/ModuleOne.vue +++ b/src/views/home/components/ModuleOne.vue @@ -48,7 +48,8 @@ const fetchOperateInfo = async () => { const response = await server.getOperateInfo() if (response.data.success) { const { level: levelData, begindate } = response.data.data - level.value = levelData + const levelMatch = String(levelData).match(/\d+/) + level.value = levelMatch ? levelMatch[0] : levelData const now = new Date() const begin = new Date(begindate) diff --git a/src/views/home/components/ModuleSix.vue b/src/views/home/components/ModuleSix.vue index 47363c8..bbbc26e 100644 --- a/src/views/home/components/ModuleSix.vue +++ b/src/views/home/components/ModuleSix.vue @@ -39,6 +39,7 @@ import { ref, onMounted, nextTick } from 'vue' import * as echarts from 'echarts' import server from '@/utils/service' +import { processYunyingInfo } from '@/utils/yunying' const chartRefs = ref([]) @@ -55,8 +56,6 @@ const charts = ref(chartDataList.value) const chartInstances = ref([]) -const timeKeys = ['time1', 'time2', 'time3', 'time4', 'time5', 'time6', 'time7', 'time8', 'time9', 'time10'] - const getNumberBoxColor = (number) => { const colors = { 3: '#FBD61D', @@ -99,60 +98,18 @@ const getBaifenbiColorStyle = (chart) => { } } const processData = (dataList) => { - // 根据当前系统时间计算对应的 time 索引 - const getCurrentTimeIndex = () => { - const now = new Date() - const hour = now.getHours() - const minutes = now.getMinutes() - // 5 点和 6 点对应 time1,7 点和 8 点对应 time2,以此类推 - // time1: 5-6, time2: 7-8, time3: 9-10, time4: 11-12, time5: 13-14 - // time6: 15-16, time7: 17-18, time8: 19-20, time9: 21-22, time10: 23-24 - if (hour < 5) return 0 - // 如果当前小时是偶数且分钟大于 0,或者当前小时是奇数,都算下一个时间段 - // 例如:10:30,10 是偶数,但已经过了 10 点,应该算 time4 - // 11:00-11:59 也算 time4,12:00-12:59 也算 time4 - const timeIndex = Math.ceil((hour - 5) / 2) - const result = Math.min(timeIndex, 9) // 最大到 time10 (索引 9) - console.log(`当前时间:${hour}:${minutes},计算得到的 time 索引:${result}, 对应 ${timeKeys[result]}`) - return result - } - - const currentTimeIndex = getCurrentTimeIndex() - - return dataList.map(item => { - const getValues = (dataObj) => { - return timeKeys.map(key => { - const value = dataObj[key] - return value ? parseFloat(value) : 0 - }) - } - - const barData = getValues(item.today) - const yellowData = getValues(item.compareDay) - const redData = getValues(item.maxDay) - - // 根据当前时间获取对应的 maxDay 值和 compareDay 值 - const maxValue = redData[currentTimeIndex] - const compareMaxValue = yellowData[currentTimeIndex] - const totalSum = item.today?.totalSum ? parseFloat(item.today.totalSum) : 0 - - const total = barData.reduce((sum, val) => sum + val, 0) - const totalStr = Math.floor(total).toString() - const digits = totalStr.split('').map(Number) - - return { - barData, - yellowData, - redData, - number: parseInt(item.line), - digits: digits.length > 0 ? digits : [0], - compareDate: item.compareDate, - maxDate: item.maxDay?.maxDate || '', - maxValue: maxValue, - compareMaxValue: compareMaxValue, - totalSum: totalSum - } - }) + return processYunyingInfo(dataList).map(item => ({ + barData: item.barData, + yellowData: item.yellowData, + redData: item.redData, + number: item.line, + digits: item.digits.length > 0 ? item.digits : [0], + compareDate: item.compareDate, + maxDate: item.maxDate, + maxValue: item.maxValue, + compareMaxValue: item.compareMaxValue, + totalSum: item.totalSum + })) } const fetchYunyingInfo = async () => { diff --git a/src/views/traffic/components/StationFilter.vue b/src/views/traffic/components/StationFilter.vue index 38de5e4..11f3ea6 100644 --- a/src/views/traffic/components/StationFilter.vue +++ b/src/views/traffic/components/StationFilter.vue @@ -18,10 +18,9 @@ \ No newline at end of file + diff --git a/src/views/traffic/components/StationTimeFilter.vue b/src/views/traffic/components/StationTimeFilter.vue index 12572fb..7343a9a 100644 --- a/src/views/traffic/components/StationTimeFilter.vue +++ b/src/views/traffic/components/StationTimeFilter.vue @@ -26,12 +26,12 @@ \ No newline at end of file + diff --git a/src/views/traffic/components/Traffic1.vue b/src/views/traffic/components/Traffic1.vue index 8e218e1..e1d6edb 100644 --- a/src/views/traffic/components/Traffic1.vue +++ b/src/views/traffic/components/Traffic1.vue @@ -7,21 +7,27 @@