import { ChevronDown, ChevronRight, Eye, FileText, RefreshCcw, X, } from 'lucide-react'; import { Fragment, type ReactNode } from 'react'; import { useEffect, useRef, useState } from 'react'; import { listAdminEditorAssets } from '../api/adminApiClient'; import type { AdminEditorAssetListQuery, AdminEditorAssetPayload, } from '../api/adminApiTypes'; import { AdminEditorAssetPreviewDialog, AdminEditorAssetThumbnail, } from '../components/AdminEditorAssetMedia'; import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton'; import { handlePageError } from './pageUtils'; interface AdminEditorAssetQueryPageProps { token: string; onUnauthorized: (message?: string) => void; } const ADMIN_EDITOR_ASSET_FILTER_DEBOUNCE_MS = 300; export function AdminEditorAssetQueryPage({ token, onUnauthorized, }: AdminEditorAssetQueryPageProps) { const [entries, setEntries] = useState([]); const [keyword, setKeyword] = useState(''); const [ownerUserId, setOwnerUserId] = useState(''); const [createdAfter, setCreatedAfter] = useState(''); const [createdBefore, setCreatedBefore] = useState(''); const [nextCursor, setNextCursor] = useState(null); const [committedQueryKey, setCommittedQueryKey] = useState( null, ); const [isLoading, setIsLoading] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [detailEntry, setDetailEntry] = useState(null); const [previewEntry, setPreviewEntry] = useState(null); const [expandedTaskIds, setExpandedTaskIds] = useState>( () => new Set(), ); const [promptPreview, setPromptPreview] = useState<{ title: string; prompt: string; } | null>(null); const listRequestGenerationRef = useRef(0); const listRequestAbortControllerRef = useRef(null); const automaticQueryInitializedRef = useRef(false); const automaticQueryTokenRef = useRef(token); const activeQuery = buildListQuery(); const activeQueryKey = editorAssetListQueryKey(token, activeQuery); const hasCurrentQuerySnapshot = committedQueryKey === activeQueryKey; const visibleEntries = hasCurrentQuerySnapshot ? entries : []; const visibleNextCursor = hasCurrentQuerySnapshot ? nextCursor : null; useEffect(() => { const runImmediately = !automaticQueryInitializedRef.current || automaticQueryTokenRef.current !== token; automaticQueryInitializedRef.current = true; automaticQueryTokenRef.current = token; if (runImmediately) { void refreshPage(); return; } invalidateListRequest(); setIsLoading(true); setIsLoadingMore(false); setErrorMessage(''); const timer = setTimeout( () => void refreshPage(), ADMIN_EDITOR_ASSET_FILTER_DEBOUNCE_MS, ); return () => clearTimeout(timer); // eslint-disable-next-line react-hooks/exhaustive-deps }, [token, ownerUserId, keyword, createdAfter, createdBefore]); useEffect( () => () => { automaticQueryInitializedRef.current = false; invalidateListRequest(); }, [], ); async function refreshPage() { const query = buildListQuery(); const queryKey = editorAssetListQueryKey(token, query); const { requestGeneration, abortController } = beginListRequest(); setIsLoading(true); setIsLoadingMore(false); setErrorMessage(''); try { const response = await listAdminEditorAssets( token, query, abortController.signal, ); if (listRequestGenerationRef.current !== requestGeneration) { return; } setEntries(response.entries); setNextCursor(response.nextCursor ?? null); setCommittedQueryKey(queryKey); } catch (error: unknown) { if ( listRequestGenerationRef.current !== requestGeneration || isAbortError(error) ) { return; } handlePageError(error, onUnauthorized, setErrorMessage); } finally { if (listRequestGenerationRef.current === requestGeneration) { if (listRequestAbortControllerRef.current === abortController) { listRequestAbortControllerRef.current = null; } setIsLoading(false); } } } async function loadMore() { if (!visibleNextCursor || isLoading || isLoadingMore) { return; } const query = buildListQuery(); const queryKey = editorAssetListQueryKey(token, query); if (committedQueryKey !== queryKey) { return; } const cursor = visibleNextCursor; const { requestGeneration, abortController } = beginListRequest(); setIsLoadingMore(true); setErrorMessage(''); try { const response = await listAdminEditorAssets( token, { ...query, cursor, }, abortController.signal, ); if (listRequestGenerationRef.current !== requestGeneration) { return; } setEntries((current) => mergeAssetEntries(current, response.entries)); setNextCursor(response.nextCursor ?? null); } catch (error: unknown) { if ( listRequestGenerationRef.current !== requestGeneration || isAbortError(error) ) { return; } handlePageError(error, onUnauthorized, setErrorMessage); } finally { if (listRequestGenerationRef.current === requestGeneration) { if (listRequestAbortControllerRef.current === abortController) { listRequestAbortControllerRef.current = null; } setIsLoadingMore(false); } } } function beginListRequest() { invalidateListRequest(); const abortController = new AbortController(); listRequestAbortControllerRef.current = abortController; const requestGeneration = listRequestGenerationRef.current; return { requestGeneration, abortController }; } function invalidateListRequest() { listRequestGenerationRef.current += 1; listRequestAbortControllerRef.current?.abort(); listRequestAbortControllerRef.current = null; } function buildListQuery(): AdminEditorAssetListQuery { return { ownerUserId: ownerUserId.trim() || null, keyword: keyword.trim() || null, createdAfter: dateInputToStartRfc3339(createdAfter), createdBefore: dateInputToEndRfc3339(createdBefore), limit: 80, }; } return (

素材查询

