559 lines
21 KiB
TypeScript
559 lines
21 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { useParams } from "next/navigation";
|
||
import { fetchAPI } from "@/lib/api";
|
||
import type { RecommendationData, DayGroup } from "@/lib/api";
|
||
import { getScoreColor } from "@/lib/utils";
|
||
import KlineChart from "@/components/kline-chart";
|
||
import CapitalFlowChart from "@/components/capital-flow";
|
||
import { ErrorBoundary } from "@/components/error-boundary";
|
||
|
||
interface StockSignals {
|
||
ts_code: string;
|
||
name: string;
|
||
score: number;
|
||
trend_score: number;
|
||
signal_count: number;
|
||
ma_bullish: boolean;
|
||
volume_breakout: boolean;
|
||
macd_golden: boolean;
|
||
rsi_healthy: boolean;
|
||
pullback_support: boolean;
|
||
big_yang: boolean;
|
||
boll_support: boolean;
|
||
support_price: number | null;
|
||
resist_price: number | null;
|
||
stop_loss_price: number | null;
|
||
rally_pct_5d: number;
|
||
rally_pct_10d: number;
|
||
distance_from_high: number;
|
||
position_score: number;
|
||
}
|
||
|
||
interface RecScore {
|
||
supply_demand_score: number;
|
||
price_action_score: number;
|
||
technical_score: number;
|
||
position_score: number;
|
||
score: number;
|
||
}
|
||
|
||
interface QuoteData {
|
||
ts_code: string;
|
||
name: string;
|
||
price: number;
|
||
pct_chg: number;
|
||
volume: number;
|
||
amount: number;
|
||
turnover_rate: number;
|
||
pe: number | null;
|
||
pb: number | null;
|
||
circ_mv: number | null;
|
||
total_mv: number | null;
|
||
volume_ratio: number | null;
|
||
high: number | null;
|
||
low: number | null;
|
||
open: number | null;
|
||
pre_close: number | null;
|
||
limit_up: number | null;
|
||
limit_down: number | null;
|
||
amplitude: number | null;
|
||
}
|
||
|
||
interface FlowRecord {
|
||
trade_date: string;
|
||
main_net_inflow: number;
|
||
net_mf_amount: number;
|
||
elg_net: number;
|
||
lg_net: number;
|
||
md_net: number;
|
||
sm_net: number;
|
||
}
|
||
|
||
export default function StockDetailPage() {
|
||
const params = useParams();
|
||
const code = params.code as string;
|
||
|
||
const [quote, setQuote] = useState<QuoteData | null>(null);
|
||
const [signals, setSignals] = useState<StockSignals | null>(null);
|
||
const [recScore, setRecScore] = useState<RecScore | null>(null);
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
const [kline, setKline] = useState<any[]>([]);
|
||
const [capitalFlow, setCapitalFlow] = useState<FlowRecord[]>([]);
|
||
|
||
useEffect(() => {
|
||
if (!code) return;
|
||
Promise.all([
|
||
fetchAPI<QuoteData>(`/api/stocks/${code}/quote`).catch(() => null),
|
||
fetchAPI<StockSignals>(`/api/stocks/${code}/signals`).catch(() => null),
|
||
fetchAPI<unknown[]>(`/api/stocks/${code}/kline?days=120`).catch(() => []),
|
||
fetchAPI<FlowRecord[]>(`/api/stocks/${code}/capital_flow?days=10`).catch(() => []),
|
||
]).then(([q, s, k, c]) => {
|
||
setQuote(q);
|
||
setSignals(s);
|
||
setKline(k);
|
||
setCapitalFlow(c as FlowRecord[]);
|
||
});
|
||
// 尝试从推荐历史中获取该股票的评分
|
||
fetchAPI<DayGroup[]>(`/api/recommendations/history?days=14`).then((history) => {
|
||
for (const group of history) {
|
||
const rec = group.recommendations?.find((r) => r.ts_code === code);
|
||
if (rec && rec.supply_demand_score) {
|
||
setRecScore({
|
||
supply_demand_score: rec.supply_demand_score,
|
||
price_action_score: rec.price_action_score ?? 0,
|
||
technical_score: rec.technical_score,
|
||
position_score: rec.position_score ?? 50,
|
||
score: rec.score,
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
}).catch(() => null);
|
||
}, [code]);
|
||
|
||
const latestFlow = capitalFlow.length > 0 ? capitalFlow[capitalFlow.length - 1] : null;
|
||
|
||
return (
|
||
<ErrorBoundary>
|
||
<div className="max-w-6xl mx-auto px-4 md:px-8 pt-6 pb-20 md:pb-10 space-y-4">
|
||
{/* Back */}
|
||
<a
|
||
href="/"
|
||
className="inline-flex items-center gap-1.5 text-xs text-text-muted hover:text-text-primary transition-colors animate-fade-in-up"
|
||
>
|
||
<svg
|
||
width="12"
|
||
height="12"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="2"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
>
|
||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||
</svg>
|
||
返回
|
||
</a>
|
||
|
||
{/* Quote header */}
|
||
<div className="animate-fade-in-up">
|
||
{quote && (
|
||
<div className="glass-card-static p-5">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-lg font-bold tracking-tight">{quote.name}</span>
|
||
<span className="text-sm text-text-muted font-mono tabular-nums">{quote.ts_code}</span>
|
||
</div>
|
||
<a
|
||
href={`/diagnose?code=${code}`}
|
||
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 bg-gradient-to-r from-amber-500/20 to-amber-600/15 text-amber-400 rounded-lg hover:from-amber-500/30 hover:to-amber-600/25 transition-all border border-amber-500/10"
|
||
>
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<path d="M9 11l3 3L22 4" />
|
||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
|
||
</svg>
|
||
AI 诊断
|
||
</a>
|
||
</div>
|
||
<div className="flex items-baseline gap-3">
|
||
<span
|
||
className={`text-3xl font-bold font-mono tabular-nums tracking-tight ${
|
||
quote.pct_chg > 0
|
||
? "text-red-400"
|
||
: quote.pct_chg < 0
|
||
? "text-emerald-400"
|
||
: "text-text-primary"
|
||
}`}
|
||
>
|
||
{quote.price.toFixed(2)}
|
||
</span>
|
||
<span
|
||
className={`text-sm font-mono tabular-nums font-medium ${
|
||
quote.pct_chg > 0 ? "text-red-400" : "text-emerald-400"
|
||
}`}
|
||
>
|
||
{quote.pct_chg > 0 ? "+" : ""}
|
||
{quote.pct_chg.toFixed(2)}%
|
||
</span>
|
||
{quote.pre_close && (
|
||
<span className="text-sm text-text-muted font-mono tabular-nums">
|
||
昨收 {quote.pre_close.toFixed(2)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* OHLC row */}
|
||
<div className="grid grid-cols-4 gap-3 mt-4">
|
||
<MiniStat
|
||
label="开盘"
|
||
value={quote.open && quote.open > 0 ? quote.open.toFixed(2) : "-"}
|
||
color={
|
||
quote.open && quote.open > 0 && quote.pre_close
|
||
? quote.open >= quote.pre_close
|
||
? "text-red-400"
|
||
: "text-emerald-400"
|
||
: undefined
|
||
}
|
||
/>
|
||
<MiniStat
|
||
label="最高"
|
||
value={quote.high && quote.high > 0 ? quote.high.toFixed(2) : "-"}
|
||
color="text-red-400"
|
||
/>
|
||
<MiniStat
|
||
label="最低"
|
||
value={quote.low && quote.low > 0 ? quote.low.toFixed(2) : "-"}
|
||
color="text-emerald-400"
|
||
/>
|
||
<MiniStat
|
||
label="振幅"
|
||
value={quote.amplitude != null ? `${quote.amplitude.toFixed(2)}%` : "-"}
|
||
/>
|
||
</div>
|
||
|
||
{/* Valuation row */}
|
||
<div className="grid grid-cols-4 gap-3 mt-3">
|
||
<MiniStat label="换手率" value={`${quote.turnover_rate?.toFixed(2)}%`} />
|
||
<MiniStat label="市盈率" value={quote.pe?.toFixed(1) ?? "-"} />
|
||
<MiniStat label="市净率" value={quote.pb?.toFixed(2) ?? "-"} />
|
||
<MiniStat
|
||
label="量比"
|
||
value={quote.volume_ratio?.toFixed(2) ?? "-"}
|
||
highlight={quote.volume_ratio != null && quote.volume_ratio > 2}
|
||
/>
|
||
</div>
|
||
|
||
{/* Market cap row */}
|
||
<div className="grid grid-cols-4 gap-3 mt-3">
|
||
<MiniStat
|
||
label="总市值"
|
||
value={quote.total_mv ? `${formatBigNum(quote.total_mv)}亿` : "-"}
|
||
/>
|
||
<MiniStat
|
||
label="流通市值"
|
||
value={quote.circ_mv ? `${formatBigNum(quote.circ_mv)}亿` : "-"}
|
||
/>
|
||
<MiniStat
|
||
label="涨停"
|
||
value={quote.limit_up?.toFixed(2) ?? "-"}
|
||
color="text-red-400"
|
||
/>
|
||
<MiniStat
|
||
label="跌停"
|
||
value={quote.limit_down?.toFixed(2) ?? "-"}
|
||
color="text-emerald-400"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Position Safety + Capital Flow Breakdown */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 animate-fade-in-up delay-75">
|
||
{/* Position Safety Card */}
|
||
{signals && (
|
||
<div className="glass-card-static p-5">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<span className="w-1 h-4 rounded-full" style={{ backgroundColor: getPositionColor(signals.position_score) }} />
|
||
<h2 className="text-sm font-bold tracking-tight">
|
||
仓位安全评估
|
||
</h2>
|
||
<span className={`text-lg font-bold font-mono tabular-nums ml-auto`} style={{ color: getPositionColor(signals.position_score) }}>
|
||
{Math.round(signals.position_score)}
|
||
<span className="text-[10px] text-text-muted ml-0.5">安全分</span>
|
||
</span>
|
||
</div>
|
||
{/* 位置安全评分条 */}
|
||
<div className="mb-4">
|
||
<div className="h-2.5 bg-surface-3 rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full rounded-full transition-all duration-700 ease-out"
|
||
style={{ width: `${Math.min(signals.position_score, 100)}%`, backgroundColor: getPositionColor(signals.position_score) }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
{/* Metrics */}
|
||
<div className="grid grid-cols-3 gap-3">
|
||
<PositionBar label="5日涨幅" value={signals.rally_pct_5d} />
|
||
<PositionBar label="10日涨幅" value={signals.rally_pct_10d} />
|
||
<PositionBar label="距高点" value={signals.distance_from_high} invert />
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Capital Flow Breakdown */}
|
||
{latestFlow && (
|
||
<CapitalFlowBreakdown flow={latestFlow} />
|
||
)}
|
||
</div>
|
||
|
||
{/* Technical signals */}
|
||
{signals && (
|
||
<div className="glass-card-static p-5 animate-fade-in-up delay-75">
|
||
<div className="flex items-center justify-between mb-5">
|
||
<h2 className="text-sm font-bold tracking-tight">
|
||
技术面分析
|
||
</h2>
|
||
{recScore && (
|
||
<div className={`text-2xl font-bold font-mono tabular-nums ${getScoreColor(recScore.score)}`}>
|
||
{recScore.score}
|
||
<span className="text-xs text-text-muted ml-1">综合分</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* ── Module 1: 核心评分维度 ── */}
|
||
<div className="mb-5 pb-5 border-b border-border-subtle">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<span className="w-1 h-4 rounded-full bg-amber-500/70" />
|
||
<span className="text-xs font-semibold text-text-secondary">核心评分维度</span>
|
||
{recScore && (
|
||
<span className="text-[10px] text-text-muted/40 ml-1">综合 = 供需×50% + 形态×40% + 趋势×10%</span>
|
||
)}
|
||
</div>
|
||
{recScore ? (
|
||
<div className="grid grid-cols-4 gap-3">
|
||
<DimensionScore label="供需关系" sublabel="50%权重" value={recScore.supply_demand_score} />
|
||
<DimensionScore label="价格形态" sublabel="40%权重" value={recScore.price_action_score} />
|
||
<DimensionScore label="趋势方向" sublabel="10%权重" value={recScore.technical_score} />
|
||
<DimensionScore label="位置安全" sublabel="防追高" value={recScore.position_score} />
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<DimensionScore label="趋势评分" sublabel="均线排列+结构+MA20方向" value={signals.trend_score} />
|
||
<DimensionScore label="信号计数" sublabel="触发即加分,仅供参考" value={(signals.signal_count / 7) * 100} displayValue={`${signals.signal_count}/7`} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* ── Module 2: 辅助信号 ── */}
|
||
<div className="mb-5 pb-5 border-b border-border-subtle">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<span className="w-1 h-4 rounded-full bg-cyan-500/70" />
|
||
<span className="text-xs font-semibold text-text-secondary">辅助信号</span>
|
||
<span className="text-[10px] text-text-muted/30">·触发即加分,不参与主评分</span>
|
||
<span className={`text-xs font-mono tabular-nums ml-auto ${signals.signal_count >= 4 ? "text-amber-400" : signals.signal_count >= 2 ? "text-cyan-400" : "text-text-muted"}`}>
|
||
{signals.signal_count}/7
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-4 gap-2">
|
||
<SignalChip label="均线多头" active={signals.ma_bullish} points={15} />
|
||
<SignalChip label="放量突破" active={signals.volume_breakout} points={20} />
|
||
<SignalChip label="MACD金叉" active={signals.macd_golden} points={15} />
|
||
<SignalChip label="RSI健康" active={signals.rsi_healthy} points={10} />
|
||
<SignalChip label="缩量回踩" active={signals.pullback_support} points={15} />
|
||
<SignalChip label="放量长阳" active={signals.big_yang} points={15} />
|
||
<SignalChip label="布林支撑" active={signals.boll_support} points={10} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Module 3: 关键价位 ── */}
|
||
<div>
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<span className="w-1 h-4 rounded-full bg-emerald-500/70" />
|
||
<span className="text-xs font-semibold text-text-secondary">关键价位</span>
|
||
</div>
|
||
<div className="grid grid-cols-3 gap-3">
|
||
<PriceLevel label="支撑位" value={signals.support_price} color="text-orange-400" />
|
||
<PriceLevel label="压力位" value={signals.resist_price} color="text-red-400" />
|
||
<PriceLevel label="止损位" value={signals.stop_loss_price} color="text-emerald-400" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* K-line + Capital flow chart */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 animate-fade-in-up delay-150">
|
||
{kline.length > 0 && <KlineChart data={kline} />}
|
||
{capitalFlow.length > 0 && <CapitalFlowChart data={capitalFlow} />}
|
||
</div>
|
||
</div>
|
||
</ErrorBoundary>
|
||
);
|
||
}
|
||
|
||
/* ── Helper components ── */
|
||
|
||
function MiniStat({
|
||
label,
|
||
value,
|
||
color,
|
||
highlight,
|
||
}: {
|
||
label: string;
|
||
value: string;
|
||
color?: string;
|
||
highlight?: boolean;
|
||
}) {
|
||
return (
|
||
<div className={`rounded-xl px-3 py-2.5 border ${highlight ? "border-amber-500/20 bg-amber-500/[0.04]" : "border-border-subtle bg-surface-1"}`}>
|
||
<div className="text-[11px] text-text-muted leading-tight">{label}</div>
|
||
<div className={`text-sm font-bold font-mono tabular-nums mt-0.5 ${color ?? ""}`}>
|
||
{value}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function PositionBar({ label, value, invert = false }: { label: string; value: number; invert?: boolean }) {
|
||
const absVal = Math.abs(value);
|
||
const maxDisplay = 30;
|
||
const pct = Math.min(absVal / maxDisplay, 1) * 100;
|
||
const isPositive = value > 0;
|
||
const showWarning = invert ? value < -20 : value > 20;
|
||
const barColor = showWarning ? "bg-amber-400" : isPositive ? "bg-red-400" : "bg-emerald-400";
|
||
const textColor = showWarning ? "text-amber-400" : isPositive ? "text-red-400" : "text-emerald-400";
|
||
|
||
return (
|
||
<div className="bg-surface-1 rounded-xl px-3 py-2.5 border border-border-subtle">
|
||
<div className="text-[10px] text-text-muted leading-tight">{label}</div>
|
||
<div className={`text-sm font-bold font-mono tabular-nums ${textColor}`}>
|
||
{isPositive ? "+" : ""}
|
||
{value.toFixed(1)}%
|
||
</div>
|
||
<div className="h-1.5 rounded-full bg-surface-3 overflow-hidden mt-1.5">
|
||
<div
|
||
className={`h-full rounded-full transition-all duration-500 ${barColor}`}
|
||
style={{ width: `${pct}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function CapitalFlowBreakdown({ flow }: { flow: FlowRecord }) {
|
||
const maxDisplay = Math.max(
|
||
...[flow.elg_net, flow.lg_net, flow.md_net, flow.sm_net].map(Math.abs),
|
||
1
|
||
);
|
||
const isMainInflow = flow.main_net_inflow > 0;
|
||
return (
|
||
<div className="glass-card-static p-5">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<span className={`w-1 h-4 rounded-full ${isMainInflow ? "bg-red-500/70" : "bg-emerald-500/70"}`} />
|
||
<h2 className="text-sm font-bold tracking-tight">
|
||
今日资金流向
|
||
</h2>
|
||
<span
|
||
className={`text-lg font-bold font-mono tabular-nums ml-auto ${isMainInflow ? "text-red-400" : "text-emerald-400"}`}
|
||
>
|
||
{isMainInflow ? "+" : ""}
|
||
{formatFlowAmount(flow.main_net_inflow)}
|
||
<span className="text-[10px] text-text-muted ml-0.5">主力净流入</span>
|
||
</span>
|
||
</div>
|
||
<div className="space-y-2.5">
|
||
<FlowBar label="特大单" value={flow.elg_net} max={maxDisplay} />
|
||
<FlowBar label="大单" value={flow.lg_net} max={maxDisplay} />
|
||
<FlowBar label="中单" value={flow.md_net} max={maxDisplay} />
|
||
<FlowBar label="小单" value={flow.sm_net} max={maxDisplay} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function FlowBar({ label, value, max }: { label: string; value: number; max: number }) {
|
||
const absVal = Math.abs(value);
|
||
const pct = (absVal / max) * 100;
|
||
const isInflow = value > 0;
|
||
|
||
return (
|
||
<div>
|
||
<div className="flex items-center justify-between text-xs mb-1">
|
||
<span className="text-text-muted">{label}</span>
|
||
<span
|
||
className={`font-mono tabular-nums ${isInflow ? "text-red-400" : "text-emerald-400"}`}
|
||
>
|
||
{isInflow ? "+" : ""}
|
||
{formatFlowAmount(value)}
|
||
</span>
|
||
</div>
|
||
<div className="h-1.5 rounded-full bg-surface-2 overflow-hidden">
|
||
<div
|
||
className={`h-full rounded-full transition-all duration-500 ${
|
||
isInflow ? "bg-red-400" : "bg-emerald-400"
|
||
}`}
|
||
style={{ width: `${pct}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SignalChip({ label, active, points }: { label: string; active: boolean; points: number }) {
|
||
return (
|
||
<div
|
||
className={`flex flex-col items-center py-2.5 rounded-xl transition-all duration-200 ${
|
||
active
|
||
? "bg-red-500/[0.08] border border-red-500/15"
|
||
: "bg-surface-1 border border-transparent"
|
||
}`}
|
||
>
|
||
<span className={`text-[11px] font-medium ${active ? "text-red-400" : "text-text-muted/60"}`}>
|
||
{label}
|
||
</span>
|
||
<span className={`text-[11px] font-mono tabular-nums mt-0.5 ${active ? "text-amber-400 font-semibold" : "text-text-muted/30"}`}>
|
||
{active ? `+${points}` : "—"}
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function DimensionScore({ label, sublabel, value, displayValue }: { label: string; sublabel: string; value: number; displayValue?: string }) {
|
||
const width = Math.min(value, 100);
|
||
const gradientClass = value >= 70 ? "score-bar-gradient-high" : value >= 50 ? "score-bar-gradient-mid" : "score-bar-gradient-low";
|
||
const scoreColor = value >= 70 ? "text-amber-400" : value >= 50 ? "text-cyan-400" : "text-text-muted";
|
||
return (
|
||
<div>
|
||
<div className="flex items-baseline justify-between mb-1.5">
|
||
<div>
|
||
<span className={`text-xs font-semibold ${scoreColor}`}>{label}</span>
|
||
<span className="text-[10px] text-text-muted/40 ml-1">{sublabel}</span>
|
||
</div>
|
||
<span className={`text-sm font-bold font-mono tabular-nums ${scoreColor}`}>
|
||
{displayValue ?? value.toFixed(0)}
|
||
</span>
|
||
</div>
|
||
<div className="h-2 bg-surface-3 rounded-full overflow-hidden">
|
||
<div
|
||
className={`h-full rounded-full transition-all duration-700 ease-out ${gradientClass}`}
|
||
style={{ width: `${width}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function PriceLevel({ label, value, color }: { label: string; value: number | null; color: string }) {
|
||
return (
|
||
<div className="bg-surface-1 rounded-xl px-3 py-2.5 border border-border-subtle">
|
||
<div className="text-[10px] text-text-muted leading-tight">{label}</div>
|
||
<div className={`text-sm font-bold font-mono tabular-nums ${color}`}>
|
||
{value?.toFixed(2) ?? "—"}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ── Helper functions ── */
|
||
|
||
function getPositionColor(score: number) {
|
||
if (score >= 70) return "#22c55e";
|
||
if (score >= 40) return "#eab308";
|
||
return "#ef4444";
|
||
}
|
||
|
||
function formatBigNum(val: number): string {
|
||
if (val >= 10000) return (val / 10000).toFixed(1) + "万";
|
||
return val.toFixed(0);
|
||
}
|
||
|
||
function formatFlowAmount(val: number): string {
|
||
const absVal = Math.abs(val);
|
||
if (absVal >= 10000) return (val / 10000).toFixed(1) + "亿";
|
||
return val.toFixed(0) + "万";
|
||
}
|