"use client"; import { useEffect, useState } from "react"; import useSWR from "swr"; import { RefreshCw, Search, ChevronLeft, ChevronRight, ExternalLink, Download, Star, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { toast } from "sonner"; import { MediaThumbnail } from "./media-thumbnail"; import { AdminSurface } from "./admin-surface"; import { formatRecordTime } from "@/lib/admin/record-time"; import { buildImageProxyUrl } from "@/lib/image-delivery"; export type MediaRecord = { id: string; recordType: "image" | "video"; userName: string | null; userEmail: string | null; userDepartment: string | null; createdAt: string; modelId: string | null; chatId: string | null; thumbnailUrl: string; originalUrl: string; videoUrl?: string | null; prompt: string | null; imageSize: string | null; isFeatured: boolean; featuredCategory: string | null; featuredAt?: string | null; }; type MediaResponse = { images: MediaRecord[]; pagination: { total: number; totalPages: number; totalImages: number; totalVideos: number; page: number; }; }; export async function adminFetcher(url: string) { const response = await fetch(url); const data = await response.json(); if (!response.ok) throw new Error(data.error || data.message || "加载失败,请重试"); return data; } const selectClass = "h-10 rounded-md border bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"; function recentDate() { const d = new Date(); d.setDate(d.getDate() - 6); return new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai" }).format(d); } export function MediaRecordsPanel({ categories }: { categories: string[] }) { const [view, setView] = useState("assets"); const [search, setSearch] = useState(""); const [filters, setFilters] = useState({ search: "", type: "all", startDate: recentDate(), endDate: "", model: "", featured: "false", status: "all", }); const [page, setPage] = useState(1); const [selected, setSelected] = useState(null); const [saving, setSaving] = useState(false); const [originalFailed, setOriginalFailed] = useState(false); const update = (changes: Partial) => { setFilters((f) => ({ ...f, ...changes })); setPage(1); }; useEffect(() => { const timer = setTimeout(() => { setFilters((f) => (f.search === search ? f : { ...f, search })); setPage(1); }, 350); return () => clearTimeout(timer); }, [search]); const { data: options } = useSWR<{ models: Array<{ id: string; modelId: string; displayName: string; providerName: string }>; }>("/api/admin/ai-models?view=options", adminFetcher, { revalidateOnFocus: false }); const modelNames = new Map(); for (const model of options?.models || []) if (!modelNames.has(model.modelId)) modelNames.set(model.modelId, model.displayName); const recordModelNames = new Map(modelNames); for (const model of options?.models || []) recordModelNames.set(model.id, model.displayName); const openDetails = (item: MediaRecord) => { setSelected(item); setOriginalFailed(false); }; const query = new URLSearchParams({ ...filters, page: String(page), pageSize: "40" }); const { data, error, isLoading, isValidating, mutate } = useSWR( view === "assets" ? `/api/admin/images?${query}` : null, adminFetcher, { keepPreviousData: true, revalidateOnFocus: false }, ); const tasks = useSWR<{ tasks: Array<{ id: string; userName: string; status: string; createdAt: string; error: string | null; model: string | null; elapsedMs: number | null; }>; total: number; }>(view === "tasks" ? `/api/admin/media-tasks?${query}` : null, adminFetcher, { keepPreviousData: true, revalidateOnFocus: false, }); const busy = view === "assets" ? isValidating : tasks.isValidating; const failure = view === "assets" ? error : tasks.error; const loading = view === "assets" ? isLoading : tasks.isLoading; const total = view === "assets" ? data?.pagination.total : tasks.data?.total; const pages = Math.max(1, Math.ceil((total || 0) / 40)); async function save(changes: Partial) { if (!selected || saving) return; setSaving(true); try { const response = await fetch("/api/admin/images", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ imageId: selected.id, ...changes }), }); if (!response.ok) throw new Error("保存失败,请重试"); setSelected({ ...selected, ...changes }); toast.success("已保存"); await mutate(); } catch (e) { toast.error(e instanceof Error ? e.message : "保存失败"); } finally { setSaving(false); } } return (

生成记录

查看图片与视频产物,追踪画布任务。时间统一按北京时间展示。

{[ ["assets", "产物记录"], ["tasks", "画布任务"], ].map(([key, label]) => ( ))}
{view === "assets" ? ( ) : ( )} {view === "assets" && (
)}
{total === undefined ? "正在读取记录…" : `共 ${total.toLocaleString()} 条记录`} {view === "assets" && data ? ` · 图片 ${data.pagination.totalImages.toLocaleString()} · 视频 ${data.pagination.totalVideos.toLocaleString()}` : ""} {busy && !loading && 正在更新…}
第 {page} / {pages} 页 · 每页 40 条
{failure ? (

{failure.message}

) : loading ? (
{[0, 1, 2, 3].map((x) => (
))} 正在加载记录
) : total === 0 ? (
没有符合条件的记录,请调整筛选条件。
) : (
{view === "assets" ? ( {["产物", "用户", "生成时间", "模型 / 尺寸", "精选", ""].map((v, i) => ( ))} {data?.images.map((item) => ( ))}
{v}
openDetails(item)} />
{item.userName || item.userEmail}
{item.userDepartment || "未设置部门"}
{formatRecordTime(item.createdAt)}
{recordModelNames.get(item.modelId || "") || item.modelId || "未记录模型"}
{item.recordType === "video" ? "视频" : "图片"} ·{" "} {item.imageSize || "未知尺寸"}
{item.isFeatured ? ( {item.featuredCategory || "精选"} ) : ( "—" )}
) : ( {["用户 / 时间", "状态", "模型", "耗时", "失败原因 / 任务编号"].map((x) => ( ))} {tasks.data?.tasks.map((task) => ( ))}
{x}
{task.userName}
{formatRecordTime(task.createdAt)}
{{ failed: "失败", running: "运行中", succeeded: "成功", pending: "排队中" }[ task.status ] || task.status} {task.model || "自动选择"} {task.elapsedMs === null ? "—" : `${Math.round(task.elapsedMs / 1000)} 秒`}

{task.error || "—"}

{task.id}
)}
)}
每页 40 条 · 第 {page} / {pages} 页
{ if (!open) setSelected(null); }} > {selected?.recordType === "video" ? "视频详情" : "图片详情"} {selected && (
{selected.recordType === "video" ? (
{[ ["用户", selected.userName || selected.userEmail], ["部门", selected.userDepartment], ["生成时间", formatRecordTime(selected.createdAt)], ["尺寸", selected.imageSize], ["模型", recordModelNames.get(selected.modelId || "") || selected.modelId], ["精选时间", selected.featuredAt ? formatRecordTime(selected.featuredAt) : "—"], ].map(([k, v]) => (
{k}
{v || "未记录"}
))}

提示词

{selected.prompt || "未记录提示词"}

{selected.recordType === "image" && ( )}
{selected.recordType === "image" && (
)}
)}
); }