/**
* 股票列表单元格渲染原语(共享)。
*
* 只负责「无业务上下文」的纯数据列:价格/成交/均线/区间/技术指标/动量/连板/财务等。
* 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'
}