AigcPushRecordsPage.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. "use client";
  2. import Link from "next/link";
  3. import { useEffect, useMemo, useState } from "react";
  4. import { AppShell } from "@/components/AppShell";
  5. import { EmptyState, ErrorState, LoadingState } from "@/components/StateBlocks";
  6. import { listAigcPushRecords } from "@/lib/api/client";
  7. import type { AigcPushRecord } from "@/lib/api/types";
  8. import { platformLabel } from "@/lib/flow-ledger/business";
  9. function formatBeijingTime(value?: string | null): string {
  10. if (!value) return "未记录时间";
  11. const iso = /z$|[+-]\d{2}:\d{2}$/i.test(value) ? value : `${value}Z`;
  12. const date = new Date(iso);
  13. if (Number.isNaN(date.getTime())) return "时间待确认";
  14. return new Intl.DateTimeFormat("zh-CN", {
  15. timeZone: "Asia/Shanghai",
  16. year: "numeric",
  17. month: "2-digit",
  18. day: "2-digit",
  19. hour: "2-digit",
  20. minute: "2-digit",
  21. hour12: false
  22. }).format(date);
  23. }
  24. function byPushedDesc(a: AigcPushRecord, b: AigcPushRecord): number {
  25. return (b.pushed_at || "") > (a.pushed_at || "") ? 1 : (b.pushed_at || "") < (a.pushed_at || "") ? -1 : 0;
  26. }
  27. export function AigcPushRecordsPage() {
  28. const [records, setRecords] = useState<AigcPushRecord[]>([]);
  29. const [loading, setLoading] = useState(true);
  30. const [error, setError] = useState<string | null>(null);
  31. const [query, setQuery] = useState("");
  32. async function load() {
  33. setLoading(true);
  34. setError(null);
  35. try {
  36. const data = await listAigcPushRecords(new URLSearchParams({ limit: "500" }));
  37. setRecords(data.items || []);
  38. } catch (err) {
  39. setError(err instanceof Error ? err.message : String(err));
  40. } finally {
  41. setLoading(false);
  42. }
  43. }
  44. useEffect(() => {
  45. void load();
  46. }, []);
  47. const filtered = useMemo(() => {
  48. const q = query.trim().toLowerCase();
  49. const sorted = [...records].sort(byPushedDesc);
  50. if (!q) return sorted;
  51. return sorted.filter((record) => {
  52. const haystack = [
  53. record.push_record_id,
  54. record.run_id,
  55. record.platform,
  56. record.demand_content_id,
  57. record.demand_name,
  58. record.hive_merge_leve2,
  59. record.plans?.map((plan) => `${plan.crawler_plan_id} ${plan.produce_plan_id}`).join(" "),
  60. record.items?.map((item) => item.platform_content_id).join(" ")
  61. ].join(" ").toLowerCase();
  62. return haystack.includes(q);
  63. });
  64. }, [records, query]);
  65. return (
  66. <AppShell
  67. title="AIGC 推送记录"
  68. subtitle="只展示真实推送成功的 crawler plan,不展示 dry-run"
  69. showBack
  70. backHref="/runs"
  71. onRefresh={load}
  72. >
  73. <section className="aigc-page">
  74. <input
  75. className="rbd-search"
  76. type="search"
  77. placeholder="搜索需求 / run / 计划 ID / 视频 ID"
  78. value={query}
  79. onChange={(event) => setQuery(event.target.value)}
  80. />
  81. {loading ? <LoadingState /> : null}
  82. {error ? <ErrorState message={error} /> : null}
  83. {!loading && !error && filtered.length === 0 ? <EmptyState label="暂无真实 AIGC 推送记录" /> : null}
  84. <div className="aigc-record-list">
  85. {filtered.map((record) => (
  86. <article className="aigc-record" key={record.push_record_id}>
  87. <header className="aigc-record-head">
  88. <div>
  89. <strong>{record.demand_name || `需求 ${record.demand_content_id || ""}`}</strong>
  90. <span className="muted">
  91. {record.demand_content_id ? `#${record.demand_content_id} · ` : ""}
  92. {record.hive_merge_leve2 || "未知品类"} · {platformLabel(record.platform)}
  93. </span>
  94. </div>
  95. <div className="aigc-record-meta">
  96. <span className="chip green">已推送</span>
  97. <span className="muted">北京时间 {formatBeijingTime(record.pushed_at)}</span>
  98. </div>
  99. </header>
  100. <div className="aigc-record-grid">
  101. <div>
  102. <span className="muted">Run</span>
  103. <Link href={`/runs/${encodeURIComponent(record.run_id)}`}>{record.run_id}</Link>
  104. </div>
  105. <div>
  106. <span className="muted">需求生成</span>
  107. <b>{record.demand_dt || "未记录"}</b>
  108. </div>
  109. <div>
  110. <span className="muted">Hive gap</span>
  111. <b>{record.hive_gap_dt || "未记录"}</b>
  112. </div>
  113. <div>
  114. <span className="muted">视频数</span>
  115. <b>{record.pushed_content_count ?? record.items?.length ?? 0}</b>
  116. </div>
  117. </div>
  118. <div className="aigc-plan-list">
  119. {(record.plans || []).map((plan) => {
  120. const items = (record.items || []).filter((item) => item.plan_part === plan.plan_part);
  121. return (
  122. <details className="aigc-plan" key={`${record.push_record_id}-${plan.plan_part}`}>
  123. <summary>
  124. <span>第 {plan.plan_part} 组</span>
  125. <span className="muted">produce {plan.produce_plan_id}</span>
  126. <span className="muted">crawler {plan.crawler_plan_id}</span>
  127. <span className="chip">{plan.content_count} 条</span>
  128. </summary>
  129. <div className="aigc-video-list">
  130. {items.map((item) => (
  131. <span className="aigc-video-id" key={item.platform_content_id}>
  132. {item.platform_content_id}
  133. </span>
  134. ))}
  135. </div>
  136. </details>
  137. );
  138. })}
  139. </div>
  140. {record.notes ? <p className="muted">{record.notes}</p> : null}
  141. </article>
  142. ))}
  143. </div>
  144. </section>
  145. </AppShell>
  146. );
  147. }