feat: 自动候选筛选与股票研究工作台

This commit is contained in:
2026-10-09 20:39:23 +08:00
parent 92329ddce2
commit 57882ff568
32 changed files with 1343 additions and 87 deletions
+5 -3
View File
@@ -1,13 +1,15 @@
import {StockHistoryChart} from "./StockHistoryChart"
import {useEffect,useState} from "react"
import {ApiError,request} from "@/lib/auth"
import {Button} from "@/components/ui/button"
import {Card,CardContent,CardHeader,CardTitle} from "@/components/ui/card"
type Instrument={id:string;symbol:string;exchange:string;name:string;price:string;observed_at:string;received_at:string}
type Sync={id:string;status:string;instrument_count:number;message:string;finished_at:string}
export function MarketPanel({onSessionExpired}:{onSessionExpired:()=>void}){
const [list,setList]=useState<Instrument[]>([]),[tasks,setTasks]=useState<Sync[]>([]),[query,setQuery]=useState(""),[error,setError]=useState(""),[busy,setBusy]=useState(false)
export function MarketPanel({onSessionExpired,initialSymbol}:{onSessionExpired:()=>void;initialSymbol?:string}){
const [selected,setSelected]=useState<Instrument|null>(null)
const [list,setList]=useState<Instrument[]>([]),[tasks,setTasks]=useState<Sync[]>([]),[query,setQuery]=useState(initialSymbol??""),[error,setError]=useState(""),[busy,setBusy]=useState(false)
function fail(e:unknown){if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"行情读取失败")}
async function load(){setBusy(true);setError("");try{const [stocks,runs]=await Promise.all([request<Instrument[]>(`/api/admin/v1/instruments?q=${encodeURIComponent(query)}`),request<Sync[]>("/api/admin/v1/market/syncs")]);setList(stocks);setTasks(runs)}catch(e){fail(e)}finally{setBusy(false)}}
useEffect(()=>{void load()},[])
return <Card className="mt-6"><CardHeader><CardTitle>A 股自动行情同步</CardTitle></CardHeader><CardContent className="space-y-5"><p className="text-sm leading-7 text-muted-foreground">启用公开行情适配器后,后端启动即同步沪深股票,此后每五分钟更新。此接口用于开发研究,价格按供应商时间展示,休市或同步失败时保留上次快照。不会把抓取时间当成实时行情时间。</p><form className="flex gap-3" onSubmit={e=>{e.preventDefault();void load()}}><input className="w-full rounded-lg border px-3 py-2 text-sm" placeholder="按股票代码或名称搜索" value={query} onChange={e=>setQuery(e.target.value)}/><Button disabled={busy}>查询 / 刷新</Button></form>{error&&<p className="text-destructive text-sm">{error}</p>}<div className="max-h-96 overflow-auto"><table className="w-full text-left text-sm"><thead><tr>{["股票","价格","行情时间"].map(v=><th key={v} className="p-3">{v}</th>)}</tr></thead><tbody>{list.map(v=><tr className="border-t" key={v.id}><td className="p-3">{v.name}<p className="text-xs text-muted-foreground">{v.exchange}:{v.symbol}</p></td><td>{v.price==="0.00"?"无报价":v.price+" 元"}</td><td className="text-xs">{new Date(v.observed_at).toLocaleString("zh-CN")}</td></tr>)}</tbody></table>{list.length===0&&<p className="p-3 text-muted-foreground">暂无同步数据,请检查系统设置中的行情源与同步记录。</p>}</div><details><summary className="cursor-pointer text-sm">最近同步记录</summary><div className="mt-3 space-y-2">{tasks.length===0&&<p className="text-sm text-muted-foreground">还没有同步记录。</p>}{tasks.map(t=><div key={t.id} className="rounded-lg bg-muted p-3 text-xs">{new Date(t.finished_at).toLocaleString("zh-CN")} · {t.status==="COMPLETED"?`同步 ${t.instrument_count} 个证券`:t.message}</div>)}</div></details></CardContent></Card>
return <Card className="mt-6"><CardHeader><CardTitle>沪深股票池 · 行情与日线</CardTitle></CardHeader><CardContent className="space-y-5"><p className="text-sm leading-7 text-muted-foreground">启用公开行情适配器后,后端启动即同步沪深股票,此后每五分钟更新。此接口用于开发研究,价格按供应商时间展示,休市或同步失败时保留上次快照。不会把抓取时间当成实时行情时间。</p><form className="flex gap-3" onSubmit={e=>{e.preventDefault();void load()}}><input className="w-full rounded-lg border px-3 py-2 text-sm" placeholder="按股票代码或名称搜索" value={query} onChange={e=>setQuery(e.target.value)}/><Button disabled={busy}>查询 / 刷新</Button></form>{error&&<p className="text-destructive text-sm">{error}</p>}<div className="max-h-[560px] overflow-auto"><table className="w-full text-left text-sm"><thead><tr>{["股票","价格","行情时间"].map(v=><th key={v} className="p-3">{v}</th>)}</tr></thead><tbody>{list.map(v=><tr className="border-t" key={v.id}><td className="p-3"><button className="text-left text-base font-medium hover:text-primary" onClick={()=>setSelected(v)}>{v.name}</button><p className="text-xs text-muted-foreground">{v.exchange}:{v.symbol}</p></td><td className="font-mono text-lg">{v.price==="0.00"?"无报价":v.price+" 元"}</td><td className="text-xs">{new Date(v.observed_at).toLocaleString("zh-CN")}</td></tr>)}</tbody></table>{list.length===0&&<p className="p-3 text-muted-foreground">暂无同步数据,请检查系统设置中的行情源与同步记录。</p>}</div>{selected&&<div><div className="mb-4 flex items-end justify-between"><div><h2 className="text-xl font-semibold">{selected.name}</h2><p className="mt-1 font-mono text-xs text-muted-foreground">{selected.exchange}:{selected.symbol}</p></div><strong className="font-mono text-3xl">¥{selected.price}</strong></div><StockHistoryChart symbol={selected.symbol} exchange={selected.exchange} onSessionExpired={onSessionExpired}/></div>}<details><summary className="cursor-pointer text-sm">最近同步记录</summary><div className="mt-3 space-y-2">{tasks.length===0&&<p className="text-sm text-muted-foreground">还没有同步记录。</p>}{tasks.map(t=><div key={t.id} className="rounded-lg bg-muted p-3 text-xs">{new Date(t.finished_at).toLocaleString("zh-CN")} · {t.status==="COMPLETED"?`同步 ${t.instrument_count} 个证券`:t.message}</div>)}</div></details></CardContent></Card>
}