diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 62de75fc8..879af5c51 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -372,6 +372,7 @@ export interface AdminEditorShowcaseAssetPayload { taskId?: string | null; assetKind?: string | null; generationInputs?: Record | null; + thumbnailSrc?: string | null; generationCostMudPoints: number; refundMudPoints: number; reviewStatus: 'pending' | 'approved' | 'rejected' | string; diff --git a/apps/admin-web/src/components/AdminEditorAssetMedia.tsx b/apps/admin-web/src/components/AdminEditorAssetMedia.tsx new file mode 100644 index 000000000..9e93be572 --- /dev/null +++ b/apps/admin-web/src/components/AdminEditorAssetMedia.tsx @@ -0,0 +1,436 @@ +import { X } from 'lucide-react'; +import { useCallback, useEffect, useState } from 'react'; + +import type { AdminAssetReadUrlResponse } from '../api/adminApiClient'; +import { getAdminAssetReadUrl, isAdminApiError } from '../api/adminApiClient'; + +const ADMIN_ASSET_READ_EXPIRE_SECONDS = 300; +const ADMIN_ASSET_READ_DISPATCH_SPACING_MS = 40; +const ADMIN_ASSET_READ_RETRY_DELAYS_MS = [400, 1_200, 3_000] as const; +const ADMIN_ASSET_THUMBNAIL_ROOT_MARGIN = '240px 0px'; +const AUDIO_ASSET_COVER_SRC = `${import.meta.env.DEV ? import.meta.env.BASE_URL : '/'}creation-home/audio-asset-cover.png`; +let adminAssetReadDispatchTail = Promise.resolve(); + +export interface AdminPreviewableEditorAsset { + assetId: string; + label: string; + imageSrc: string; + objectKey?: string | null; + assetKind?: string | null; + thumbnailSrc?: string | null; +} + +export function AdminEditorAssetThumbnail({ + entry, + token, + altPrefix = '素材', +}: { + entry: AdminPreviewableEditorAsset; + token: string; + altPrefix?: string; +}) { + const thumbnailSource = resolveAdminAssetThumbnailSource(entry); + const { observeElement, shouldLoad } = useAdminAssetThumbnailVisibility(); + const imageSrc = useAdminResolvedAssetUrl( + token, + thumbnailSource.src, + thumbnailSource.objectKey, + shouldLoad, + ); + const alt = `${altPrefix}:${entry.label || entry.assetId}`; + + return imageSrc ? ( + {alt} + ) : ( +
+ ); +} + +export function AdminEditorAssetPreviewDialog({ + entry, + token, + onClose, +}: { + entry: AdminPreviewableEditorAsset; + token: string; + onClose: () => void; +}) { + return ( +
+
+
+
+

{entry.label || entry.assetId}

+ {entry.assetId} +
+ +
+ +
+
+ ); +} + +function AdminEditorAssetPreviewMedia({ + entry, + token, +}: { + entry: AdminPreviewableEditorAsset; + token: string; +}) { + const mediaKind = resolveAdminAssetMediaKind(entry); + const isAudio = mediaKind === 'audio'; + const isVideo = mediaKind === 'video'; + const mediaSrc = useAdminResolvedAssetUrl( + token, + entry.imageSrc, + entry.objectKey, + ); + const posterSrc = useAdminResolvedAssetUrl( + token, + isVideo ? (entry.thumbnailSrc ?? '') : '', + null, + ); + const label = entry.label || entry.assetId; + + if (isAudio) { + return ( +
+ {`音频封面:${label}`} + {mediaSrc ? ( +