TopBar.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536
  1. import { Activity, AlertTriangle, ChevronsDown, ChevronsUp, Columns3, RefreshCw } from "lucide-react";
  2. import { type FormEvent, useEffect, useState } from "react";
  3. import type { BuildSummary, ExecutionViewV8 } from "@/lib/types";
  4. import { warningText } from "@/lib/types";
  5. import { statusLabel, StatusBadge } from "@/components/ui/Badges";
  6. import { moduleAuditUrl } from "@/lib/api";
  7. interface Props { builds: BuildSummary[]; selectedId: string; view: ExecutionViewV8; refreshing: boolean; onSelect: (id: string) => void; expandedRounds: Set<number>; allExpanded: boolean; onRound: (round: number) => void; onToggleAll: () => void; }
  8. export function TopBar({ builds, selectedId, view, refreshing, onSelect, expandedRounds, allExpanded, onRound, onToggleAll }: Props) {
  9. const [input, setInput] = useState(selectedId);
  10. useEffect(() => setInput(selectedId), [selectedId]);
  11. const submit = (event: FormEvent) => { event.preventDefault(); onSelect(input); };
  12. const noteCount = view.warnings.length;
  13. return <header className="runHeader" aria-label="运行控制">
  14. <div className="topBar">
  15. <div className="productMark"><span className="productIcon"><Activity size={17} /></span><h1>脚本构建</h1></div>
  16. <form className="buildPicker" onSubmit={submit}>
  17. <label className="srOnly" htmlFor="recent-build">最近构建</label>
  18. <select id="recent-build" aria-label="最近构建" value={builds.some((build) => build.id === selectedId) ? selectedId : ""} onChange={(event) => onSelect(event.target.value)}><option value="" disabled>{builds.length ? "选择构建" : "暂无列表"}</option>{builds.map((build) => <option key={build.id} value={build.id}>#{build.id} · {statusLabel(build.status)}</option>)}</select>
  19. <label className="srOnly" htmlFor="build-id">脚本构建 ID</label>
  20. <input id="build-id" value={input} onChange={(event) => setInput(event.target.value)} placeholder="Run ID" title="输入 Run ID 后按 Enter 打开" enterKeyHint="go" />
  21. </form>
  22. <nav className="roundNav" aria-label="轮次导航">{view.rounds.map((round) => <button type="button" key={round.id} className={expandedRounds.has(round.roundIndex) ? "active" : ""} onClick={() => onRound(round.roundIndex)}><span>第 {round.roundIndex} 轮</span><StatusBadge status={round.state} /></button>)}</nav>
  23. <div className="runMeta">
  24. {refreshing ? <span className="refreshing" aria-label="正在刷新"><RefreshCw size={13} /></span> : null}
  25. {view.dataShape === "legacy-incomplete" ? <StatusBadge status="legacy" /> : null}
  26. <a className="quietButton compactAction" href={moduleAuditUrl(selectedId)} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none" }} title="对照这个 Run 的模块实际使用、读取与原始记录"><Columns3 size={15} /><span>数据比对</span></a>
  27. <button className="quietButton compactAction" type="button" onClick={onToggleAll} aria-expanded={allExpanded} title={allExpanded ? "折叠全部内容" : "展开全部内容"}>{allExpanded ? <ChevronsUp size={15} /> : <ChevronsDown size={15} />}<span>{allExpanded ? "折叠" : "展开全部"}</span></button>
  28. {noteCount ? <details className="dataNotes"><summary aria-label={`数据说明 ${noteCount} 条`}><AlertTriangle size={14} /><span>数据说明</span><b>{noteCount}</b></summary><div><ul>{view.warnings.map((warning, index) => <li key={typeof warning === "string" ? warning : warning.id || index}>{warningText(warning)}</li>)}</ul><small>数据时间:{formatTime(view.capturedAt)}</small></div></details> : null}
  29. </div>
  30. </div>
  31. </header>;
  32. }
  33. const formatTime = (value: string) => { const date = new Date(value); return Number.isNaN(date.getTime()) ? value : date.toLocaleString("zh-CN", { hour12: false }); };