fix(web): 资源预览 - 请求明细资源失效时显示预览占位
This commit is contained in:
@@ -3,6 +3,7 @@ import { LayoutGrid, List, Plus, RefreshCw, Search, Trash2 } from "lucide-react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
import { MediaPreview } from "@/components/media-preview";
|
||||
import { ListToolbar, PageHeader, PaginationBar, WorkspacePage } from "@/components/layout/workspace-page";
|
||||
import { WorkspaceState } from "@/components/layout/workspace-state";
|
||||
import { CONTENT_MODERATION_ERROR_CODE, generationErrorMessage, isContentModerationError } from "@/lib/generation-error";
|
||||
@@ -19,7 +20,6 @@ import { listProjects, type ProjectSummary } from "@/services/api/projects";
|
||||
import { TaskGridCard } from "./task-grid-card";
|
||||
import { TaskGroupHeader, type TaskGroup } from "./task-group-header";
|
||||
import { TaskListRow } from "./task-list-row";
|
||||
import { TaskMediaPreview } from "./task-media-preview";
|
||||
import { formatModelName, getTaskCanvasContext, isTaskFailed, providerCancelStatusLabel, taskMediaKind } from "./task-shared";
|
||||
import { TaskStatusFilterBar, type TaskStatusFilter } from "./task-status-filter";
|
||||
|
||||
@@ -587,7 +587,7 @@ export default function TasksPage() {
|
||||
className="library-modal task-media-preview-modal"
|
||||
>
|
||||
{mediaPreview ? (
|
||||
<TaskMediaPreview
|
||||
<MediaPreview
|
||||
src={mediaPreview.url}
|
||||
kind={mediaPreview.kind}
|
||||
alt={mediaPreview.title}
|
||||
@@ -628,7 +628,7 @@ function TaskResultMedia({ value, taskType }: { value?: string; taskType: string
|
||||
{urls.map((url, index) => {
|
||||
const isVideo = isVideoResult(url, taskType);
|
||||
return (
|
||||
<TaskMediaPreview
|
||||
<MediaPreview
|
||||
key={`${url}-${index}`}
|
||||
src={url}
|
||||
kind={isVideo ? "video" : "image"}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Button, Tooltip } from "antd";
|
||||
import { Eye, FileText, Image as ImageIcon, RotateCcw, Video, X } from "lucide-react";
|
||||
|
||||
import { MediaPreview } from "@/components/media-preview";
|
||||
import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/generation-error";
|
||||
import { statusLabel } from "@/lib/generation-task-display";
|
||||
import type { GenerationTask } from "@/services/api/task-center";
|
||||
import { TaskMediaPreview } from "./task-media-preview";
|
||||
import { isTaskFailed, statusDotClassName, TaskDate } from "./task-shared";
|
||||
|
||||
export function TaskGridCard({ task, actingId, onOpen, onRetry, onCancel }: { task: GenerationTask; actingId: string; onOpen: () => void; onRetry: () => void; onCancel: () => void }) {
|
||||
@@ -17,7 +17,7 @@ export function TaskGridCard({ task, actingId, onOpen, onRetry, onCancel }: { ta
|
||||
<article className={`task-grid-card${isFailed ? " is-attention" : ""}`}>
|
||||
<div className="task-grid-thumb">
|
||||
{task.previewUrl ? (
|
||||
<TaskMediaPreview src={task.previewUrl} kind={isVideo ? "video" : "image"} loading="lazy" className="h-full w-full object-cover" />
|
||||
<MediaPreview src={task.previewUrl} kind={isVideo ? "video" : "image"} loading="lazy" className="h-full w-full object-cover" />
|
||||
) : (
|
||||
<Icon />
|
||||
)}
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Button, Tooltip } from "antd";
|
||||
import { Eye, FileText, FolderKanban, Image as ImageIcon, Play, RotateCcw, Video, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { MediaPreview } from "@/components/media-preview";
|
||||
import { CONTENT_MODERATION_ERROR_CODE, generationErrorMessage, isContentModerationError } from "@/lib/generation-error";
|
||||
import { formatTaskKind, statusLabel } from "@/lib/generation-task-display";
|
||||
import type { GenerationTask } from "@/services/api/task-center";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
import { TaskMediaPreview } from "./task-media-preview";
|
||||
import { formatModelName, getTaskCanvasContext, isTaskFailed, statusDotClassName, taskAttentionReason, TaskBilling, TaskDate } from "./task-shared";
|
||||
|
||||
export function TaskListRow({
|
||||
@@ -126,7 +126,7 @@ function TaskPreviewThumbnail({ task, onOpen }: { task: GenerationTask; onOpen:
|
||||
aria-label={previewUnavailable ? "预览不可用,素材可能已删除" : isVideo ? "放大预览生成视频" : "放大预览生成图片"}
|
||||
title={previewUnavailable ? "预览不可用,素材可能已删除" : undefined}
|
||||
>
|
||||
<TaskMediaPreview src={task.previewUrl} kind={isVideo ? "video" : "image"} width={68} height={48} loading="lazy" className="h-full w-full object-cover" fallbackLabel="预览不可用" onUnavailable={() => setUnavailableUrl(task.previewUrl || "")} />
|
||||
<MediaPreview src={task.previewUrl} kind={isVideo ? "video" : "image"} width={68} height={48} loading="lazy" className="h-full w-full object-cover" fallbackLabel="预览不可用" onUnavailable={() => setUnavailableUrl(task.previewUrl || "")} />
|
||||
{!previewUnavailable ? (
|
||||
<span className="absolute inset-0 grid place-items-center bg-black/0 text-white opacity-0 transition-[background-color,opacity] duration-150 group-hover:bg-black/30 group-hover:opacity-100 group-focus-visible:bg-black/30 group-focus-visible:opacity-100">
|
||||
{isVideo ? <Play className="size-4 fill-current" /> : <Eye className="size-4" />}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { ImageOff } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DEFAULT_UNAVAILABLE_LABEL = "预览不可用,素材可能已删除";
|
||||
|
||||
export function TaskMediaPreview({
|
||||
src,
|
||||
kind,
|
||||
alt = "",
|
||||
className,
|
||||
fallbackClassName,
|
||||
fallbackLabel = DEFAULT_UNAVAILABLE_LABEL,
|
||||
controls = false,
|
||||
loading,
|
||||
width,
|
||||
height,
|
||||
onUnavailable,
|
||||
}: {
|
||||
src: string;
|
||||
kind: "image" | "video";
|
||||
alt?: string;
|
||||
className?: string;
|
||||
fallbackClassName?: string;
|
||||
fallbackLabel?: string;
|
||||
controls?: boolean;
|
||||
loading?: "eager" | "lazy";
|
||||
width?: number;
|
||||
height?: number;
|
||||
onUnavailable?: () => void;
|
||||
}) {
|
||||
const [failedSrc, setFailedSrc] = useState("");
|
||||
const unavailable = failedSrc === src;
|
||||
|
||||
const handleUnavailable = () => {
|
||||
setFailedSrc(src);
|
||||
onUnavailable?.();
|
||||
};
|
||||
|
||||
if (unavailable) {
|
||||
return (
|
||||
<span className={cn("task-media-unavailable", fallbackClassName)} role="img" aria-label={fallbackLabel} title={fallbackLabel}>
|
||||
<ImageOff aria-hidden="true" />
|
||||
<span>{fallbackLabel}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (kind === "video") {
|
||||
return <video src={src} width={width} height={height} muted={!controls} playsInline controls={controls} preload="metadata" className={className} onError={handleUnavailable} />;
|
||||
}
|
||||
|
||||
return <img src={src} alt={alt} width={width} height={height} loading={loading} className={className} onError={handleUnavailable} />;
|
||||
}
|
||||
Reference in New Issue
Block a user