| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- "use client";
- import { Search } from "lucide-react";
- import { useCallback, useEffect, useMemo, useState } from "react";
- import { AppShell } from "@/components/layout/AppShell";
- import { DataOriginBadge, StatusBadge } from "@/components/badges/StatusBadge";
- import { listRuns } from "@/lib/api/client";
- import { platformLabel } from "@/lib/platform/content";
- import { statusLabel } from "@/lib/status/status";
- import { DemandDetail } from "@/features/runs/DemandDetail";
- import type { RunListItem, RunListResponse } from "@/lib/api/types";
- // run 级状态仅此 4 种(RunState.status 合同);pending/rule_blocked 是内容/决策级状态,不在此列。
- const statusOptions = ["", "success", "partial_success", "failed", "running"];
- export function RunListPage() {
- const [status, setStatus] = useState("");
- const [search, setSearch] = useState("");
- const [data, setData] = useState<RunListResponse | null>(null);
- const [error, setError] = useState<string | null>(null);
- const [loading, setLoading] = useState(true);
- const [selectedId, setSelectedId] = useState<string | null>(null);
- const load = useCallback(async () => {
- setLoading(true);
- setError(null);
- const params = new URLSearchParams({ page_size: "50" });
- if (status) {
- params.set("status", status);
- }
- try {
- setData(await listRuns(params));
- } catch (err) {
- setError(err instanceof Error ? err.message : String(err));
- } finally {
- setLoading(false);
- }
- }, [status]);
- useEffect(() => {
- void load();
- }, [load]);
- const items = useMemo(() => {
- let source = data?.items || [];
- const keyword = search.trim().toLowerCase();
- if (keyword) {
- source = source.filter((item) =>
- [item.run_id, item.policy_run_id, item.error_code, item.platform_mode]
- .filter(Boolean)
- .some((value) => String(value).toLowerCase().includes(keyword))
- );
- }
- // 按运行时间倒序(最新在最上);无时间的(如冒烟测试)沉到最底。
- return [...source].sort((a, b) => {
- const ta = a.started_at ? Date.parse(a.started_at) : NaN;
- const tb = b.started_at ? Date.parse(b.started_at) : NaN;
- const va = Number.isNaN(ta) ? -Infinity : ta;
- const vb = Number.isNaN(tb) ? -Infinity : tb;
- return vb - va;
- });
- }, [data, search]);
- // 默认选中第一条(最新);当前选中项被筛掉时回退到第一条。
- useEffect(() => {
- if (!items.length) {
- setSelectedId(null);
- return;
- }
- if (!selectedId || !items.some((it) => it.run_id === selectedId)) {
- setSelectedId(items[0].run_id);
- }
- }, [items, selectedId]);
- const selected = items.find((it) => it.run_id === selectedId) || null;
- return (
- <AppShell onRefresh={load}>
- <section className="filter-bar">
- <div className="filter-title">
- <span>Run 列表</span>
- <strong>真实运行记录</strong>
- </div>
- <label className="field">
- <span>状态</span>
- <select className="select" value={status} onChange={(event) => setStatus(event.target.value)}>
- {statusOptions.map((option) => (
- <option key={option || "all"} value={option}>
- {option || "全部"}
- </option>
- ))}
- </select>
- </label>
- <label className="field">
- <span>搜索</span>
- <Search size={15} />
- <input
- className="input"
- value={search}
- onChange={(event) => setSearch(event.target.value)}
- placeholder="run_id / error_code"
- />
- </label>
- {data ? <DataOriginBadge origin={data.data_origin} /> : null}
- </section>
- <section className="main-grid">
- <div className="list-panel">
- {loading ? <div className="loading-state">加载中</div> : null}
- {error ? <div className="error-state">{error}</div> : null}
- {!loading && !error && !items.length ? <div className="empty-state">没有匹配的 run</div> : null}
- {!loading && !error
- ? items.map((item) => (
- <RunCard
- item={item}
- active={item.run_id === selectedId}
- onSelect={() => setSelectedId(item.run_id)}
- key={item.run_id}
- />
- ))
- : null}
- </div>
- <div className="detail-panel">
- {selected ? (
- <DemandDetail item={selected} key={selected.run_id} />
- ) : (
- <div className="empty-state">左侧选择一条需求查看详情。</div>
- )}
- </div>
- </section>
- </AppShell>
- );
- }
- // 内容形态文案;接口给英文枚举(video/image/...),列表统一显示中文。
- function contentFormatLabel(value: unknown): string {
- const map: Record<string, string> = { video: "视频", image: "图文", note: "图文", live: "直播" };
- const key = String(value || "").toLowerCase();
- return map[key] || (key ? key : "");
- }
- // ISO 时间 → 「YYYY-MM-DD HH:mm」(本地时区);拿不到则空。
- function fmtRunTime(iso?: string | null): string {
- if (!iso) return "";
- const d = new Date(iso);
- if (Number.isNaN(d.getTime())) return "";
- const p = (n: number) => String(n).padStart(2, "0");
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
- }
- function RunCard({ item, active, onSelect }: { item: RunListItem; active: boolean; onSelect: () => void }) {
- // 数据源行:平台 · 内容形态 · 策略(用户选「显示数据源平台+格式」取代未落库的服务器字段)。
- const source = [
- item.platform ? platformLabel(item.platform) : "",
- contentFormatLabel(item.content_format),
- item.strategy_version ? `策略 ${item.strategy_version}` : ""
- ].filter(Boolean);
- const runTime = fmtRunTime(item.started_at);
- return (
- <button type="button" className={`run-card ${active ? "active" : ""}`} onClick={onSelect}>
- <div className="run-card-title">
- <span className="run-card-demand">{item.demand_name || item.run_id}</span>
- <StatusBadge status={item.status} />
- </div>
- <div className="run-card-meta">
- <span className="run-card-time">🕒 {runTime || "运行时间未知"}</span>
- {source.length ? <span>{source.join(" · ")}</span> : null}
- </div>
- <div className="run-card-meta">
- <span className="run-card-id">{item.run_id}</span>
- {item.validation_status ? <span>校验{statusLabel(item.validation_status)}</span> : null}
- {item.error_code ? <span className="run-card-err">{item.error_code}</span> : null}
- </div>
- </button>
- );
- }
|