From 1af765d52a286a58c0d0e9cbf94e4f1c2c1d40c6 Mon Sep 17 00:00:00 2001 From: MasterChiefCN <49179444+MasterChiefCN@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:14:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E8=B5=84=E6=BA=90=E9=A2=84?= =?UTF-8?q?=E8=A7=88=20-=20=E8=AF=B7=E6=B1=82=E6=98=8E=E7=BB=86=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E5=A4=B1=E6=95=88=E6=97=B6=E6=98=BE=E7=A4=BA=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E5=8D=A0=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../media-preview.tsx} | 4 ++-- web/src/pages/admin/logs/logs-page.tsx | 13 ++++++++----- web/src/pages/tasks/index.tsx | 6 +++--- web/src/pages/tasks/task-grid-card.tsx | 4 ++-- web/src/pages/tasks/task-list-row.tsx | 4 ++-- web/src/styles/globals.css | 8 ++++---- web/test/task-media-fallback.test.ts | 18 +++++++++++++----- 7 files changed, 34 insertions(+), 23 deletions(-) rename web/src/{pages/tasks/task-media-preview.tsx => components/media-preview.tsx} (88%) diff --git a/web/src/pages/tasks/task-media-preview.tsx b/web/src/components/media-preview.tsx similarity index 88% rename from web/src/pages/tasks/task-media-preview.tsx rename to web/src/components/media-preview.tsx index 0d2c7b11a..8f3579da2 100644 --- a/web/src/pages/tasks/task-media-preview.tsx +++ b/web/src/components/media-preview.tsx @@ -5,7 +5,7 @@ import { cn } from "@/lib/utils"; const DEFAULT_UNAVAILABLE_LABEL = "预览不可用,素材可能已删除"; -export function TaskMediaPreview({ +export function MediaPreview({ src, kind, alt = "", @@ -40,7 +40,7 @@ export function TaskMediaPreview({ if (unavailable) { return ( - + diff --git a/web/src/pages/admin/logs/logs-page.tsx b/web/src/pages/admin/logs/logs-page.tsx index e1faba19a..2eca586a9 100644 --- a/web/src/pages/admin/logs/logs-page.tsx +++ b/web/src/pages/admin/logs/logs-page.tsx @@ -6,6 +6,7 @@ import { useEffect, useRef, useState } from "react"; import { useSearchParams } from "react-router"; import { PaginationBar } from "@/components/layout/workspace-page"; +import { MediaPreview } from "@/components/media-preview"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { exportAdminApiLogs, listAdminApiLogs, type ApiCallLog } from "@/services/api/auth"; import { ApiLogDetailDrawer } from "../components/api-log-detail-drawer"; @@ -83,7 +84,7 @@ export default function LogsPage() { /> setDetailLogId(null)} onLogUpdated={(next) => setLogs((items) => items.map((item) => item.id === next.id ? next : item))} /> setMediaPreview(null)} footer={mediaPreview ? : null} destroyOnHidden> - {mediaPreview?.kind === "video" ? ); @@ -100,12 +101,14 @@ function formatCost(log: ApiCallLog) { return `${log.currency || "USD"} ${(log.e function MediaResult({ log, onPreview }: { log: ApiCallLog; onPreview: (url: string, kind: "image" | "video") => void }) { const url = log.mediaPreviewUrl; const kind = log.mediaPreviewKind; + const [unavailableUrl, setUnavailableUrl] = useState(""); if (!url || (kind !== "image" && kind !== "video")) return --; + const previewUnavailable = unavailableUrl === url; return
-
; diff --git a/web/src/pages/tasks/index.tsx b/web/src/pages/tasks/index.tsx index 6ba68f27a..1a6ecfddb 100644 --- a/web/src/pages/tasks/index.tsx +++ b/web/src/pages/tasks/index.tsx @@ -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 ? ( - { const isVideo = isVideoResult(url, taskType); return ( - void; onRetry: () => void; onCancel: () => void }) { @@ -17,7 +17,7 @@ export function TaskGridCard({ task, actingId, onOpen, onRetry, onCancel }: { ta
{task.previewUrl ? ( - + ) : ( )} diff --git a/web/src/pages/tasks/task-list-row.tsx b/web/src/pages/tasks/task-list-row.tsx index b79ecc982..ded078002 100644 --- a/web/src/pages/tasks/task-list-row.tsx +++ b/web/src/pages/tasks/task-list-row.tsx @@ -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} > - setUnavailableUrl(task.previewUrl || "")} /> + setUnavailableUrl(task.previewUrl || "")} /> {!previewUnavailable ? ( {isVideo ? : } diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css index 159fbbd70..dcdb3e5b0 100644 --- a/web/src/styles/globals.css +++ b/web/src/styles/globals.css @@ -4334,9 +4334,9 @@ .task-record-thumb { position: relative; display: grid; width: 84px; height: 60px; flex: 0 0 auto; place-items: center; overflow: hidden; border: 0; border-radius: var(--r-md); background: color-mix(in srgb, var(--foreground) 4%, transparent); padding: 0; color: color-mix(in srgb, var(--foreground) 28%, transparent); } /* 媒体脱离 Grid 固有尺寸计算,避免竖图按原始宽高比把固定缩略图撑出任务行。 */ .task-record-thumb img, .task-record-thumb video { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: cover; } - /* 资源已被删除或失效时,由组件接管原生破图,任务记录本身仍保持可读。 */ - .task-media-unavailable { display: flex; width: 100%; height: 100%; flex-direction: column; align-items: center; justify-content: center; gap: 5px; color: color-mix(in srgb, var(--foreground) 38%, transparent); font-size: var(--fs-tiny); line-height: 1.25; text-align: center; } - .task-media-unavailable > svg { width: 18px; height: 18px; flex: 0 0 auto; } + /* 资源已被删除或失效时,由组件接管原生破图,相关页面仍保持可读。 */ + .media-unavailable { display: flex; width: 100%; height: 100%; flex-direction: column; align-items: center; justify-content: center; gap: 5px; color: color-mix(in srgb, var(--foreground) 38%, transparent); font-size: var(--fs-tiny); line-height: 1.25; text-align: center; } + .media-unavailable > svg { width: 18px; height: 18px; flex: 0 0 auto; } .task-record-thumb:disabled { cursor: default; } .task-record-meta { display: flex; min-width: 0; align-items: center; gap: 8px; margin-top: 7px; overflow: hidden; color: color-mix(in srgb, var(--foreground) 40%, transparent); font-size: var(--fs-label); white-space: nowrap; } .task-record-meta-canvas { display: inline-flex; min-width: 0; align-items: center; gap: 4px; overflow: hidden; text-overflow: ellipsis; } @@ -4391,7 +4391,7 @@ .task-grid-card.is-attention .task-grid-body { background: color-mix(in srgb, var(--palette-status-error) 5%, transparent); } .task-grid-thumb { position: relative; display: grid; aspect-ratio: 16 / 9; place-items: center; overflow: hidden; background: color-mix(in srgb, var(--foreground) 4%, transparent); color: color-mix(in srgb, var(--foreground) 30%, transparent); } .task-grid-thumb > svg { width: 26px; height: 26px; } - .task-grid-thumb .task-media-unavailable > svg { width: 26px; height: 26px; } + .task-grid-thumb .media-unavailable > svg { width: 26px; height: 26px; } .task-grid-thumb img, .task-grid-thumb video { width: 100%; height: 100%; object-fit: cover; } .task-grid-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 6px; background: color-mix(in srgb, var(--background) 58%, transparent); opacity: 0; backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); transition: opacity 200ms ease-out; } .task-grid-card:hover .task-grid-overlay, .task-grid-card:focus-within .task-grid-overlay { opacity: 1; } diff --git a/web/test/task-media-fallback.test.ts b/web/test/task-media-fallback.test.ts index 15e429770..26d3b85bc 100644 --- a/web/test/task-media-fallback.test.ts +++ b/web/test/task-media-fallback.test.ts @@ -6,9 +6,9 @@ function source(path: string) { return readFileSync(resolve(import.meta.dir, path), "utf8"); } -describe("task media fallback", () => { +describe("media fallback", () => { test("replaces failed image and video elements with an unavailable state", () => { - const preview = source("../src/pages/tasks/task-media-preview.tsx"); + const preview = source("../src/components/media-preview.tsx"); expect(preview).toContain("failedSrc === src"); expect(preview).toContain("onError={handleUnavailable}"); @@ -21,9 +21,17 @@ describe("task media fallback", () => { const grid = source("../src/pages/tasks/task-grid-card.tsx"); const page = source("../src/pages/tasks/index.tsx"); - expect(list).toContain(" { + const page = source("../src/pages/admin/logs/logs-page.tsx"); + + expect(page.match(/ setUnavailableUrl(url)}"); }); });