From 30f63462528599b6003690efd63548d7c3bc028e Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Fri, 9 Jan 2026 07:36:31 +0800 Subject: [PATCH] =?UTF-8?q?heropanel=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 90 +++++++++ .../Community/components/EventDailyStats.js | 190 +++++++++++++----- 2 files changed, 230 insertions(+), 50 deletions(-) diff --git a/app.py b/app.py index 3bd0761e..7a2acad4 100755 --- a/app.py +++ b/app.py @@ -11274,6 +11274,95 @@ def get_events_effectiveness_stats(): 'positiveRate': round(positive_count / len(avg_chg_list) * 100, 1) if avg_chg_list else 0, } + # 查询全市场实时涨跌统计(从 ClickHouse) + market_stats = { + 'risingCount': 0, + 'fallingCount': 0, + 'flatCount': 0, + 'totalCount': 0, + 'risingRate': 0, + } + try: + client = get_clickhouse_client() + + # 获取当前交易日 + if hasattr(current_trading_day, 'strftime'): + target_date = current_trading_day + else: + target_date = datetime.strptime(str(current_trading_day), '%Y-%m-%d').date() + + market_start = datetime.combine(target_date, dt_time(9, 30)) + market_end = datetime.combine(target_date, dt_time(15, 0)) + + # 获取前收盘日期 + prev_trading_day = None + try: + target_idx = trading_days.index(target_date) + if target_idx > 0: + prev_trading_day = trading_days[target_idx - 1] + except (ValueError, IndexError): + pass + + if prev_trading_day: + prev_date_str = prev_trading_day.strftime('%Y%m%d') + + # 查询所有 A 股的最新价格(排除指数和北交所) + market_price_query = """ + SELECT + code, + argMax(close, timestamp) as last_price + FROM stock_minute + WHERE timestamp >= %(start)s + AND timestamp <= %(end)s + AND (code LIKE '%%.SH' OR code LIKE '%%.SZ') + AND code NOT LIKE '399%%' + AND code NOT LIKE '000%%' + GROUP BY code + """ + market_data = client.execute(market_price_query, { + 'start': market_start, + 'end': market_end + }) + + if market_data: + # 提取股票代码并获取前收盘价 + all_base_codes = [row[0].split('.')[0] for row in market_data] + all_prev_close = get_cached_prev_close(all_base_codes, prev_date_str) + + rising = 0 + falling = 0 + flat = 0 + + for row in market_data: + code = row[0] + last_price = float(row[1]) if row[1] else None + base_code = code.split('.')[0] + prev_close = all_prev_close.get(base_code) + + if last_price and prev_close and prev_close > 0: + change_pct = (last_price - prev_close) / prev_close * 100 + if change_pct > 0.01: # 上涨 + rising += 1 + elif change_pct < -0.01: # 下跌 + falling += 1 + else: # 平盘 + flat += 1 + + total = rising + falling + flat + market_stats = { + 'risingCount': rising, + 'fallingCount': falling, + 'flatCount': flat, + 'totalCount': total, + 'risingRate': round(rising / total * 100, 1) if total > 0 else 0, + } + print(f'[effectiveness-stats] 市场统计: 上涨={rising}, 下跌={falling}, 平盘={flat}, 上涨率={market_stats["risingRate"]}%') + + except Exception as e: + import traceback + print(f'[effectiveness-stats] 查询市场统计失败: {e}') + traceback.print_exc() + # 按日期分组统计 daily_data = {} for event in events_query: @@ -11343,6 +11432,7 @@ def get_events_effectiveness_stats(): 'data': { 'currentDate': current_trading_day.strftime('%Y-%m-%d') if hasattr(current_trading_day, 'strftime') else str(current_trading_day), 'summary': summary, + 'marketStats': market_stats, 'dailyStats': daily_stats, 'topPerformers': top_performers_list, 'topStocks': stock_stats diff --git a/src/views/Community/components/EventDailyStats.js b/src/views/Community/components/EventDailyStats.js index 5b0a0d9b..2207e5fe 100644 --- a/src/views/Community/components/EventDailyStats.js +++ b/src/views/Community/components/EventDailyStats.js @@ -61,89 +61,175 @@ const getChgColor = (val) => { }; /** - * 胜率仪表盘组件 + * 获取胜率颜色(>50%红色,<50%绿色) */ -const WinRateGauge = ({ rate }) => { +const getRateColor = (rate) => { + if (rate >= 50) return '#FF4D4F'; // 红色(上涨多) + return '#52C41A'; // 绿色(下跌多) +}; + +/** + * 半圆仪表盘组件 - 参考用户提供的设计 + * 使用渐变色:左侧绿色 -> 中间黄色 -> 右侧红色 + */ +const SemiCircleGauge = ({ rate, label, size = 'normal' }) => { const validRate = Math.min(100, Math.max(0, rate || 0)); - const angle = (validRate / 100) * 180; + // 角度:0% = -90deg (左), 50% = 0deg (上), 100% = 90deg (右) + const needleAngle = (validRate / 100) * 180 - 90; - const getGaugeColor = (r) => { - if (r >= 70) return '#52C41A'; - if (r >= 50) return '#FFD700'; - return '#FF4D4F'; - }; - - const gaugeColor = getGaugeColor(validRate); + const gaugeColor = getRateColor(validRate); + const isSmall = size === 'small'; + const gaugeWidth = isSmall ? 80 : 100; + const gaugeHeight = gaugeWidth / 2; + const needleLength = isSmall ? 30 : 38; return ( - + + {/* 半圆背景(渐变色弧线) */} - - + _before={{ + content: '""', + position: 'absolute', + top: '0', + left: '0', + right: '0', + bottom: '0', + borderTopLeftRadius: `${gaugeHeight}px`, + borderTopRightRadius: `${gaugeHeight}px`, + border: `${isSmall ? '6px' : '8px'} solid transparent`, + borderBottom: 'none', + background: 'linear-gradient(90deg, #52C41A 0%, #FADB14 50%, #FF4D4F 100%) border-box', + mask: 'linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0)', + maskComposite: 'exclude', + WebkitMaskComposite: 'destination-out', + }} + /> + {/* 内部遮罩(让弧线更细) */} + + {/* 指针 */} + + {/* 指针中心点 */} + {/* 刻度标记 */} + + 0 + + + 100 + + + {/* 数值和标签 */} - - {validRate.toFixed(0)}% + + {validRate.toFixed(1)}% - 胜率 + {label} + + ); +}; - 0 - 100 +/** + * 胜率对比组件 - 双仪表盘 + */ +const WinRateGauge = ({ eventRate, marketRate, marketStats }) => { + const eventRateVal = eventRate || 0; + const marketRateVal = marketRate || 0; + + return ( + + {/* 双仪表盘对比 */} + + + + + + + + + + + {/* 市场统计 */} + {marketStats && ( + + + + {marketStats.risingCount}涨 + + + + {marketStats.flatCount}平 + + + + {marketStats.fallingCount}跌 + + + )} ); }; @@ -350,7 +436,7 @@ const EventDailyStats = () => { ); } - const { summary, topPerformers = [], topStocks = [] } = stats; + const { summary, marketStats, topPerformers = [], topStocks = [] } = stats; return ( { {/* 内容区域 - 使用 flex: 1 填充剩余空间 */} - {/* 胜率仪表盘 */} - + {/* 胜率对比仪表盘 */} + {/* 核心指标 - 2x2 网格 */}