个股中心,折线图

This commit is contained in:
renzhijun
2026-02-06 17:55:17 +08:00
parent 8445e95ba3
commit 890be2e3e9

View File

@@ -80,12 +80,12 @@
mode="widthFix"></image>
<view style="font-size: 28rpx; color: #2B2B2B; font-weight: bold; flex: 1; margin-left: 10rpx;">异动监控
</view>
<!-- <view @click="allAction(1)"
<view @click="allAction(1)"
style="border: 1rpx solid #DCDCDC; border-radius: 5rpx; padding: 2rpx 10rpx; display: flex; align-items: center; justify-content: center; margin: 0 10rpx;">
<view style="color: #888888; font-size: 22rpx; font-weight: 500;">全部</view>
<image style="width: 11rpx; height: 6rpx; margin-left: 40rpx;"
src="/static/icon/invest/downArrow.png" mode="widthFix"></image>
</view> -->
</view>
<view @click="allAction(2)"
style="border: 1rpx solid #DCDCDC; border-radius: 5rpx; padding: 2rpx 10rpx; display: flex; align-items: center; justify-content: center;">
<view style="color: #888888; font-size: 22rpx; font-weight: 500;">{{currentDate}}</view>
@@ -94,9 +94,25 @@
</view>
</view>
<!-- <view
style="height: 400rpx; display: flex; align-items: center; justify-content: center; background-color: red;">
折线图占位 </view> -->
<!-- 图表容器相对定位作为右侧最值的定位父级保留原有宽高 -->
<view style="width: 100%; height: 600rpx; position: relative; box-sizing: border-box;">
<l-echart ref="chartRef"></l-echart>
<!-- 右侧最上方最大值红色#EC3440带负号2位小数+% -->
<view
style="position: absolute; top: 0; right: 10rpx; font-size: 24rpx; color: #EC3440; z-index: 99; line-height: 1.2;margin-top: 60rpx;margin-right: 30rpx;"
>
{{y2MaxText}}
</view>
<!-- 右侧最下方最小值绿色#01AB5D带负号2位小数+% -->
<view
style="position: absolute; bottom: 0; right: 10rpx; font-size: 24rpx; color: #01AB5D; z-index: 99; line-height: 1.2;margin-bottom: 110rpx;margin-right: 30rpx;"
>
{{y2MinText}}
</view>
</view>
<view style="height: 1rpx; margin: 0 20rpx; background-color: #E7E7E7;"></view>
<view style="height: 88rpx; display: flex; align-items: center; margin: 0 20rpx;">
@@ -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)}`;
};
// 辅助函数310分钟分组核心逻辑→分组归类→组内降序→取第一条
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.datay轴值有效
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