/** * 股票列表单元格渲染原语(共享)。 * * 只负责「无业务上下文」的纯数据列:价格/成交/均线/区间/技术指标/动量/连板/财务等。 * symbol、strategies、score、signals、candle 等需要页面上下文(加自选按钮、失效行、 * kline 数据、信号提取)的列由各页面的 renderCell 自行处理。 * * 口径与原 ScreenerTable 对齐(已和自选页校准过):amplitude 用 *100、annual_vol/ * 财务率类用 fmtPct、kdj 用 toFixed(1)、vol_ma 用 fmtBigNum 等。 */ import type { ReactNode } from 'react' import { fmtPrice, fmtPct, fmtBigNum, priceColorClass } from '@/lib/format' import type { ColumnConfig } from '@/lib/list-columns' import { NUM_CELL_CLASS } from '@/lib/stock-table' // ===== 板块标识(自选/策略页统一口径) ===== export function boardTag(symbol: string): { label: string; color: string } | null { if (/^(300|301)/.test(symbol)) return { label: '创', color: 'text-[#f97316] bg-[#f97316]/12 border-[#f97316]/25' } if (/^688/.test(symbol)) return { label: '科', color: 'text-cyan-400 bg-cyan-400/12 border-cyan-400/25' } if (/\.BJ$/.test(symbol)) return { label: '北', color: 'text-purple-400 bg-purple-400/12 border-purple-400/25' } return null } // ===== RSI 指标带颜色 ===== export function RSIBadge({ value }: { value: number | null | undefined }) { if (value == null || Number.isNaN(value)) return let color = 'text-secondary' if (value >= 80) color = 'text-danger font-medium' else if (value >= 70) color = 'text-bull' else if (value <= 20) color = 'text-bear font-medium' else if (value <= 30) color = 'text-bear' return {value.toFixed(1)} } // ===== 纯数据列渲染 ===== function fmtMaybePrice(value: any) { return value != null && !Number.isNaN(value) ? fmtPrice(value) : '—' } /** * 渲染一个纯数据内置列的 。 * 返回 null 表示该列不属于纯数据列(symbol/strategies/score/signals/candle 等),由调用方处理。 */ export function renderBuiltinDataCell(r: any, col: ColumnConfig): ReactNode | null { if (col.source.type !== 'builtin') return null const key = col.source.key // 这些列需要业务上下文,不在此处理 if (key === 'symbol' || key === 'strategies' || key === 'score' || key === 'signals' || key === 'candle') { return null } const numCls = `${alignTdClass(col.align)} ${NUM_CELL_CLASS}` switch (key) { // 价格 case 'price': return {fmtMaybePrice(r.close)} case 'pct': return {fmtPct(r.change_pct)} case 'change_amount': return {r.change_amount != null ? fmtPrice(r.change_amount) : '—'} case 'amplitude': return {r.amplitude != null ? `${(r.amplitude * 100).toFixed(2)}%` : '—'} // 成交 case 'turnover': return {r.turnover_rate != null ? `${Number(r.turnover_rate).toFixed(2)}%` : '—'} case 'amount': return {fmtBigNum(r.amount)} case 'float_val': return {r.float_shares && r.close ? fmtBigNum(r.float_shares * r.close) : '—'} case 'vol_ratio': return ( = 2 ? 'text-accent font-medium' : ''}> {fmtMaybePrice(r.vol_ratio_5d)} ) case 'annual_vol': return {r.annual_vol_20d != null ? fmtPct(r.annual_vol_20d) : '—'} // 均线 case 'ma5': return {fmtMaybePrice(r.ma5)} case 'ma10': return {fmtMaybePrice(r.ma10)} case 'ma20': return {fmtMaybePrice(r.ma20)} case 'ma60': return {fmtMaybePrice(r.ma60)} // 区间 case 'high_60d': return {r.high_60d != null ? fmtPrice(r.high_60d) : '—'} case 'low_60d': return {r.low_60d != null ? fmtPrice(r.low_60d) : '—'} // 技术指标 case 'rsi6': return case 'rsi14': return case 'rsi24': return case 'macd_dif': return {r.macd_dif != null ? fmtPrice(r.macd_dif) : '—'} case 'macd_dea': return {r.macd_dea != null ? fmtPrice(r.macd_dea) : '—'} case 'macd_hist': return {r.macd_hist != null ? fmtPrice(r.macd_hist) : '—'} case 'kdj_k': return {r.kdj_k != null ? r.kdj_k.toFixed(1) : '—'} case 'kdj_d': return {r.kdj_d != null ? r.kdj_d.toFixed(1) : '—'} case 'kdj_j': return {r.kdj_j != null ? r.kdj_j.toFixed(1) : '—'} case 'boll_upper': return {r.boll_upper != null ? fmtPrice(r.boll_upper) : '—'} case 'boll_lower': return {r.boll_lower != null ? fmtPrice(r.boll_lower) : '—'} case 'atr14': return {r.atr_14 != null ? fmtPrice(r.atr_14) : '—'} case 'vol_ma5': return {r.vol_ma5 != null ? fmtBigNum(r.vol_ma5) : '—'} case 'vol_ma10': return {r.vol_ma10 != null ? fmtBigNum(r.vol_ma10) : '—'} // 动量 case 'momentum_5d': return {fmtPct(r.momentum_5d)} case 'momentum_10d': return {fmtPct(r.momentum_10d)} case 'momentum_20d': return {fmtPct(r.momentum_20d)} case 'momentum_30d': return {fmtPct(r.momentum_30d)} case 'momentum_60d': return {fmtPct(r.momentum_60d)} // 连板 case 'limit_ups': return ( {r.consecutive_limit_ups > 0 ? ( {r.consecutive_limit_ups} ) : ( )} ) case 'limit_downs': return ( {r.consecutive_limit_downs > 0 ? ( {r.consecutive_limit_downs} ) : ( )} ) // 财务指标(后端 enriched 未返回时显示 —) case 'eps': return {r.eps != null ? fmtPrice(r.eps) : '—'} case 'bps': return {r.bps != null ? fmtPrice(r.bps) : '—'} case 'roe': return {r.roe != null ? fmtPct(r.roe) : '—'} case 'pe_ttm': return {r.pe_ttm != null ? fmtPrice(r.pe_ttm) : '—'} case 'pb': return {r.pb != null ? fmtPrice(r.pb) : '—'} case 'gross_margin': return {r.gross_margin != null ? fmtPct(r.gross_margin) : '—'} case 'net_margin': return {r.net_margin != null ? fmtPct(r.net_margin) : '—'} case 'revenue_yoy': return {r.revenue_yoy != null ? fmtPct(r.revenue_yoy) : '—'} case 'net_income_yoy':return {r.net_income_yoy != null ? fmtPct(r.net_income_yoy) : '—'} case 'debt_ratio': return {r.debt_ratio != null ? fmtPct(r.debt_ratio) : '—'} default: return — } } /** 根据列对齐返回 td 基础 class(不含 num 样式) */ function alignTdClass(align: ColumnConfig['align']): string { if (align === 'right') return 'px-3 py-2 text-right' if (align === 'center') return 'px-3 py-2 text-center' return 'px-3 py-2' }