Checks / verify (push) Failing after 9m52s
统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
16 lines
3.4 KiB
TypeScript
16 lines
3.4 KiB
TypeScript
import {useEffect,useRef,useState,type FormEvent} from "react"
|
|
import {Button} from "./ui/button"
|
|
import {Input} from "./ui/input"
|
|
import {Badge} from "./ui/badge"
|
|
import {Card,CardContent,CardHeader,CardTitle,CardDescription} from "./ui/card"
|
|
import {ApiError,request} from "@/lib/auth"
|
|
type Quote={id:string;symbol:string;exchange:string;price:string;observed_at:string;version:number}
|
|
export function SimulationQuotesPanel({onSessionExpired}:{onSessionExpired:()=>void}){
|
|
const [quotes,setQuotes]=useState<Quote[]>([]),[symbol,setSymbol]=useState(""),[exchange,setExchange]=useState("SSE"),[price,setPrice]=useState(""),[error,setError]=useState(""),[busy,setBusy]=useState(false),controller=useRef<AbortController|null>(null)
|
|
function failed(e:unknown){if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"操作失败")}
|
|
async function refresh(){controller.current?.abort();const next=new AbortController();controller.current=next;try{const result=await request<Quote[]>("/api/admin/v1/simulation-quotes",{signal:next.signal});if(!next.signal.aborted)setQuotes(result)}catch(e){if(!next.signal.aborted)failed(e)}}
|
|
useEffect(()=>{void refresh();return()=>controller.current?.abort()},[])
|
|
async function save(e:FormEvent){e.preventDefault();setBusy(true);setError("");try{await request("/api/admin/v1/simulation-quotes",{method:"POST",body:JSON.stringify({symbol,exchange,price,observed_at:new Date().toISOString()})});setPrice("");await refresh()}catch(e){failed(e)}finally{setBusy(false)}}
|
|
return <Card className="mt-6"><CardHeader><div className="flex items-center gap-3"><CardTitle>模拟价格回放</CardTitle><Badge variant="outline">手工数据</Badge></div><CardDescription>用于验证计划条件和站内提醒。价格由此表单录入,不代表在线行情;触发不改变现金或持仓。</CardDescription></CardHeader><CardContent className="space-y-5"><form onSubmit={e=>void save(e)} className="grid items-end gap-4 sm:grid-cols-4"><label className="space-y-2 text-sm">证券代码<Input required value={symbol} pattern="[0-9]{6}" maxLength={6} disabled={busy} onChange={e=>setSymbol(e.target.value)}/></label><label className="space-y-2 text-sm">交易所<select value={exchange} disabled={busy} onChange={e=>setExchange(e.target.value)} className="h-9 w-full rounded-md border bg-background px-3"><option value="SSE">上交所</option><option value="SZSE">深交所</option></select></label><label className="space-y-2 text-sm">模拟价格(元)<Input required value={price} inputMode="decimal" disabled={busy} onChange={e=>setPrice(e.target.value)}/></label><Button type="submit" disabled={busy}>{busy?"提交中":"录入模拟价格"}</Button></form>{error&&<p role="alert" className="text-sm text-destructive">{error}</p>}<div className="overflow-auto"><table className="w-full text-left text-sm"><thead><tr className="border-b text-muted-foreground"><th className="p-3">证券</th><th className="p-3">价格</th><th className="p-3">观察时间</th><th className="p-3">版本</th></tr></thead><tbody>{quotes.map(q=><tr key={q.id} className="border-b"><td className="p-3">{q.exchange}:{q.symbol}</td><td className="p-3">¥{q.price}</td><td className="p-3 text-muted-foreground">{new Date(q.observed_at).toLocaleString("zh-CN")}</td><td className="p-3">{q.version}</td></tr>)}</tbody></table>{quotes.length===0&&<p className="py-6 text-center text-sm text-muted-foreground">尚未录入模拟价格。</p>}</div></CardContent></Card>
|
|
}
|