Files
JiaZhiQianYan/pages/geGuCenter/geGuCenter.vue
2026-02-06 17:55:27 +08:00

1277 lines
46 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view>
<navBar leftText="个股中心" hideNavBg hideBack></navBar>
<image class="topBg absolute" src="/static/image/index/conceptTopBg.png" mode="widthFix"></image>
<view class="searchC fixed flex" :style="'top:'+navH+'px;'">
<image class="icon" src="/static/icon/home/conceptCenter/search.png" mode="widthFix"></image>
<input class="flex1" type="text" v-model="keywords" placeholder="输入股票代码或名称"
placeholder-style="color:#eeeeee" confirm-type="search" @input="clickSearch()" />
</view>
<view v-if="searchShow" class="searchResultList fixed" :style="'top:'+searchResultTop+'px;'" @click="clickSearchResultBg()">
<view class="list">
<view class="item" v-for="(item,index) in searchResultList" :key="index" @click.stop="clickSearchResultListItem(item)">
{{item.stock_code}} {{item.stock_name}}
</view>
</view>
</view>
<scroll-view scroll-y class="stockDetailsC fixed" :style="'top:'+contentTop+'px;'">
<view>
<view style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16rpx; padding: 20rpx;">
<view v-for="(item,index) in topLists" :key="index"
style="padding: 20rpx 26rpx 26rpx 35rpx; border: 1rpx dashed #777777; overflow: hidden; position: relative;">
<image
style="position: absolute; top: 0; left: 0; bottom: 0; right: 0; width: 100%; height: 100%;"
:src="item.backIcon" mode="aspectFill"></image>
<view style="position: relative; z-index: 1;">
<view style="font-size: 24rpx; color: #777777; font-weight: 500;">{{item.title}}</view>
<view style="font-size: 30rpx; margin-top: 10rpx; font-weight: bold;"
:style="{color: item.color}">{{item.value}}</view>
</view>
</view>
</view>
<view style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16rpx; margin: 0 20rpx;">
<view @click="handleTypeClick(index)" v-for="(item,index) in topLists2" :key="index"
style="border: 1rpx solid #D2D2D2; padding: 12rpx;"
:style="{border: `1rpx solid ${list2Index == index ? '#F2C369' : '#D2D2D2'}`}">
<view style="font-size: 24rpx; color: #070707; font-weight: bold; text-align: center;"
:style="{color: (list2Index == index ? '#BB8520' : '#070707'), 'background-color': (list2Index == index ? '#FFFAF1' : '#FFFFFF')}">
{{item.title}}
</view>
<view style="font-size: 20rpx; font-weight: 400; text-align: center;"
:style="{color: (list2Index == index ? '#BB8520' : '#070707')}">{{item.value}}</view>
</view>
</view>
<!-- '股票名称', '涨跌幅', '市值', '成交额', '行业' -->
<view
style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 10rpx; background-color: #FAFAFC; line-height: 60rpx; margin: 0 20rpx; margin-top: 20rpx;">
<view v-for="(item,index) in ['股票名称', '涨跌幅', '市值', '成交额', '行业']" :key="index"
style="color: #666666; font-size: 20rpx; font-weight: 500; text-align: center;">
{{item}}
</view>
</view>
<!-- '股票名称', '涨跌幅', '市值', '成交额', '行业' 内容 -->
<view v-for="(obj, j) in filteredData" @click="itemDetails(obj)"
style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 10rpx; min-height: 60rpx; margin: 0 20rpx;"
:style="{'background-color': (j % 2 == 0 ? '#fff' : '#FAFAFC')}">
<!-- 外层循环每一行数据 -->
<view v-for="(item,index) in getTableItem(obj)" :key="index"
style="padding: 10rpx 0; color: #666666; font-size: 20rpx; font-weight: 500; text-align: center; display: flex; justify-content: center; align-items: center; flex-direction: column;"
:style="{ color: (index == 0 ? '#222222' : index == 1 ? (item[2] === 'positive' ? '#EC3440' : '#01AB5D') : '#666666') }">
<view>{{item[0]}}</view>
<view v-if="index == 0" style="color: #666666; font-size: 20rpx; font-weight: 500;">{{item[1]}}
</view>
</view>
</view>
<view @click="moreAction"
style="display: flex;align-items: center;justify-content: center; height: 80rpx;">
<view style="font-size: 24rpx; color: #3D455C; font-weight: 500;">查看更多</view>
<image style="width: 10rpx; height: 19rpx; margin-left: 20rpx;"
src="/static/icon/home/conceptCenter/next.png" mode="widthFix"></image>
</view>
<view style="height: 1rpx; margin: 0 20rpx; background-color: #E7E7E7;"></view>
<view style="height: 78rpx; display: flex; align-items: center; margin: 0 20rpx;">
<image style="width: 40rpx; height: 40rpx;" src="/pages/geGuCenter/icon/ydjk-icon.png"
mode="widthFix"></image>
<view style="font-size: 28rpx; color: #2B2B2B; font-weight: bold; flex: 1; margin-left: 10rpx;">异动监控
</view>
<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 @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>
<image style="width: 11rpx; height: 6rpx; margin-left: 20rpx;"
src="/static/icon/invest/downArrow.png" mode="widthFix"></image>
</view>
</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;">
<image style="width: 40rpx; height: 40rpx;" src="/pages/geGuCenter/icon/ydjk-icon.png"
mode="widthFix"></image>
<view style="font-size: 28rpx; color: #2B2B2B; font-weight: bold; flex: 1; margin-left: 10rpx;">
板块异动明细
</view>
</view>
<view v-for="(item,index) in marketAlertsList" :key="index" @click="bkydAction(item)"
style="margin: 20rpx; margin-top: 0; background-color: #FAFAFC; border-radius: 10rpx; overflow: hidden; padding: 20rpx 30rpx; font-weight: 500;">
<view style="color: #888888; font-size: 20rpx;">{{item.time}}</view>
<view style="display: flex; align-items: center; margin-top: 10rpx;">
<view style="color: #2B2B2B; font-weight: bold; font-size: 26rpx; margin-right: 10rpx;">
{{truncateText(item.concept_name, 5)}}
</view>
<view :style="{
color: alertTypeConfig[item.alert_type]?.color || '#FF7A45',
fontSize: '20rpx',
border: '1rpx solid ' + (alertTypeConfig[item.alert_type]?.color || '#FF7A45'),
borderRadius: '15rpx',
height: '30rpx',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0 10rpx',
boxSizing: 'border-box'
}">
<image style="width: 18rpx; height: auto;"
:style="{ filter: alertTypeConfig[item.alert_type]?.filter || '' }"
src="/pages/geGuCenter/icon/ydjk-zs.png" mode="widthFix">
</image>
<view style="margin-left: 10rpx;">
{{ alertTypeConfig[item.alert_type]?.text || '异动' }}
</view>
</view>
<view style="flex: 1; font-size: 22rpx; text-align: right;">
<text style="color: #71675D;">板块均涨</text>
<text :style="{
color: Number(item.alpha) > 0 ? '#EC3440' : '#01AB5D',
fontWeight: 'bold',
marginLeft: '5rpx',
marginRight: '25rpx'
}">
{{ item.formattedAvg }}%
</text>
<text :style="{
color: item.upCount > 0 ? '#EC3440' : '#888888',
fontWeight: 'bold'
}">
{{item.upCount}}
</text>
<text style="color: #888888; margin: 0 5rpx;">/</text>
<text :style="{
color: item.downCount > 0 ? '#01AB5D' : '#888888',
fontWeight: 'bold'
}">
{{item.downCount}}
</text>
</view>
</view>
<view
style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20rpx; font-size: 22rpx; color: #71675D; font-weight: 500; margin-top: 15rpx;">
<view style="text-align: left;">
<text>评分</text>
<text
style="color: #EC3440; font-weight: bold; margin-left: 10rpx;">{{ Math.round(item.final_score) }}</text>
</view>
<view style="text-align: center;">
<text>超额收益</text>
<text style="color: #EC3440; font-weight: bold; margin-left: 10rpx;">+ 0%</text>
</view>
<view style="text-align: right;"
v-if="item && Number(item.limit_up_ratio) > 0 && !isNaN(Number(item.limit_up_ratio))">
<text>涨停比</text>
<text style="color: #EC3440; font-weight: bold; margin-left: 10rpx;">
{{ formatLimitUpRatio(item.limit_up_ratio, 0) }}
</text>
</view>
</view>
</view>
<view style="height: 25rpx;"></view>
</view>
</scroll-view>
<uni-popup ref="typePopup" type="bottom" :safeArea="false">
<view class="detailPopup">
<view
style="height: 120rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 500;">
<view style="color: #727A8E; padding: 0 25rpx;" @click="closeAction(1)">取消</view>
<view style="flex: 1; text-align: center; color: #333333; font-size: 36rpx; font-weight: bold;">选择分类
</view>
<view style="color: #D79412; padding: 0 25rpx;" @click="confirmAction(1)">确定</view>
</view>
<view v-for="(item,index) in typeList" :key="index">
<view style="height: 1rpx; background-color: #EAEAEA; margin: 0 20rpx;"></view>
<view style="display: flex; align-items: center; justify-content: center; height: 80rpx;">
<image style="width: 26rpx; height: 26rpx; margin-right: 18rpx;" :src="item.backIcon"
mode="aspectFit"></image>
<view
style="min-width: 100rpx; text-align: center; font-size: 24rpx; font-weight: 500; color: #070707;">
{{item.title}}
</view>
</view>
</view>
</view>
</uni-popup>
<uni-popup ref="datePopup" type="bottom" :safeArea="false">
<view class="detailPopup">
<view
style="height: 120rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 500;">
<view style="color: #727A8E; padding: 0 25rpx;" @click="closeAction(2)">取消</view>
<view style="flex: 1; text-align: center; color: #333333; font-size: 36rpx; font-weight: bold;">选择日期
</view>
<view style="color: #D79412; padding: 0 25rpx;" @click="confirmAction(2)">确定</view>
</view>
<view style="margin: 0 38rpx; padding-bottom: 38rpx;">
<LCCalendar2 @date-change="handleDateChange"></LCCalendar2>
</view>
</view>
</uni-popup>
<uni-popup ref="detailPopup" type="bottom" :safeArea="false">
<view class="detailPopup" style="height: 550rpx;">
<view
style="height: 120rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 500;">
<view style="color: #727A8E; width: 60rpx;"></view>
<view style="flex: 1; text-align: center; color: #333333; font-size: 36rpx; font-weight: bold;">详情
</view>
<view
style="color: #D79412; width: 60rpx; display: flex; align-items: center; justify-content: center;"
@click="closeAction(3)">
<image style="width: 20rpx; height: 20rpx;" src="/static/icon/home/close.png" mode="widthFix">
</image>
</view>
</view>
<view style="height: 1rpx; margin: 0 20rpx; background-color: #E7E7E7;"></view>
<view
style="padding: 0 25rpx; box-sizing: border-box; height: 45rpx; margin: 0 45rpx; margin-top: 15rpx; background-color: #FAFAFC;">
<view style="display: flex; align-items: center; height: 100%;">
<view style="color: #666666; font-weight: 500; font-size: 24rpx; margin-right: 10rpx;">相关股票
</view>
<view style="flex: 1; font-size: 22rpx; text-align: right;">
<text style="color: #71675D;">板块均涨</text>
<text :style="{
color: Number(formattedAvg) > 0 ? '#EC3440' : '#01AB5D',
fontWeight: 'bold',
marginLeft: '10rpx',
marginRight: '20rpx'
}">{{ formattedAvg }}%</text>
<text :style="{
color: upCount > 0 ? '#EC3440' : '#888888',
fontWeight: 'bold'
}">
{{upCount}}
</text>
<text style="color: #888888; margin: 0 5rpx;">/</text>
<text :style="{
color: downCount > 0 ? '#01AB5D' : '#888888',
fontWeight: 'bold'
}">
{{downCount}}
</text>
<text style="color: #71675D; margin-left: 20rpx;">涨停比</text>
<text
style="color: #EC3440; font-weight: bold; margin-left: 10rpx;">{{ formatLimitUpRatio(limit_up_ratio, 0) }}</text>
</view>
</view>
</view>
<scroll-view scroll-y="true" show-scrollbar="false" style="height: 360rpx; ">
<view v-for="(item, index) in conceptStocksList" :key="index"
style="padding: 0 25rpx; box-sizing: border-box; height: 45rpx; margin: 0 45rpx; display: flex; align-items: center; font-weight: 500;"
:style="{ 'background-color': (index % 2 == 0 ? '#fff' : '#FAFAFC')}">
<!-- 股票名称 -->
<view style="color: #222222; font-size: 24rpx; font-weight: bold;">{{ item.name }}</view>
<!-- 股票代码 -->
<view style="flex: 1; color: #888888; font-size: 20rpx; margin: 0 20rpx;">{{ item.code }}</view>
<!-- 涨跌幅动态绑定颜色和格式化显示 -->
<view
:style="{ color: getChangeColor(item.change_pct), fontSize: '22rpx', fontWeight: 'bold' }">
{{ formatChangePct(item.change_pct) }}
</view>
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script>
import {
inject
} from 'vue'
import {
conceptsDailyTop,
marketHeatmap,
marketStatistics,
marketHotspotOverview,
conceptStocks,
searchStockInfo,
stockBasicInfo
} from '@/request/api'
const echarts = require('../../uni_modules/lime-echart/static/echarts.min.js');
export default {
data() {
return {
navH: inject('navHeight'),
contentTop: '',
currentDate: '', // 最终要赋值的日期
selectedDate: '', // 临时存储选中的日期
allStockData: [],
filteredData: [],
conceptStocksList: [],
alertTypeConfig: {
'surge': {
text: '异动',
color: '#FF7A45', // rgb(255, 122, 69)
filter: 'brightness(0) saturate(100%) invert(54%) sepia(60%) saturate(467%) hue-rotate(344deg) brightness(102%) contrast(101%)'
},
'shrink_surge_up': {
text: '缩量急涨',
color: '#722ED1', // rgb(114, 46, 209)
filter: 'brightness(0) saturate(100%) invert(24%) sepia(90%) saturate(2865%) hue-rotate(266deg) brightness(87%) contrast(98%)'
},
'volume_surge_up': {
text: '放量急涨',
color: '#EB2F96', // rgb(235, 47, 150)
filter: 'brightness(0) saturate(100%) invert(34%) sepia(82%) saturate(1970%) hue-rotate(313deg) brightness(91%) contrast(94%)'
},
'volume_oscillation': {
text: '放量震荡',
color: '#13C2C2', // rgb(19, 194, 194)
filter: 'brightness(0) saturate(100%) invert(71%) sepia(62%) saturate(487%) hue-rotate(142deg) brightness(91%) contrast(93%)'
},
'surge_up': {
text: '急涨',
color: '#FF4D4F', // rgb(255, 77, 79)
filter: 'brightness(0) saturate(100%) invert(42%) sepia(93%) saturate(727%) hue-rotate(346deg) brightness(102%) contrast(104%)'
},
'surge_down': {
text: '急跌',
color: '#52C41A', // rgb(82, 196, 26)
filter: 'brightness(0) saturate(100%) invert(68%) sepia(65%) saturate(456%) hue-rotate(71deg) brightness(91%) contrast(86%)'
},
'shrink_surge_down': {
text: '缩量急跌',
color: '#FF7A45', // rgb(255, 122, 69)
filter: 'brightness(0) saturate(100%) invert(54%) sepia(60%) saturate(467%) hue-rotate(344deg) brightness(102%) contrast(101%)'
}
},
topLists: [{
title: '大盘涨跌幅',
value: '+0.00%',
color: '#EC3440',
backIcon: '/static/icon/gegu/gg-top-0.png'
},
{
title: '涨停/跌停',
value: '+0.00%',
color: '#070707',
backIcon: '/static/icon/gegu/gg-top-1.png'
},
{
title: '多空对比',
value: '0/0',
color: '#070707',
backIcon: '/static/icon/gegu/gg-top-2.png'
},
{
title: '今日成交额',
value: '0万亿',
color: '#070707',
backIcon: '/static/icon/gegu/gg-top-3.png'
},
{
title: 'A股总市值',
value: '0万亿',
color: '#070707',
backIcon: '/static/icon/gegu/gg-top-4.png'
},
{
title: '连板龙头',
value: '0只',
color: '#F59B38',
backIcon: '/static/icon/gegu/gg-top-5.png'
}
],
topLists2: [{
title: '超大盘股',
value: '>1000亿',
},
{
title: '大盘股',
value: '500-1000亿',
},
{
title: '中盘股',
value: '100-500亿',
}
],
list2Index: 0,
typeList: [{
title: '缩量急涨',
backIcon: '/static/icon/gegu/cate-0.png'
},
{
title: '异动',
backIcon: '/static/icon/gegu/cate-1.png'
},
{
title: '急跌',
backIcon: '/static/icon/gegu/cate-2.png'
},
{
title: '急涨',
backIcon: '/static/icon/gegu/cate-3.png'
},
{
title: '放量震荡',
backIcon: '/static/icon/gegu/cate-4.png'
}
],
marketAlertsList: [],
formattedAvg: 0,
upCount: 0,
downCount: 0,
limit_up_ratio: 0,
searchResultTop:'', //搜索结果
contentTop: '',
keywords: '', //搜索关键字
searchShow:false, //是否展示搜索结果
searchResultList:[], //搜索结果
selectSearchStockInfo:null, //选中的搜索股票信息
isShowTime:false,
ec: { lazyLoad: true }, // 延迟加载 ECharts
chart: null,
y2MaxText: '', // 右侧顶部最大值文本2.36% / -0.89%
y2MinText: '' // 右侧底部最小值文本(例:-3.12% / 0.56%
}
},
onLoad(e) {
this.activeIndex = e.index
this.searchResultTop = this.navH + (20 + 70) / 750 * inject('windowWidth')
this.contentTop = this.navH + (20 + 70 + 25) / 750 * inject('windowWidth')
this.conceptsDailyTop()
// 获取当前日期,并减去一天
const now = new Date()
// 核心修改:将日期减去 1 天1 天 = 24 * 60 * 60 * 1000 毫秒)
// now.setTime(now.getTime() - 24 * 60 * 60 * 1000)
const year = now.getFullYear()
const month = (now.getMonth() + 1).toString().padStart(2, '0')
const day = now.getDate().toString().padStart(2, '0')
this.currentDate = `${year}-${month}-${day}`
},
onShow() {
this.isShowTime=false;
this.marketHeatmap();
this.marketStatistics()
this.marketHotspotListOverview()
},
methods: {
truncateText(text, length) {
if (!text) return ''; // 处理空值,避免报错
return text.length > length
? text.substring(0, length) + '...'
: text;
},
/**
* 点击搜索
*/
clickSearch() {
if(this.keywords) {
this.getSearchStockInfoListData()
}else
this.selectSearchStockInfo = null
},
/**
* 点击搜索结果背景
*/
clickSearchResultBg()
{
this.searchShow = false
},
/**
* 点击搜索结果列表项
*/
clickSearchResultListItem(item) {
this.selectSearchStockInfo = item
this.searchShow = false
this.getStockBasicInfoData()
//this.getQuoteDetailsData()
},
/**
* 根据输入内容获取搜索列表项
*/
getSearchStockInfoListData() {
let param = {q:this.keywords,limit:10}
searchStockInfo(param).then(res=>{
this.searchResultList = res.data
this.searchShow = this.searchResultList.length>0
}).catch(error=>{
})
},
/**
* 获取股票基本信息
*/
getStockBasicInfoData() {
let code = this.stockCode
if (this.selectSearchStockInfo) {
code = this.selectSearchStockInfo.stock_code
}
// stockBasicInfo(code).then(res=>{
// this.stockBasicInfo = res.data
// this.navTitle = res.data.SECNAME+'('+res.data.SECCODE+')'
// }).catch(error=>{
// })
uni.navigateTo({
url: '/pagesStock/stockCenterDetails/stockCenterDetails?code=' + code
})
},
/**
* 获取股票当前市场价格信息
*/
getQuoteDetailsData() {
let code = this.stockCode
if (this.selectSearchStockInfo) {
code = this.selectSearchStockInfo.stock_code
}
quoteDetailsInfo(code).then(res=>{
this.quoteDetailsInfo = res.data
}).catch(error=>{
})
},
formatAlpha(value) {
// 1. 空值/非数字处理
if (value === null || value === undefined || isNaN(Number(value))) {
return '0.0';
}
// 2. 转数字后保留1位小数
return Number(value).toFixed(1);
},
handleTypeClick(index) {
this.list2Index = index;
// 先请求数据,再筛选
this.marketHeatmap(this.currentDate);
},
getTableItem(obj) {
// 1. 处理空值,避免 toFixed 调用时报错
const marketCap = obj.market_cap ? obj.market_cap.toFixed(2) : '0.00';
const amount = obj.amount ? obj.amount.toFixed(2) : '0.00';
// 统一处理涨跌幅空值默认0转数字避免字符串干扰判断
const changePercent = obj.change_percent ? Number(obj.change_percent) : 0;
// 2. 处理涨跌幅的符号和类型标记
let changePercentStr = '';
let changeType = ''; // 标记正负positive/negative/zero
if (changePercent > 0) {
changePercentStr = `+${changePercent}%`; // 正数拼接+号
changeType = 'positive';
} else if (changePercent < 0) {
changePercentStr = `${changePercent}%`; // 负数直接显示
changeType = 'negative';
} else {
changePercentStr = '0%'; // 0值统一显示
changeType = 'zero';
}
// 3. 返回数组:涨跌幅位置新增 changeType 用于模板判断颜色
return [
[obj.stock_name, obj.stock_code],
[changePercentStr, '', changeType], // 第三个元素存类型标记
[`${marketCap}亿元`],
[`${amount}亿元`],
[obj.industry || '暂无'] // 处理行业为空的情况
];
},
// 处理涨停比:转百分比 + 四舍五入(可指定保留小数位数)
formatLimitUpRatio(value, decimalPlaces = 0) {
// 1. 先判断值是否有效无效直接返回空或0%
if (!value || isNaN(Number(value))) {
return '0%';
}
// 2. 正常计算逻辑
const percentValue = Number(value) * 100;
const result = decimalPlaces === 0 ? Math.round(percentValue) : percentValue.toFixed(decimalPlaces);
return `${result}%`;
},
conceptsDailyTop() {
conceptsDailyTop().then(res => {
}).catch(error => {
})
},
marketHeatmap(currentDate) {
let param = {
limit: 500
}
if(this.isShowTime){
if (currentDate && currentDate !== 'undefined' && currentDate.trim() !== '') {
param.date = currentDate;
}
}
marketHeatmap(param).then(res => {
this.topLists[2].value = res.statistics.rising_count + "/" + res.statistics.falling_count;
// 存储原始数据
this.allStockData = res.data || [];
// 2. 计算涨停数和跌停数(核心新增逻辑)
// 涨停数:涨幅 >= 9.9% 的股票数量
const limitUpCount = this.allStockData.filter((s) => {
// 做空值/非数字保护避免change_percent异常导致判断错误
const changePercent = Number(s.change_percent);
return !isNaN(changePercent) && changePercent >= 9.9;
}).length;
// 跌停数:跌幅 <= -9.9% 的股票数量
const limitDownCount = this.allStockData.filter((s) => {
const changePercent = Number(s.change_percent);
return !isNaN(changePercent) && changePercent <= -9.9;
}).length;
this.topLists[1].value = limitUpCount + "/" + limitDownCount;
this.topLists[5].value = limitUpCount + "只";
// 调用筛选方法
this.filterStockByMarketCap();
}).catch(error => {
})
},
// 根据市值区间筛选数据
filterStockByMarketCap() {
const {
list2Index,
allStockData
} = this;
let filtered = [];
switch (list2Index) {
case 0: // 超大盘股(>1000亿
filtered = allStockData.filter(item => item.market_cap > 1000);
break;
case 1: // 大盘股500-1000亿
filtered = allStockData.filter(item => item.market_cap >= 500 && item.market_cap <= 1000);
break;
case 2: // 中盘股100-500亿
filtered = allStockData.filter(item => item.market_cap >= 100 && item.market_cap <= 500);
break;
default:
filtered = allStockData;
}
this.filteredData = filtered.slice(0, 10);
},
marketStatistics() {
marketStatistics().then(res => {
this.topLists[3].value = this.formatToTrillion(res.summary.total_amount);
// 格式化 total_market_cap 为 114.7 万亿
this.topLists[4].value = this.formatToTrillion(res.summary.total_market_cap);
}).catch(error => {
this.topLists[3].value = '0.0 万亿';
this.topLists[4].value = '0.0 万亿';
})
},
formatToTrillion(num) {
if (typeof num !== 'number' || isNaN(num)) {
return '0.0 万亿'; // 处理非数字的异常情况
}
// 转换为万亿单位(除以 10000并保留 1 位小数
const trillionValue = (num / 10000).toFixed(1);
return `${trillionValue} 万亿`;
},
marketHotspotListOverview() {
let param = {
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;
// 校验数值有效性,转成数字类型
if (changePct && !isNaN(Number(changePct))) {
numPct = Number(changePct);
}
const roundedPct = Math.round(numPct * 100) / 100;
const fixedPct = roundedPct.toFixed(2);
// 3. 处理正负符号和百分号
let formattedPct = '';
if (roundedPct > 0) {
formattedPct = `+${fixedPct}%`; // 正数拼接+号
} else if (roundedPct < 0) {
formattedPct = `${fixedPct}%`; // 负数直接显示
} else {
formattedPct = '0.00%'; // 0值统一显示
}
// 4. 根据正负值设置颜色
const color = roundedPct > 0 ? '#EC3440' : (roundedPct < 0 ? '#01AB5D' : '#666666');
// 5. 赋值给topLists
this.topLists[0].value = formattedPct;
this.topLists[0].color = color;
// ========== 新增:处理每个异动条目的均涨/涨数/跌数 ==========
const processedAlerts = alerts.map(alertItem => {
// 1. 获取当前异动条目下的股票列表(假设字段是 stocks需根据实际接口调整
const stocks = alertItem.stocks || [];
// 2. 过滤有效股票change_pct 非空且是数字)
const validStocks = stocks.filter(s => s.change_pct != null && !isNaN(Number(s
.change_pct)));
// 3. 计算板块均涨
const avgChange = validStocks.length > 0 ?
validStocks.reduce((sum, s) => sum + Number(s.change_pct), 0) / validStocks
.length :
0;
// 4. 计算上涨/下跌股票数量
const upCount = validStocks.filter(s => Number(s.change_pct) > 0).length;
const downCount = validStocks.filter(s => Number(s.change_pct) < 0).length;
// 5. 格式化均涨值保留2位小数处理正负号
const roundedAvg = Math.round(avgChange * 100) / 100; // 四舍五入保留2位
const formattedAvg = roundedAvg > 0 ? `+${roundedAvg.toFixed(2)}` : roundedAvg
.toFixed(2);
// 6. 返回原数据 + 新增计算字段
return {
...alertItem,
alpha: avgChange, // 供模板中判断颜色和显示数值
upCount: upCount, // 上涨股票数
downCount: downCount, // 下跌股票数
formattedAvg: formattedAvg // 格式化后的均涨值(带正负号)
};
});
// 2. 定义时间排序函数:将 time 字符串(如 "09:42")转换为分钟数进行比较
const sortByTimeDesc = (a, b) => {
// 把 "HH:MM" 格式的时间转成分钟数(比如 09:42 → 9*60+42=582 分钟)
const timeToMinutes = (timeStr) => {
const [hours, minutes] = timeStr.split(':').map(Number);
return hours * 60 + minutes;
};
// 计算两个条目的分钟数倒序排列b - a 实现时间大的在前)
const minutesA = timeToMinutes(a.time);
const minutesB = timeToMinutes(b.time);
return minutesB - minutesA;
};
// 3. 对 alerts 数组进行排序
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
})
},
moreAction() {
if(this.isShowTime){
uni.navigateTo({
url: '/pages/geGuCenter/detail?currentDate=' + this.currentDate
})}else{
uni.navigateTo({
url: '/pages/geGuCenter/detail'
})
}
},
allAction(index) {
if (index == 1) {
this.$refs["typePopup"].open()
} else if (index == 2) {
this.$refs["datePopup"].open()
}
},
closeAction(index) {
if (index == 1) {
this.$refs["typePopup"].close()
} else if (index == 2) {
this.$refs["datePopup"].close()
} else if (index == 3) {
this.$refs["detailPopup"].close()
}
},
handleDateChange(date) {
this.selectedDate = date
console.log('选中的日期:', date)
},
confirmAction(index) {
if (index == 1) {
this.$refs["typePopup"].close()
} else if (index == 2) {
if (this.selectedDate) {
this.currentDate = this.selectedDate
console.log('最终确认的日期:', this.currentDate)
} else {
// 如果没有选择日期,使用当前日期
const now = new Date()
const year = now.getFullYear()
const month = (now.getMonth() + 1).toString().padStart(2, '0')
const day = now.getDate().toString().padStart(2, '0')
this.currentDate = `${year}-${month}-${day}`
}
this.isShowTime=true;
this.marketHeatmap(this.currentDate)
this.marketStatistics()
this.marketHotspotListOverview()
this.$refs["datePopup"].close()
}
},
bkydAction(item) {
this.$refs["detailPopup"].open()
this.formattedAvg = item.formattedAvg,
this.upCount = item.upCount,
this.downCount = item.downCount,
this.limit_up_ratio = item.limit_up_ratio,
this.conceptStocksDetails(item.concept_id)
},
conceptStocksDetails(concept_id) {
console.log("concept_id", concept_id)
conceptStocks(concept_id, {}).then(res => {
if (res.data && res.data.stocks) {
// 将接口数据赋值给列表数组
let rawData = res.data.stocks;
// 2. 对数据进行排序处理
this.conceptStocksList = rawData.sort((a, b) => {
// 将 None 值转换为 -999
const aValue = a.change_pct === null || a.change_pct === undefined ? -999 :
Number(a.change_pct);
const bValue = b.change_pct === null || b.change_pct === undefined ? -999 :
Number(b.change_pct);
// 降序排列(涨幅高的在前)
return bValue - aValue;
});
} else {
console.warn('接口返回数据格式异常', res);
}
}).catch(error => {
})
},
// 格式化涨跌幅显示(处理正负号、保留两位小数)
formatChangePct(change_pct) {
if (typeof change_pct !== 'number') return '0.00%';
// 正数加+号,负数保留-号,保留两位小数
const symbol = change_pct >= 0 ? '+' : '';
return `${symbol}${change_pct.toFixed(2)}%`;
},
// 获取涨跌幅文字颜色(涨红跌绿,平盘灰色)
getChangeColor(change_pct) {
if (typeof change_pct !== 'number') return '#888888';
if (change_pct > 0) return '#EC3440'; // 上涨:红色
if (change_pct < 0) return '#00B42A'; // 下跌:绿色
return '#888888'; // 平盘:灰色
}
}
}
</script>
<style lang="less">
page {
background-color: #070707;
}
.topBg {
top: 0;
left: 0;
width: 100%;
height: auto;
}
.searchC {
background-color: #292929B3;
left: 0;
right: 0;
margin: 20rpx 25rpx 0;
padding: 0 25rpx;
height: 70rpx;
border-radius: 35rpx;
font-size: 22rpx;
font-weight: 500;
.icon {
margin-right: 12rpx;
width: 25rpx;
height: auto;
}
input {
height: 100%;
color: white;
}
}
.stockDetailsC {
left: 25rpx;
width: calc(100vw - 50rpx);
bottom: calc(55px + env(safe-area-inset-bottom));
background-color: white;
border-radius: 10rpx;
}
.detailPopup {
max-height: 70%;
background-color: white;
color: red;
border-radius: 20rpx 20rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
}
.searchResultList {
background-color: #00000080;
left: 0;
right: 0;
bottom: 0;
padding: 0 25rpx;
.list
{
background-color: white;
border-radius: 10rpx;
.item
{
padding: 0 42rpx;
line-height: 60rpx;
font-size: 22rpx;
font-weight: 500;
color: #333;
}
}
z-index: 20;
}
</style>