From 1adbeda168d427e4774f0e74810a849927c61fa6 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Wed, 10 Dec 2025 12:22:40 +0800 Subject: [PATCH] update pay ui --- app.py | 174 ++++++++++++------ .../components/FlexScreen/index.tsx | 30 ++- .../components/ConceptAlertList.js | 106 ++++++----- .../HotspotOverview/utils/chartHelpers.js | 7 +- 4 files changed, 197 insertions(+), 120 deletions(-) diff --git a/app.py b/app.py index 4266207c..6adbdfbb 100755 --- a/app.py +++ b/app.py @@ -12053,10 +12053,11 @@ def get_market_summary(seccode): @app.route('/api/stocks/search', methods=['GET']) def search_stocks(): - """搜索股票(支持股票代码、股票简称、拼音首字母)""" + """搜索股票和指数(支持代码、名称搜索)""" try: query = request.args.get('q', '').strip() limit = request.args.get('limit', 20, type=int) + search_type = request.args.get('type', 'all') # all, stock, index if not query: return jsonify({ @@ -12064,73 +12065,132 @@ def search_stocks(): 'error': '请输入搜索关键词' }), 400 + results = [] + with engine.connect() as conn: - test_sql = text(""" - SELECT SECCODE, SECNAME, F001V, F003V, F010V, F011V - FROM ea_stocklist - WHERE SECCODE = '300750' - OR F001V LIKE '%ndsd%' LIMIT 5 - """) - test_result = conn.execute(test_sql).fetchall() + # 搜索指数(优先显示指数,因为通常用户搜索代码时指数更常用) + if search_type in ('all', 'index'): + index_sql = text(""" + SELECT DISTINCT + INDEXCODE as stock_code, + SECNAME as stock_name, + INDEXNAME as full_name, + F018V as exchange + FROM ea_exchangeindex + WHERE ( + UPPER(INDEXCODE) LIKE UPPER(:query_pattern) + OR UPPER(SECNAME) LIKE UPPER(:query_pattern) + OR UPPER(INDEXNAME) LIKE UPPER(:query_pattern) + ) + ORDER BY CASE + WHEN UPPER(INDEXCODE) = UPPER(:exact_query) THEN 1 + WHEN UPPER(SECNAME) = UPPER(:exact_query) THEN 2 + WHEN UPPER(INDEXCODE) LIKE UPPER(:prefix_pattern) THEN 3 + WHEN UPPER(SECNAME) LIKE UPPER(:prefix_pattern) THEN 4 + ELSE 5 + END, + INDEXCODE + LIMIT :limit + """) - # 构建搜索SQL - 支持股票代码、股票简称、拼音简称搜索 - search_sql = text(""" - SELECT DISTINCT SECCODE as stock_code, - SECNAME as stock_name, - F001V as pinyin_abbr, - F003V as security_type, - F005V as exchange, - F011V as listing_status - FROM ea_stocklist - WHERE ( - UPPER(SECCODE) LIKE UPPER(:query_pattern) - OR UPPER(SECNAME) LIKE UPPER(:query_pattern) - OR UPPER(F001V) LIKE UPPER(:query_pattern) - ) - -- 基本过滤条件:只搜索正常的A股和B股 - AND (F011V = '正常上市' OR F010V = '013001') -- 正常上市状态 - AND F003V IN ('A股', 'B股') -- 只搜索A股和B股 - ORDER BY CASE - WHEN UPPER(SECCODE) = UPPER(:exact_query) THEN 1 - WHEN UPPER(SECNAME) = UPPER(:exact_query) THEN 2 - WHEN UPPER(F001V) = UPPER(:exact_query) THEN 3 - WHEN UPPER(SECCODE) LIKE UPPER(:prefix_pattern) THEN 4 - WHEN UPPER(SECNAME) LIKE UPPER(:prefix_pattern) THEN 5 - WHEN UPPER(F001V) LIKE UPPER(:prefix_pattern) THEN 6 - ELSE 7 - END, - SECCODE LIMIT :limit - """) + index_result = conn.execute(index_sql, { + 'query_pattern': f'%{query}%', + 'exact_query': query, + 'prefix_pattern': f'{query}%', + 'limit': limit + }).fetchall() - result = conn.execute(search_sql, { - 'query_pattern': f'%{query}%', - 'exact_query': query, - 'prefix_pattern': f'{query}%', - 'limit': limit - }).fetchall() + for row in index_result: + results.append({ + 'stock_code': row.stock_code, + 'stock_name': row.stock_name, + 'full_name': row.full_name, + 'exchange': row.exchange, + 'isIndex': True, + 'security_type': '指数' + }) - stocks = [] - for row in result: - # 获取当前价格 - current_price, _ = get_latest_price_from_clickhouse(row.stock_code) + # 搜索股票 + if search_type in ('all', 'stock'): + stock_sql = text(""" + SELECT DISTINCT SECCODE as stock_code, + SECNAME as stock_name, + F001V as pinyin_abbr, + F003V as security_type, + F005V as exchange, + F011V as listing_status + FROM ea_stocklist + WHERE ( + UPPER(SECCODE) LIKE UPPER(:query_pattern) + OR UPPER(SECNAME) LIKE UPPER(:query_pattern) + OR UPPER(F001V) LIKE UPPER(:query_pattern) + ) + AND (F011V = '正常上市' OR F010V = '013001') + AND F003V IN ('A股', 'B股') + ORDER BY CASE + WHEN UPPER(SECCODE) = UPPER(:exact_query) THEN 1 + WHEN UPPER(SECNAME) = UPPER(:exact_query) THEN 2 + WHEN UPPER(F001V) = UPPER(:exact_query) THEN 3 + WHEN UPPER(SECCODE) LIKE UPPER(:prefix_pattern) THEN 4 + WHEN UPPER(SECNAME) LIKE UPPER(:prefix_pattern) THEN 5 + WHEN UPPER(F001V) LIKE UPPER(:prefix_pattern) THEN 6 + ELSE 7 + END, + SECCODE + LIMIT :limit + """) - stocks.append({ - 'stock_code': row.stock_code, - 'stock_name': row.stock_name, - 'current_price': current_price or 0, # 添加当前价格 - 'pinyin_abbr': row.pinyin_abbr, - 'security_type': row.security_type, - 'exchange': row.exchange, - 'listing_status': row.listing_status - }) + stock_result = conn.execute(stock_sql, { + 'query_pattern': f'%{query}%', + 'exact_query': query, + 'prefix_pattern': f'{query}%', + 'limit': limit + }).fetchall() + + for row in stock_result: + results.append({ + 'stock_code': row.stock_code, + 'stock_name': row.stock_name, + 'pinyin_abbr': row.pinyin_abbr, + 'security_type': row.security_type, + 'exchange': row.exchange, + 'listing_status': row.listing_status, + 'isIndex': False + }) + + # 如果搜索全部,按相关性重新排序(精确匹配优先) + if search_type == 'all': + def sort_key(item): + code = item['stock_code'].upper() + name = item['stock_name'].upper() + q = query.upper() + # 精确匹配代码优先 + if code == q: + return (0, not item['isIndex'], code) # 指数优先 + # 精确匹配名称 + if name == q: + return (1, not item['isIndex'], code) + # 前缀匹配代码 + if code.startswith(q): + return (2, not item['isIndex'], code) + # 前缀匹配名称 + if name.startswith(q): + return (3, not item['isIndex'], code) + return (4, not item['isIndex'], code) + + results.sort(key=sort_key) + + # 限制总数 + results = results[:limit] return jsonify({ 'success': True, - 'data': stocks, - 'count': len(stocks) + 'data': results, + 'count': len(results) }) except Exception as e: + app.logger.error(f"搜索股票/指数错误: {e}") return jsonify({ 'success': False, 'error': str(e) diff --git a/src/views/StockOverview/components/FlexScreen/index.tsx b/src/views/StockOverview/components/FlexScreen/index.tsx index 57b4d189..dfda551e 100644 --- a/src/views/StockOverview/components/FlexScreen/index.tsx +++ b/src/views/StockOverview/components/FlexScreen/index.tsx @@ -209,11 +209,14 @@ const FlexScreen: React.FC = () => { (security: SearchResultItem | WatchlistItem): void => { const code = 'stock_code' in security ? security.stock_code : security.code; const name = 'stock_name' in security ? security.stock_name : security.name; - const isIndex = - security.isIndex || code.startsWith('000') || code.startsWith('399'); + // 优先使用 API 返回的 isIndex 字段 + const isIndex = security.isIndex === true; - // 检查是否已存在 - if (watchlist.some(item => item.code === code)) { + // 生成唯一标识(带后缀的完整代码) + const fullCode = getFullCode(code, isIndex); + + // 检查是否已存在(使用带后缀的代码比较,避免上证指数和平安银行冲突) + if (watchlist.some(item => getFullCode(item.code, item.isIndex) === fullCode)) { toast({ title: '已在自选列表中', status: 'info', @@ -227,7 +230,7 @@ const FlexScreen: React.FC = () => { setWatchlist(prev => [...prev, { code, name, isIndex }]); toast({ - title: `已添加 ${name}`, + title: `已添加 ${name}${isIndex ? '(指数)' : ''}`, status: 'success', duration: 2000, isClosable: true, @@ -397,7 +400,7 @@ const FlexScreen: React.FC = () => { {searchResults.map((stock, index) => ( { > - - {stock.stock_name} - + + + {stock.stock_name} + + + {stock.isIndex ? '指数' : '股票'} + + {stock.stock_code} diff --git a/src/views/StockOverview/components/HotspotOverview/components/ConceptAlertList.js b/src/views/StockOverview/components/HotspotOverview/components/ConceptAlertList.js index 75d0f7cf..c1613aba 100644 --- a/src/views/StockOverview/components/HotspotOverview/components/ConceptAlertList.js +++ b/src/views/StockOverview/components/HotspotOverview/components/ConceptAlertList.js @@ -112,29 +112,29 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => { {getAlertTypeLabel(alert.alert_type)} {/* 确认率 */} - {isV2 && alert.confirm_ratio !== undefined && ( + {isV2 && alert.confirm_ratio != null && ( = 0.8 ? 'green.500' : 'orange.500'} + bg={(alert.confirm_ratio || 0) >= 0.8 ? 'green.500' : 'orange.500'} /> - {Math.round(alert.confirm_ratio * 100)}% + {Math.round((alert.confirm_ratio || 0) * 100)}% )} {/* Alpha + Z-Score 简化显示 */} - {alert.alpha !== undefined && ( - = 0 ? 'red.500' : 'green.500'} fontWeight="medium"> - α {alert.alpha >= 0 ? '+' : ''}{alert.alpha.toFixed(2)}% + {alert.alpha != null && ( + = 0 ? 'red.500' : 'green.500'} fontWeight="medium"> + α {(alert.alpha || 0) >= 0 ? '+' : ''}{(alert.alpha || 0).toFixed(2)}% )} - {isV2 && alert.alpha_zscore !== undefined && ( - + {isV2 && alert.alpha_zscore != null && ( + { > = 0 ? '50%' : undefined} - right={alert.alpha_zscore < 0 ? '50%' : undefined} - w={`${Math.min(Math.abs(alert.alpha_zscore) / 5 * 50, 50)}%`} + left={(alert.alpha_zscore || 0) >= 0 ? '50%' : undefined} + right={(alert.alpha_zscore || 0) < 0 ? '50%' : undefined} + w={`${Math.min(Math.abs(alert.alpha_zscore || 0) / 5 * 50, 50)}%`} h="100%" - bg={alert.alpha_zscore >= 0 ? 'red.500' : 'green.500'} + bg={(alert.alpha_zscore || 0) >= 0 ? 'red.500' : 'green.500'} /> - = 0 ? 'red.400' : 'green.400'}> - {alert.alpha_zscore >= 0 ? '+' : ''}{alert.alpha_zscore.toFixed(1)}σ + = 0 ? 'red.400' : 'green.400'}> + {(alert.alpha_zscore || 0) >= 0 ? '+' : ''}{(alert.alpha_zscore || 0).toFixed(1)}σ )} - {alert.limit_up_ratio > 0.05 && ( + {(alert.limit_up_ratio || 0) > 0.05 && ( - {Math.round(alert.limit_up_ratio * 100)}% + {Math.round((alert.limit_up_ratio || 0) * 100)}% )} @@ -193,40 +193,44 @@ const AlertCard = ({ alert, isExpanded, onToggle, stocks, loadingStocks }) => { - {stocks.slice(0, 10).map((stock, idx) => ( - handleStockClick(e, stock.code || stock.stock_code)} - > - - - {stock.name || stock.stock_name} - - - - 0 ? 'red.400' : - stock.change_pct < 0 ? 'green.400' : 'gray.400' - } - > - {stock.change_pct !== undefined - ? `${stock.change_pct > 0 ? '+' : ''}${stock.change_pct.toFixed(2)}%` - : '-' - } - - - - - {stock.reason || '-'} - - - - ))} + {stocks.slice(0, 10).map((stock, idx) => { + const changePct = stock.change_pct; + const hasChange = changePct != null && !isNaN(changePct); + return ( + handleStockClick(e, stock.code || stock.stock_code)} + > + + + {stock.name || stock.stock_name || '-'} + + + + 0 ? 'red.400' : + hasChange && changePct < 0 ? 'green.400' : 'gray.400' + } + > + {hasChange + ? `${changePct > 0 ? '+' : ''}${changePct.toFixed(2)}%` + : '-' + } + + + + + {stock.reason || '-'} + + + + ); + })} {stocks.length > 10 && ( diff --git a/src/views/StockOverview/components/HotspotOverview/utils/chartHelpers.js b/src/views/StockOverview/components/HotspotOverview/utils/chartHelpers.js index 96ba2d5b..54ff9eba 100644 --- a/src/views/StockOverview/components/HotspotOverview/utils/chartHelpers.js +++ b/src/views/StockOverview/components/HotspotOverview/utils/chartHelpers.js @@ -152,8 +152,9 @@ export const formatScore = (score) => { * @returns {string} 颜色代码 */ export const getScoreColor = (score) => { - if (score >= 80) return '#ff4757'; - if (score >= 60) return '#ff6348'; - if (score >= 40) return '#ffa502'; + const s = score || 0; + if (s >= 80) return '#ff4757'; + if (s >= 60) return '#ff6348'; + if (s >= 40) return '#ffa502'; return '#747d8c'; };