{errorMessage ? (
{errorMessage}
) : null}
{visibleEntries.map((entry) => { const promptText = entry.prompt || entry.actualPrompt || '-'; const expanded = expandedTaskIds.has(entry.assetId); const children = entry.children ?? []; return ( {expanded ? children.map((child) => { const childPrompt = child.prompt || child.actualPrompt || '-'; return ( ); }) : null} ); })}
资源图 创建时间 用户 提示词 生成器 生成成本 详情
{children.length ? ( ) : ( )}
{entry.label || '-'}
{formatDateTime(entry.createdAt)}
{authorDisplayName(entry)} {entry.authorPublicUserCode?.trim() || '-'}
{entry.taskGenerator || entry.generator || '-'} {taskGenerationCostLabel(entry)}
{child.label || '-'}
{formatDateTime(child.createdAt)} {authorDisplayName(child)} {child.generator || '-'} {stageGenerationCostLabel(child)}
{!isLoading && !errorMessage && visibleEntries.length === 0 ? (
暂无生成素材
) : null} {visibleNextCursor ? ( ) : null}
{detailEntry ? ( setDetailEntry(null)} onPreview={(entry) => setPreviewEntry(entry)} onPromptPreview={(entry, prompt) => setPromptPreview({ title: entry.label || entry.assetId, prompt, }) } /> ) : null} {previewEntry ? ( setPreviewEntry(null)} /> ) : null} {promptPreview ? (

完整提示词

{promptPreview.title}
              {promptPreview.prompt}
            
) : null}
); } function AdminAssetPromptButton({ entry, promptText, onOpen, }: { entry: AdminEditorAssetPayload; promptText: string; onOpen: (value: { title: string; prompt: string }) => void; }) { return ( ); } function AdminAssetDetailDialog({ entry, token, onClose, onPreview, onPromptPreview, onUnauthorized, }: { entry: AdminEditorAssetPayload; token: string; onUnauthorized: (message?: string) => void; onClose: () => void; onPreview: (entry: AdminEditorAssetPayload) => void; onPromptPreview: (entry: AdminEditorAssetPayload, prompt: string) => void; }) { const promptText = entry.prompt || entry.actualPrompt || ''; return (

{entry.label || entry.assetId}

{entry.assetId}
{authorDisplayName(entry)} {entry.authorPublicUserCode?.trim() || '-'}
{entry.ownerUserId} {entry.width} x {entry.height} {entry.children?.length ? taskGenerationCostLabel(entry) : stageGenerationCostLabel(entry)} {entry.children?.length ? entry.taskGenerator || entry.generator || '-' : entry.generator || '-'} {entry.children?.length ? ( {entry.generator || '-'} ) : null} {entry.model || '-'} {entry.provider || '-'} {entry.taskId || '-'} {entry.groupTaskId?.trim() && entry.groupTaskId.trim() !== entry.taskId?.trim() ? ( {entry.groupTaskId.trim()} ) : null} {entry.objectKey || '-'} {formatDateTime(entry.createdAt)} {formatDateTime(entry.updatedAt)} {promptText ? ( ) : ( '-' )}
                {formatGenerationInputs(entry.generationInputs)}
              
); } function taskGenerationCostLabel(entry: AdminEditorAssetPayload) { return `${entry.taskCostMudPoints ?? entry.generationCostMudPoints} 泥点`; } function stageGenerationCostLabel(entry: AdminEditorAssetPayload) { return entry.generationCostMudPoints > 0 ? `${entry.generationCostMudPoints} 泥点` : '本阶段不额外扣费'; } function AdminInfoItem({ label, children, }: { label: string; children: ReactNode; }) { return (
{label}
{children}
); } function mergeAssetEntries( current: AdminEditorAssetPayload[], incoming: AdminEditorAssetPayload[], ) { const byId = new Map(); [...current, ...incoming].forEach((entry) => byId.set(entry.assetId, entry)); return [...byId.values()].sort( (left, right) => parseAdminTimestamp(right.createdAt) - parseAdminTimestamp(left.createdAt) || right.assetId.localeCompare(left.assetId), ); } function editorAssetListQueryKey( token: string, query: AdminEditorAssetListQuery, ) { return JSON.stringify([ token.trim(), query.ownerUserId?.trim() || null, query.keyword?.trim() || null, query.createdAfter?.trim() || null, query.createdBefore?.trim() || null, query.limit ?? null, ]); } function isAbortError(error: unknown) { return ( typeof error === 'object' && error !== null && 'name' in error && error.name === 'AbortError' ); } function dateInputToStartRfc3339(value: string) { return value ? `${value}T00:00:00+08:00` : null; } function dateInputToEndRfc3339(value: string) { return value ? `${value}T23:59:59.999+08:00` : null; } function formatDateTime(value: string) { const timestamp = parseAdminTimestamp(value); if (!Number.isFinite(timestamp)) { return value || '-'; } return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }).format(timestamp); } function parseAdminTimestamp(value: string | null | undefined) { const normalizedValue = value?.trim() ?? ''; const secondsMicrosMatch = normalizedValue.match(/^(-?\d+)\.(\d{6})Z$/u); if (secondsMicrosMatch) { const seconds = Number(secondsMicrosMatch[1]); const micros = Number(secondsMicrosMatch[2]); if (Number.isFinite(seconds) && Number.isFinite(micros)) { return seconds * 1000 + Math.floor(micros / 1000); } } return Date.parse(normalizedValue); } function authorDisplayName(entry: AdminEditorAssetPayload) { return ( entry.authorDisplayName?.trim() || entry.authorPublicUserCode?.trim() || '-' ); } function formatGenerationInputs( value: Record | null | undefined, ) { if (!value) { return '-'; } return JSON.stringify(value, null, 2); }