19 lines
2.8 KiB
TypeScript
19 lines
2.8 KiB
TypeScript
import {useEffect,useRef,useState} from "react"
|
|
import {ScanLine} from "lucide-react"
|
|
import {request,ApiError} from "@/lib/auth"
|
|
import {Button} from "./ui/button"
|
|
type RunState={state:string;targets:number;completed:number;generated:number;skipped:number;failed:number;started_at:string|null;finished_at:string|null;message:string}
|
|
export function ScreeningRunControl({onSessionExpired,onCompleted}:{onSessionExpired:()=>void;onCompleted:()=>void}){
|
|
const [run,setRun]=useState<RunState|null>(null),[busy,setBusy]=useState(false),[error,setError]=useState("")
|
|
const lastFinished=useRef<string|null>(null),completed=useRef(onCompleted)
|
|
completed.current=onCompleted
|
|
function fail(e:unknown){if(e instanceof ApiError&&e.status===401)onSessionExpired();else setError(e instanceof Error?e.message:"筛选状态读取失败")}
|
|
useEffect(()=>{const controller=new AbortController();let fetching=false
|
|
const refresh=async()=>{if(fetching)return;fetching=true;try{const result=await request<RunState>("/api/admin/v1/strategy-screening-status",{signal:controller.signal});if(controller.signal.aborted)return;setRun(result);if(result.finished_at&&lastFinished.current!==result.finished_at){lastFinished.current=result.finished_at;completed.current()}}catch(e){if(!controller.signal.aborted)fail(e)}finally{fetching=false}}
|
|
void refresh();const timer=setInterval(()=>void refresh(),3000);return()=>{controller.abort();clearInterval(timer)}
|
|
},[])
|
|
async function scan(){setBusy(true);setError("");try{setRun(await request<RunState>("/api/admin/v1/strategy-screenings/run",{method:"POST",body:"{}"}))}catch(e){fail(e)}finally{setBusy(false)}}
|
|
const running=run?.state==="RUNNING"||run?.state==="QUEUED"
|
|
return <div className="rounded-xl border border-primary/20 bg-accent/40 p-5"><div className="flex flex-wrap items-center justify-between gap-4"><div><p className="font-medium">自动筛选 → 人工审核 → App 发布</p><p className="mt-2 text-sm text-muted-foreground">从已启用的策略范围获取真实日线,自动填写依据与价格规则,无需逐条录入股票。</p></div><Button disabled={busy||running} onClick={()=>void scan()}><ScanLine className={running?"animate-pulse":""}/>{running?"正在自动筛选":"立即自动筛选"}</Button></div><p className="mt-4 text-sm" aria-live="polite">{run?.finished_at&&"最近一批:"}{run?.message??"正在读取筛选状态…"}{run?.finished_at&&<span className="ml-3 text-xs text-muted-foreground">{new Date(run.finished_at).toLocaleString("zh-CN")}</span>}</p>{run?.state==="COMPLETED"&&run.targets===0&&<p className="mt-2 text-xs text-muted-foreground">当前没有新的可处理日线:已处理的结果会保留,数据陈旧或策略未启用时会等待下一次更新。</p>}{error&&<p role="alert" className="mt-3 text-sm text-destructive">{error}</p>}</div>
|
|
}
|