diff --git a/pages/geGuCenter/geGuCenter.vue b/pages/geGuCenter/geGuCenter.vue index 979b111..4bfa81e 100644 --- a/pages/geGuCenter/geGuCenter.vue +++ b/pages/geGuCenter/geGuCenter.vue @@ -80,12 +80,12 @@ mode="widthFix"> 异动监控 - + {{currentDate}} @@ -94,9 +94,25 @@ - + + + + + + + + {{y2MaxText}} + + + + + {{y2MinText}} + + @@ -317,6 +333,7 @@ searchStockInfo, stockBasicInfo } from '@/request/api' + const echarts = require('../../uni_modules/lime-echart/static/echarts.min.js'); export default { data() { @@ -448,7 +465,13 @@ searchShow:false, //是否展示搜索结果 searchResultList:[], //搜索结果 selectSearchStockInfo:null, //选中的搜索股票信息 - isShowTime:false + isShowTime:false, + ec: { lazyLoad: true }, // 延迟加载 ECharts + chart: null, + y2MaxText: '', // 右侧顶部最大值文本(例:2.36% / -0.89%) + y2MinText: '' // 右侧底部最小值文本(例:-3.12% / 0.56%) + + } }, onLoad(e) { @@ -706,8 +729,9 @@ date: this.currentDate } marketHotspotOverview(param).then(res => { + const data = res?.data; const alerts = res?.data?.alerts || []; - +// ========== 处理指数涨跌 ========== const changePct = res.data.index.change_pct; let numPct = 0; // 校验数值有效性,转成数字类型 @@ -788,10 +812,282 @@ const sortedAlerts = processedAlerts.sort(sortByTimeDesc); // 赋值给页面变量的是处理+排序后的数组 this.marketAlertsList = sortedAlerts; + // ========== 初始化折线图 ========== + this.initChart(data.index.timeline, processedAlerts); }).catch(error => { }) }, + +async initChart(timeline, alerts) { + // 无数据直接return + if (!timeline || timeline.length === 0) return; + + const chart = await this.$refs.chartRef.init(echarts); + this.chartInstance = chart; + + // 1. 提取基础数据:X轴时间、Y轴价格、change_pct(用于右侧最值) + const xAxisTime = timeline.map(item => item.time?.trim() || ''); // 时间去空格,统一格式 + const yAxisPrice = timeline.map(item => Number(item.price) || 0); // 价格兜底0,避免NaN + // 提取timeline中的change_pct并转数字,过滤无效值 + const changePctList = timeline + .map(item => Number(item.change_pct)) + .filter(val => !isNaN(val) && val !== null && val !== undefined); + + // 2. 处理第一个Y轴(上证指数价格):动态计算最值+上下缓冲(增加空值判断,避免报错) + const validPrices = yAxisPrice.filter(val => val !== 0 && !isNaN(val)); + const priceMin = validPrices.length > 0 ? Math.min(...validPrices) : 0; + const priceMax = validPrices.length > 0 ? Math.max(...validPrices) : 0; + const priceRange = priceMax - priceMin; + const yAxisMin = priceRange > 0 ? (priceMin - priceRange * 0.1) : priceMin; + const yAxisMax = priceRange > 0 ? (priceMax + priceRange * 0.25) : priceMax; + + // 3. 处理change_pct最值:格式化(保留2位+带%+保留负号)赋值给页面变量 + let y2Min = 0, y2Max = 0; + if (changePctList.length > 0) { + y2Min = Math.min(...changePctList); + y2Max = Math.max(...changePctList); + this.y2MaxText = Number(y2Max).toFixed(2) + '%'; + this.y2MinText = Number(y2Min).toFixed(2) + '%'; + } else { + this.y2MaxText = '0.00%'; + this.y2MinText = '0.00%'; + } + + // 4. 告警点基础处理:同时间保留最高评分(小程序全兼容,带统计) + const alertObj = {}; + let totalAlert = 0; // 过滤后有效告警总数量 + let sameTimeCount = 0; // 相同时间的告警去重数量 + let sameScoreCount = 0; // 相同时间且相同评分数量 + + alerts.forEach(alert => { + if (!alert) return; // 跳过空对象 + const alertTime = alert.time?.trim() || ''; // 时间去空格,兜底空字符串 + const alertScore = Number(alert.importance_score); // 转数字 + if (alertTime === '' || isNaN(alertScore)) return; // 过滤空时间/非数字评分 + + // 时间匹配:xAxisTime也做去空格匹配 + const idx = xAxisTime.findIndex(t => t?.trim() === alertTime); + if (idx === -1) return; // 时间不匹配直接跳过 + + totalAlert++; + // 同时间保留最高评分核心逻辑 + if (!alertObj[alertTime]) { + alertObj[alertTime] = { ...alert, idx, importance_score: alertScore }; + } else { + sameTimeCount++; + const existAlert = alertObj[alertTime]; + if (alertScore > existAlert.importance_score) { + alertObj[alertTime] = { ...alert, idx, importance_score: alertScore }; + } else if (alertScore === existAlert.importance_score) { + sameScoreCount++; + } + } + }); + + // ===== 核心修改:10分钟时间段分组(如09:40-09:50),组内按评分降序取第一条 ===== + // 辅助函数1:将时间字符串(09:42)转为分钟数(便于计算分组) + const timeToMinutes = (timeStr) => { + const [hour, minute] = timeStr.split(':').map(Number); + return hour * 60 + minute; + }; + // 辅助函数2:根据分钟数,生成所属的10分钟时间段(如09:42→09:40-09:50) + const get10MinGroup = (minutes) => { + // 取整10分钟作为分组起点(如42分→40分,35分→30分,5分→0分) + const startMin = Math.floor(minutes / 10) * 10; + const endMin = startMin + 9; // 分组终点(起点+9分钟) + // 转成时间字符串并补0,返回「HH:MM-HH:MM」格式 + const formatTime = (m) => { + const h = Math.floor(m / 60).toString().padStart(2, '0'); + const mi = (m % 60).toString().padStart(2, '0'); + return `${h}:${mi}`; + }; + return `${formatTime(startMin)}-${formatTime(endMin)}`; + }; + // 辅助函数3:10分钟分组核心逻辑→分组归类→组内降序→取第一条 + const filterBy10MinGroup = (alertObj) => { + // 步骤1:提取alertObj中所有有效时间,转为[{group: '09:40-09:50', score: 90, data: 告警数据}, ...] + const alertGroupList = Object.keys(alertObj) + .filter(time => time && time.includes(':')) // 过滤无效时间 + .map(time => { + const minutes = timeToMinutes(time); + return { + group: get10MinGroup(minutes), // 所属10分钟分组 + score: alertObj[time].importance_score, // 告警评分 + data: alertObj[time] // 原始告警数据 + }; + }); + + if (alertGroupList.length === 0) return {}; // 无有效告警,直接返回空 + + // 步骤2:按分组聚合,key=分组名,value=该组下所有告警 + const groupMap = {}; + alertGroupList.forEach(item => { + if (!groupMap[item.group]) { + groupMap[item.group] = []; + } + groupMap[item.group].push(item); + }); + + // 步骤3:遍历每个分组,按评分**降序**排序,取第一条(评分最高的) + const finalAlertObj = {}; + Object.keys(groupMap).forEach(groupName => { + const groupItems = groupMap[groupName]; + // 降序排序(评分高的在前,相同评分保留原顺序) + const sortedItems = groupItems.sort((a, b) => b.score - a.score); + // 取分组内第一条(评分最高的),按原始时间作为key + const topItem = sortedItems[0]; + finalAlertObj[topItem.data.time] = topItem.data; + }); + + return finalAlertObj; + }; + + // 先执行基础去重,再执行10分钟分组筛选 + const filteredAlertObj = filterBy10MinGroup(alertObj); + + // ===== 多维度日志打印(更新分组统计,更直观)===== + const originalKeyLen = Object.keys(alertObj).length; // 基础去重后数量 + const filteredKeyLen = Object.keys(filteredAlertObj).length; // 10分钟分组后数量 + // 新增:打印分组详情(便于排查分组是否正确) + const groupDetail = Object.keys(filteredAlertObj).map(time => { + const minutes = timeToMinutes(time); + return { time, group: get10MinGroup(minutes), score: filteredAlertObj[time].importance_score }; + }); + console.log('===== 告警点处理全统计(10分钟分组版)====='); + console.log('1. 过滤后有效告警总数量:', totalAlert); + console.log('2. 相同时间的告警去重数量:', sameTimeCount); + console.log('3. 相同时间且相同评分数量:', sameScoreCount); + console.log('4. 基础去重后(同时间最高评分)数量:', originalKeyLen); + console.log('5. 10分钟分组后(每组取最高评分)数量:', filteredKeyLen); + console.log('6. 分组详情(时间→所属分组→评分):', groupDetail); + console.log('7. 分组后最终告警详情:', filteredAlertObj); + + // 5. 格式化ECharts所需的markPoint数据(保留原逻辑,兼容已修复的显示配置) + const alertPoints = Object.values(filteredAlertObj).map(alert => { + // 新增:校验索引有效性,避免x/y轴匹配失败(核心修复显示问题) + const validIdx = !isNaN(alert.idx) && alert.idx >= 0 && alert.idx < xAxisTime.length ? alert.idx : 0; + const xVal = xAxisTime[validIdx] || ''; + const yVal = !isNaN(yAxisPrice[validIdx]) ? yAxisPrice[validIdx] : 0; + return { + name: alert.concept_name || '未知概念', // 概念名兜底 + coord: [xVal, yVal], // 确保x轴值严格匹配xAxis.data,y轴值有效 + value:yVal, + itemStyle: { color: '#FF4444' }, // 告警点红色 + label: { + formatter(){ + return alert.concept_name + }, + show: true, + position: 'top', + fontSize: 10, + color: '#FF4444', + fontWeight: '500', + distance: 5 + } + + }; + }).filter(Boolean); // 最终兜底过滤无效项 + console.log('8. 最终ECharts告警点数据(10分钟分组):', alertPoints); + + // 6. ECharts核心配置项(保留所有显示修复:show/z/描边等) + const option = { + grid: { left: '4%', right: '8%', bottom: '8%', top: '10%', containLabel: true }, + xAxis: { + type: 'category', + boundaryGap: false, + data: xAxisTime, + axisLabel: { + fontSize: 12, + rotate: 30, + interval: Math.floor(xAxisTime.length / 6) + }, + axisTick: { + alignWithLabel: true, + interval: Math.floor(xAxisTime.length / 6) + } + }, + yAxis: [ + { + type: 'value', + min: yAxisMin, + max: yAxisMax, + nameTextStyle: { fontSize: 12 }, + axisLabel: { + formatter: (val) => val.toFixed(0), + fontSize: 12 + }, + splitLine: { lineStyle: { type: 'dashed', color: '#EEEEEE' } }, + boundaryGap: [0.05, 0.05] // 上下留5%缓冲,避免顶点告警点被裁剪 + } + ], + dataZoom: [], + series: [ + { + name: '上证指数', + type: 'line', + smooth: true, + symbol: 'circle', + symbolSize: 5, + itemStyle: { color: '#0092FF' }, + lineStyle: { + width: 2, + color: '#0092FF', + shadowColor: 'rgba(0,146,255,0.5)', + shadowBlur: 8, + shadowOffsetY: 3, + shadowOffsetX: 0 + }, + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: 'rgba(0,146,255,0.25)' }, + { offset: 1, color: 'rgba(0,146,255,0)' } + ]) + }, + data: yAxisPrice, + // 保留所有markPoint显示修复配置(强制显示/层级/样式) + markPoint: { + show: true, // 强制开启显示(关键!) + symbol: 'circle', + symbolSize:5, // 比折线大,避免被遮挡 + z: 10, // 层级置顶,不被任何元素遮挡 + + data: alertPoints, + itemStyle: { + color: '#FF4444', + borderColor: '#fff', // 白色描边,更醒目 + borderWidth: 1, + + }, + label: { + show: true, + position: 'top', + fontSize: 10, + color: '#FF4444', + fontWeight: '500', + distance: 6, + backgroundColor: 'rgba(255,255,255,0.8)', // 标签白色背景,防融合 + padding: [2, 4], + borderRadius: 2, + borderColor: '#FF4444', // 白色描边,更醒目 + borderWidth: 1, + } + }, + yAxisIndex: 0 + } + ], + + + }; +console.log('7. 分组后最终告警详情:', JSON.stringify(option.series)); + chart.setOption(option, true); + // 窗口自适应(优化:避免重复监听,页面销毁时可移除) + uni.onWindowResize(() => { + this.chartInstance && this.chartInstance.resize(); + }); +}, + + itemDetails(item) { uni.navigateTo({ url: '/pagesStock/stockCenterDetails/stockCenterDetails?code=' + item.stock_code