import { motion } from 'framer-motion' import { Loader2, CheckCircle2, Settings, Table2 } from 'lucide-react' import { formatNumber } from '@/lib/format' import { fmtDate } from '@/lib/format' import { Skeleton } from './Skeleton' // 卡片能力定义:capKey → 查 capability limits;tierReq → 无权限时显示的档位要求 // capKey 为空串表示该数据在 free-api 服务器(None 档/Free 档)即可获取,无需付费能力门控。 export const CARD_META: Record = { // 标的维表走 exchanges 端点,free-api 服务器即可获取,无需付费能力 instruments: { capKey: '', tierReq: '' }, daily: { capKey: 'kline.daily.batch', tierReq: 'Starter+' }, adj_factor: { capKey: 'adj_factor', tierReq: 'Starter+' }, enriched: { capKey: '', tierReq: '' }, // ETF 复用日K批量能力(免费档 kline.daily.batch 即可),不显示档位徽章 etf: { capKey: 'kline.daily.batch', tierReq: '' }, minute: { capKey: 'kline.minute.batch', tierReq: 'Pro+' }, financials: { capKey: 'financial', tierReq: 'Expert' }, } export function Pill({ label, value }: { label: string; value: number | string }) { return (
{label}
{value}
) } function CapBadge({ hasCap, isLocal, tierLabel, tierReq, capInfo, localSuffix }: { hasCap: boolean isLocal: boolean tierLabel?: string tierReq?: string capInfo?: { rpm: number | null; batch: number | null; subscribe: number | null } | undefined localSuffix?: string }) { if (isLocal) { return ( 本地计算{localSuffix ? ` · ${localSuffix}` : ''} ) } if (hasCap && capInfo && tierLabel) { const parts = [tierLabel, `${capInfo.rpm}/min`] if (capInfo.batch != null && capInfo.batch > 1) parts.push(`${capInfo.batch}股/批`) return ( {parts.join(' · ')} ) } if (!hasCap && tierReq && tierReq !== 'Free') { // 缺权限且非 Free 档(付费档位才提示升级);Free 档人人可用, // 若显示"需 Free"会造成 Expert 等用户困惑(通常是探测瞬时失败丢能力) return ( 需 {tierReq} ) } if (hasCap) { return ( {tierLabel ?? '已授权'} ) } return null } export type FieldTab = { label: string; table: string } export function StatCard({ title, hint, stats, isInstrument = false, loading = false, active = false, done = false, skipped = false, stagePct = 0, tierKey, capLimits, tierLabel, auto, onSettings, onShowFields, settingsOpen, subLabel, localBadgeSuffix, fieldTabs, }: { title: string hint: string stats: any | null | undefined isInstrument?: boolean loading?: boolean active?: boolean done?: boolean skipped?: boolean stagePct?: number tierKey?: string capLimits?: Record tierLabel?: string onSettings?: () => void onShowFields?: (table?: string) => void settingsOpen?: boolean auto?: boolean subLabel?: string localBadgeSuffix?: string // 多表字段入口: [{label: '维表', table: 'index_instruments'}, ...] // 提供时渲染多个图标按钮(每个对应一张表的字段说明); 否则回退到单个 onShowFields fieldTabs?: FieldTab[] }) { const empty = loading || !stats || (stats.rows === 0 && !stats.trading_days && !stats.fields) const borderCls = active ? 'border-accent/50' : done ? 'border-bear/30' : 'border-border' const bgCls = active ? 'bg-accent/[0.03]' : 'bg-surface' const meta = tierKey ? CARD_META[tierKey] : undefined const isLocal = meta?.capKey === '' const capInfo = meta?.capKey ? capLimits?.[meta.capKey] : undefined const hasCap = isLocal || !!capInfo // 渲染字段说明入口图标 // - fieldTabs 提供时: 返回 null (图标由 renderSubLabelInline 内联到文字后) // - 否则: 单个图标按钮 (onShowFields) const renderFieldButtons = () => { if (fieldTabs && fieldTabs.length > 0) return null if (onShowFields) { return ( ) } return null } // 单个图标按钮 (复用样式) const fieldIconButton = (tab: FieldTab) => ( ) // subLabel 文本内容 (不含图标) const subLabelText = subLabel ?? (isInstrument ? `标的 · ${((stats?.named ?? stats?.rows) ?? 0).toLocaleString()} 个含名称` : stats?.fields ? '字段 · 复权 · 技术指标' : title === '日 K' && stats?.trading_days ? '日 · A股标的 · 日线' : stats?.trading_days && !stats?.rows ? '日 · A股标的 · 分钟级' : (() => { const parts = [`行 · ${(stats?.symbols_covered ?? 0)} 只标的`] if (stats?.trading_days) parts.push(`· ${stats.trading_days} 日`) return parts.join(' ') })()) // 有 fieldTabs 时: 把 subLabel 按分隔符拆开, 每个匹配词后面内联图标 // 例如 "日 · 维表 · 日K · 指标" → 日 · 维表[icon] · 日K[icon] · 指标[icon] const renderSubLabelInline = () => { if (!fieldTabs || fieldTabs.length === 0) { return <>{subLabelText}{renderFieldButtons()} } const labels = fieldTabs.map(t => t.label) // 按非字母数字汉字的分隔符拆分, 保留分隔符 const tokens = subLabelText.split(/(\s*·\s*|\s+)/).filter(t => t !== '') const used = new Set() return ( <> {tokens.map((tok, i) => { const trimmed = tok.trim() // 跳过纯分隔符 if (trimmed === '' || trimmed === '·') return {tok} // 匹配某个 tab label (整体匹配, 避免部分子串误命中) const idx = labels.indexOf(trimmed) if (idx >= 0 && !used.has(trimmed)) { used.add(trimmed) return {tok}{fieldIconButton(fieldTabs[idx])} } return {tok} })} ) } return (

{title}

{auto !== undefined && !loading && ( {auto ? '自动' : '关闭'} )} {active && } {done && !active && !skipped && } {skipped && !active && ( 本次跳过 )} {onSettings && ( )}
{hint}
{loading ? ( ) : ( )}
{loading ? ( <> ) : empty ? ( <>
暂无数据{renderFieldButtons()}
) : ( <>
{stats.fields ? stats.fields : stats.trading_days && !stats.rows ? stats.trading_days.toLocaleString() : formatNumber(stats.rows)}
{renderSubLabelInline()}
)}
{loading ? ( <>
) : empty ? ( <>
{isInstrument ? '快照日' : '起'}
{isInstrument ? '标的数' : '止'}
) : ( <>
{isInstrument ? '快照日' : '起'} {fmtDate(isInstrument ? stats.latest_as_of : stats.earliest_date)}
{isInstrument ? '标的数' : '止'} {isInstrument ? String(stats.rows) : fmtDate(stats.latest_date)}
)}
{active && stagePct > 0 && (
)}
) }