Spaces:
Running
Running
| import React, { useState } from 'react' | |
| import { OHLCVPoint } from '../api/stockApi' | |
| interface Props { | |
| data: OHLCVPoint[] | |
| rows?: number | |
| } | |
| function fmt(v: number | undefined): string { | |
| if (v == null) return '—' | |
| const abs = Math.abs(v) | |
| const sign = v < 0 ? '-' : '+' | |
| if (abs >= 1_000_000) return `${sign}${(abs / 1_000_000).toFixed(2)}M` | |
| if (abs >= 1_000) return `${sign}${(abs / 1_000).toFixed(0)}K` | |
| return `${sign}${Math.round(abs)}` | |
| } | |
| function cls(v: number | undefined): string { | |
| if (v == null || v === 0) return 'text-slate-400' | |
| return v > 0 ? 'text-green-400' : 'text-red-400' | |
| } | |
| export const InstitutionalFlowTable: React.FC<Props> = ({ data, rows = 20 }) => { | |
| const [expanded, setExpanded] = useState(false) | |
| const visible = expanded ? rows : 10 | |
| const filtered = data | |
| .filter((d) => d.institutional_available) | |
| .slice(-rows) | |
| .reverse() | |
| .slice(0, visible) | |
| if (!filtered.length) return ( | |
| <p className="text-slate-500 text-xs px-2 py-3">尚無三大法人資料(僅台股 TWSE/TPEx)</p> | |
| ) | |
| return ( | |
| <div className="overflow-x-auto"> | |
| <table className="w-full text-xs border-collapse min-w-[640px]"> | |
| <thead> | |
| <tr className="text-slate-400 border-b border-slate-700"> | |
| <th className="text-left py-1.5 px-2 font-medium">日期</th> | |
| <th className="text-right py-1.5 px-2 font-medium text-blue-400">外資買</th> | |
| <th className="text-right py-1.5 px-2 font-medium text-blue-400">外資賣</th> | |
| <th className="text-right py-1.5 px-2 font-medium text-blue-300">外資淨</th> | |
| <th className="text-right py-1.5 px-2 font-medium text-yellow-400">投信買</th> | |
| <th className="text-right py-1.5 px-2 font-medium text-yellow-400">投信賣</th> | |
| <th className="text-right py-1.5 px-2 font-medium text-yellow-300">投信淨</th> | |
| <th className="text-right py-1.5 px-2 font-medium text-pink-400">自營買</th> | |
| <th className="text-right py-1.5 px-2 font-medium text-pink-400">自營賣</th> | |
| <th className="text-right py-1.5 px-2 font-medium text-pink-300">自營淨</th> | |
| <th className="text-right py-1.5 px-2 font-medium text-white">合計淨</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {filtered.map((d, i) => ( | |
| <tr key={d.date} className={i % 2 === 0 ? 'bg-slate-900/40' : ''}> | |
| <td className="py-1 px-2 text-slate-300 whitespace-nowrap"> | |
| {d.date} | |
| {d.institutional_carry_forward && ( | |
| <span className="ml-1 text-slate-600 text-[10px]">↑</span> | |
| )} | |
| </td> | |
| <td className={`py-1 px-2 text-right ${cls(d.foreign_buy)}`}>{fmt(d.foreign_buy)}</td> | |
| <td className={`py-1 px-2 text-right ${cls(-(d.foreign_sell ?? 0))}`}>{d.foreign_sell != null ? fmt(-d.foreign_sell) : '—'}</td> | |
| <td className={`py-1 px-2 text-right font-medium ${cls(d.foreign_net)}`}>{fmt(d.foreign_net)}</td> | |
| <td className={`py-1 px-2 text-right ${cls(d.trust_buy)}`}>{fmt(d.trust_buy)}</td> | |
| <td className={`py-1 px-2 text-right ${cls(-(d.trust_sell ?? 0))}`}>{d.trust_sell != null ? fmt(-d.trust_sell) : '—'}</td> | |
| <td className={`py-1 px-2 text-right font-medium ${cls(d.trust_net)}`}>{fmt(d.trust_net)}</td> | |
| <td className={`py-1 px-2 text-right ${cls(d.dealer_buy)}`}>{fmt(d.dealer_buy)}</td> | |
| <td className={`py-1 px-2 text-right ${cls(-(d.dealer_sell ?? 0))}`}>{d.dealer_sell != null ? fmt(-d.dealer_sell) : '—'}</td> | |
| <td className={`py-1 px-2 text-right font-medium ${cls(d.dealer_net)}`}>{fmt(d.dealer_net)}</td> | |
| <td className={`py-1 px-2 text-right font-bold ${cls(d.institutional_net)}`}>{fmt(d.institutional_net)}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| {filtered.length >= 10 && rows > 10 && ( | |
| <button | |
| onClick={() => setExpanded(e => !e)} | |
| className="mt-2 text-xs text-slate-500 hover:text-slate-300 px-2" | |
| > | |
| {expanded ? '▲ 收合' : `▼ 顯示更多(最近 ${rows} 日)`} | |
| </button> | |
| )} | |
| </div> | |
| ) | |
| } | |