fix(canvas): 画布媒体 - 修复预览首帧音量和创作状态展示
This commit is contained in:
1 parent
3a2a794e8d
commit
92bbab8257
30 files changed
+689
-182
No files matched your search
@@ -578,7 +578,7 @@ export default function StorageSettingsPage() {
|
||||
<Form.Item name="requireCDN" label="必须走 CDN" valuePropName="checked" extra="开启后 CDN 鉴权配置不完整时直接失败,不会静默回源。">
|
||||
<Switch checkedChildren="严格" unCheckedChildren="允许回源" />
|
||||
</Form.Item>
|
||||
<Form.Item name="allowPrivateProxy" label="允许私有源站代理" valuePropName="checked" extra="仅作为显式兜底;开启后源站私有时由平台承担带宽。">
|
||||
<Form.Item name="allowPrivateProxy" label="允许模型输入代理" valuePropName="checked" extra="仅允许服务端向第三方模型提交参考素材时读取私有源站。浏览器展示、复制、下载和本地处理始终直连 OSS/CDN,不会经平台中转媒体正文。">
|
||||
<Switch checkedChildren="允许" unCheckedChildren="禁止" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@ import { WorkspaceState } from "@/components/layout/workspace-state";
|
||||
import { AssetMediaPreview } from "@/components/asset-media-preview";
|
||||
import { AssetLibraryCard, AssetLibraryCardMedia } from "@/components/assets/asset-library-card";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import { saveAs } from "file-saver";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { useCopyText } from "@/hooks/use-copy-text";
|
||||
@@ -23,6 +22,7 @@ import { resourceStorageLabel, resourceStorageLocation, resourceStorageTitle } f
|
||||
import { formatBytes, readFileAsDataUrl, readImageMeta } from "@/lib/image-utils";
|
||||
import { uploadImage } from "@/services/image-storage";
|
||||
import { uploadMediaFile } from "@/services/file-storage";
|
||||
import { downloadBrowserMedia } from "@/services/browser-download";
|
||||
import { flushAssetStorePersistence, useAssetStore, type Asset, type AssetCategory, type AssetKind, type ImageAsset } from "@/stores/use-asset-store";
|
||||
import { exportAssets, readAssetPackage } from "./asset-transfer";
|
||||
import { AssetStorageUsage, assetStorageUsageQueryKey } from "./asset-storage-usage";
|
||||
@@ -395,11 +395,15 @@ export default function AssetsPage() {
|
||||
copyText(asset.data.content, "文本已复制");
|
||||
};
|
||||
|
||||
const downloadImage = (asset: LibraryAsset) => {
|
||||
const downloadImage = async (asset: LibraryAsset) => {
|
||||
if (asset.kind !== "image" && asset.kind !== "video" && asset.kind !== "audio" && asset.kind !== "model") return;
|
||||
const url = asset.kind === "image" ? asset.data.dataUrl : asset.data.url;
|
||||
const extension = asset.kind === "model" ? asset.data.fileName.split(".").pop() || "glb" : asset.data.mimeType.split("/")[1] || "png";
|
||||
saveAs(url, `${asset.title || "asset"}.${extension}`);
|
||||
try {
|
||||
await downloadBrowserMedia({ storageKey: asset.data.storageKey, url, fileName: `${asset.title || "asset"}.${extension}` });
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "下载失败");
|
||||
}
|
||||
};
|
||||
|
||||
const exportAllAssets = async () => {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Modal } from "antd";
|
||||
import { XCircle } from "lucide-react";
|
||||
|
||||
import { CanvasImagePreview } from "@/components/canvas/canvas-image-preview";
|
||||
import { TaskDetailItem } from "./canvas-project-feedback";
|
||||
import { generationTaskShowsProgress, generationTaskStageLabel } from "@/lib/generation-task-display";
|
||||
import { canCancelGenerationTask, generationTaskShowsProgress, generationTaskStageLabel } from "@/lib/generation-task-display";
|
||||
import { formatTaskLog, type GenerationTask, type TaskLog } from "@/services/api/task-center";
|
||||
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
|
||||
import { VideoPlayer } from "@/components/video-player";
|
||||
import { AppModal } from "@/components/ui/product/app-modal";
|
||||
import { modelDisplayName, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import { resolveMediaUrl } from "@/services/file-storage";
|
||||
|
||||
type CanvasProjectStatusDialogsProps = {
|
||||
theme: { node: { stroke: string; panel: string; muted: string; fill: string } };
|
||||
@@ -52,7 +54,7 @@ export function CanvasProjectStatusDialogs({ theme, task, taskLogs, taskLoading,
|
||||
</div>
|
||||
</div>
|
||||
<TaskGenerationParameters inputJson={task.inputJson} theme={theme} />
|
||||
{onCancelTask && (task.status === "queued" || task.status === "running") ? (
|
||||
{onCancelTask && canCancelGenerationTask(task) ? (
|
||||
<div className="flex justify-end">
|
||||
<Button danger icon={<XCircle className="size-4" />} onClick={() => onCancelTask(task)}>
|
||||
取消任务
|
||||
@@ -77,7 +79,7 @@ export function CanvasProjectStatusDialogs({ theme, task, taskLogs, taskLoading,
|
||||
|
||||
<AppModal
|
||||
title="视频预览"
|
||||
open={Boolean(previewNode?.metadata?.content && previewNode.type === CanvasNodeType.Video)}
|
||||
open={Boolean(previewNode?.type === CanvasNodeType.Video && (previewNode.metadata?.content || previewNode.metadata?.storageKey))}
|
||||
centered
|
||||
onCancel={onClosePreview}
|
||||
footer={null}
|
||||
@@ -85,14 +87,15 @@ export function CanvasProjectStatusDialogs({ theme, task, taskLogs, taskLoading,
|
||||
flush
|
||||
styles={{ body: { display: "flex", justifyContent: "center", alignItems: "center", maxHeight: "84vh", overflow: "hidden", background: "var(--workspace-canvas-deep)" } }}
|
||||
>
|
||||
{previewNode?.metadata?.content && previewNode.type === CanvasNodeType.Video ? (
|
||||
<VideoPlayer src={previewNode.metadata.content} mimeType={previewNode.metadata.mimeType} title={previewNode.title || "视频预览"} hasAudio={typeof previewNode.metadata.hasAudio === "boolean" ? previewNode.metadata.hasAudio : undefined} className="max-h-[84vh] max-w-full bg-black" />
|
||||
{previewNode?.type === CanvasNodeType.Video && (previewNode.metadata?.content || previewNode.metadata?.storageKey) ? (
|
||||
<ResolvedCanvasVideoPreview node={previewNode} />
|
||||
) : null}
|
||||
</AppModal>
|
||||
|
||||
{previewNode?.metadata?.content && previewNode.type === CanvasNodeType.Image ? (
|
||||
{previewNode?.type === CanvasNodeType.Image && (previewNode.metadata?.content || previewNode.metadata?.storageKey) ? (
|
||||
<CanvasImagePreview
|
||||
src={previewNode.metadata.content}
|
||||
storageKey={previewNode.metadata.storageKey}
|
||||
alt={previewNode.title || "图片"}
|
||||
onClose={onClosePreview}
|
||||
/>
|
||||
@@ -118,6 +121,34 @@ export function CanvasProjectStatusDialogs({ theme, task, taskLogs, taskLoading,
|
||||
);
|
||||
}
|
||||
|
||||
function ResolvedCanvasVideoPreview({ node }: { node: CanvasNodeData }) {
|
||||
const [src, setSrc] = useState(node.metadata?.storageKey ? "" : node.metadata?.content || "");
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setSrc(node.metadata?.storageKey ? "" : node.metadata?.content || "");
|
||||
setFailed(false);
|
||||
void resolveMediaUrl(node.metadata?.storageKey, node.metadata?.content || "")
|
||||
.then((url) => {
|
||||
if (!cancelled) {
|
||||
setSrc(url);
|
||||
setFailed(!url);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setFailed(true);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [node.metadata?.content, node.metadata?.storageKey]);
|
||||
|
||||
if (failed) return <div className="grid min-h-72 w-full place-items-center text-sm text-white/55">视频地址获取失败,请关闭后重试</div>;
|
||||
if (!src) return <div className="grid min-h-72 w-full place-items-center text-sm text-white/55">正在加载视频…</div>;
|
||||
return <VideoPlayer src={src} mimeType={node.metadata?.mimeType} title={node.title || "视频预览"} hasAudio={typeof node.metadata?.hasAudio === "boolean" ? node.metadata.hasAudio : undefined} className="max-h-[84vh] max-w-full bg-black" />;
|
||||
}
|
||||
|
||||
function TaskGenerationParameters({ inputJson, theme }: { inputJson?: string; theme: CanvasProjectStatusDialogsProps["theme"] }) {
|
||||
const fields = taskParameterRows(inputJson);
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CanvasWorkspacePanel } from "@/components/canvas/canvas-workspace-panel";
|
||||
import { isCanvasNodeGenerating } from "@/lib/canvas/canvas-node-task-state";
|
||||
import { createCanvasStateWriter } from "@/lib/canvas/canvas-editor-state";
|
||||
import { canCancelGenerationTask } from "@/lib/generation-task-display";
|
||||
import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import type { Dispatch, MouseEvent as ReactMouseEvent, SetStateAction } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -570,6 +571,10 @@ function InfiniteCanvasPage() {
|
||||
|
||||
const cancelCanvasTask = useCallback(
|
||||
(task: import("@/services/api/task-center").GenerationTask) => {
|
||||
if (!canCancelGenerationTask(task)) {
|
||||
message.info("第三方请求已提交,任务将继续创作,不能取消");
|
||||
return;
|
||||
}
|
||||
modal.confirm({
|
||||
title: "取消生成任务?",
|
||||
content: "任务会立即停止本地执行;如果已经提交到上游,系统会继续核对取消结果和积分状态。",
|
||||
@@ -577,6 +582,11 @@ function InfiniteCanvasPage() {
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: "继续等待",
|
||||
onOk: async () => {
|
||||
const latestTask = taskDetail?.id === task.id ? taskDetail : task;
|
||||
if (!canCancelGenerationTask(latestTask)) {
|
||||
message.info("第三方请求已提交,任务将继续创作,不能取消");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const next = await cancelGenerationTask(task.id);
|
||||
const node = nodesRef.current.find((item) => item.metadata?.taskId === task.id);
|
||||
@@ -590,7 +600,7 @@ function InfiniteCanvasPage() {
|
||||
},
|
||||
});
|
||||
},
|
||||
[bindGenerationTask, message, modal, nodesRef, projectId, queryClient, setTaskDetail],
|
||||
[bindGenerationTask, message, modal, nodesRef, projectId, queryClient, setTaskDetail, taskDetail],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1331,6 +1341,13 @@ function InfiniteCanvasPage() {
|
||||
const characterCover = asset.character?.representations.find((item) => item.role === "turnaround_sheet") || asset.character?.representations.find((item) => item.role === "primary") || asset.character?.representations[0];
|
||||
const type = asset.category === "character" || asset.mediaType === "image" ? CanvasNodeType.Image : asset.mediaType === "video" ? CanvasNodeType.Video : asset.mediaType === "audio" ? CanvasNodeType.Audio : CanvasNodeType.Text;
|
||||
const remoteResourceId = resourceIdFromStorageKey(asset.storageKey);
|
||||
const storageKey = characterCover
|
||||
? `resource:${characterCover.resourceId}`
|
||||
: local?.kind === "image" || local?.kind === "video" || local?.kind === "audio"
|
||||
? local.data.storageKey
|
||||
: remoteResourceId
|
||||
? asset.storageKey
|
||||
: undefined;
|
||||
const content = characterCover
|
||||
? resourceFileUrl(characterCover.resourceId)
|
||||
: local?.kind === "image"
|
||||
@@ -1342,7 +1359,7 @@ function InfiniteCanvasPage() {
|
||||
: remoteResourceId
|
||||
? resourceFileUrl(remoteResourceId)
|
||||
: asset.previewText || "";
|
||||
const preview: CanvasNodeData = { id: asset.id, type, title: asset.title, position: { x: 0, y: 0 }, width: 240, height: 160, metadata: { assetId: asset.id, content } };
|
||||
const preview: CanvasNodeData = { id: asset.id, type, title: asset.title, position: { x: 0, y: 0 }, width: 240, height: 160, metadata: { assetId: asset.id, content, storageKey } };
|
||||
const current = result.get(asset.folderId) || [];
|
||||
current.push(preview);
|
||||
result.set(asset.folderId, current);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { App } from "antd";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { FOLDER_COLLAPSED_HEIGHT, FOLDER_COLLAPSED_WIDTH, FRAME_COLLAPSED_HEIGHT, FRAME_COLLAPSED_WIDTH, getFrameChildIds, isCanvasFolderNode, isFrameNode } from "@/lib/canvas/canvas-frame";
|
||||
@@ -10,6 +9,7 @@ import { writeCanvasNodePrompt } from "@/lib/canvas/canvas-node-prompt";
|
||||
import { applyBatchPrimaryImage, applyNodeConfigPatch } from "@/lib/canvas/canvas-project-domain";
|
||||
import { resetGenerationTaskMetadata } from "@/lib/canvas/canvas-project-generation";
|
||||
import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/generation-error";
|
||||
import { downloadBrowserMedia } from "@/services/browser-download";
|
||||
import { ensureCanvasNodeAsset } from "@/services/project-asset-sync";
|
||||
import { CanvasNodeType, type CanvasFolderStyle, type CanvasFolderTheme, type CanvasNodeData, type CanvasNodeMetadata, type Position } from "@/types/canvas";
|
||||
|
||||
@@ -196,10 +196,18 @@ export function useCanvasNodeEditor({
|
||||
.catch((error) => message.error(error instanceof Error ? error.message : "资产分类更新失败"));
|
||||
}, [canvasId, domainProjectId, message, nodesRef, queryClient, setNodes]);
|
||||
|
||||
const downloadNodeImage = useCallback((node: CanvasNodeData) => {
|
||||
if ((node.type !== CanvasNodeType.Image && node.type !== CanvasNodeType.Video && node.type !== CanvasNodeType.Audio) || !node.metadata?.content) return;
|
||||
saveAs(node.metadata.content, buildCanvasMediaDownloadFileName(canvasTitle, node));
|
||||
}, [canvasTitle]);
|
||||
const downloadNodeImage = useCallback(async (node: CanvasNodeData) => {
|
||||
const supported = node.type === CanvasNodeType.Image || node.type === CanvasNodeType.Video || node.type === CanvasNodeType.Audio;
|
||||
const content = node.metadata?.content?.trim();
|
||||
const storageKey = node.metadata?.storageKey?.trim();
|
||||
if (!supported || (!content && !storageKey)) return;
|
||||
try {
|
||||
const downloadName = buildCanvasMediaDownloadFileName(canvasTitle, node);
|
||||
await downloadBrowserMedia({ storageKey, url: content, fileName: downloadName });
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "下载失败");
|
||||
}
|
||||
}, [canvasTitle, message]);
|
||||
|
||||
const saveNodeAsset = useCallback(async (node: CanvasNodeData) => {
|
||||
if (node.type !== CanvasNodeType.Text && node.type !== CanvasNodeType.Image && node.type !== CanvasNodeType.Video && node.type !== CanvasNodeType.Audio) return message.error("当前节点类型不能保存为素材");
|
||||
|
||||
@@ -16,7 +16,8 @@ import { CANVAS_FOLDER_THEME_OPTIONS, resolveCanvasFolderTheme } from "@/lib/can
|
||||
import { resolveProjectCanvasStyle } from "@/components/canvas/canvas-style-picker-modal";
|
||||
import { CHARACTER_VOICE_FORMAT_LABEL, CHARACTER_VOICE_UPLOAD_ACCEPT, characterVoiceFormatName, characterVoiceTitleFromFileName, isSupportedCharacterVoiceFile } from "@/lib/character-voice-formats";
|
||||
import { ASSET_CATEGORIES, defaultAssetCategoryForKind, normalizeAssetCategory } from "@/lib/asset-category";
|
||||
import { resourceFileUrl, resourceIdFromStorageKey } from "@/services/api/resources";
|
||||
import { resourceFileUrl, resourceIdFromStorageKey, resourceStorageKey } from "@/services/api/resources";
|
||||
import { downloadBrowserMedia } from "@/services/browser-download";
|
||||
import { uploadMediaFile } from "@/services/file-storage";
|
||||
import {
|
||||
bindProjectCharacterVoice,
|
||||
@@ -44,7 +45,6 @@ import { saveRemoteUserDataNow } from "@/services/user-data-sync";
|
||||
import { useAssetStore, type Asset, type AssetCategory, type AssetStatus, type EntityAsset, type ImageAsset } from "@/stores/use-asset-store";
|
||||
import { useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import { CanvasNodeType, type CanvasFolderStyle, type CanvasFolderTheme, type CanvasNodeData } from "@/types/canvas";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
import { ProjectCharacterCard } from "./project-character-card";
|
||||
import { linkSelectedProjectAssets } from "./project-asset-linking";
|
||||
@@ -371,20 +371,28 @@ export default function ProjectAssetsView({ detail, refreshProject }: ProjectDet
|
||||
if (folderEditor?.folder) renameFolderMutation.mutate({ id: folderEditor.folder.id, name });
|
||||
else if (folderEditor) createFolderMutation.mutate({ name, parentId: folderEditor.parentId });
|
||||
};
|
||||
const downloadPreviewAsset = (asset: ProjectAsset) => {
|
||||
const personal = personalAssets.find((item) => item.id === asset.id);
|
||||
if (personal && (personal.kind === "image" || personal.kind === "video" || personal.kind === "audio" || personal.kind === "model")) {
|
||||
const url = personal.kind === "image" ? personal.data.dataUrl : personal.data.url;
|
||||
const extension = personal.kind === "model" ? personal.data.fileName.split(".").pop() || "glb" : personal.data.mimeType.split("/")[1] || "bin";
|
||||
saveAs(url, `${asset.title || "asset"}.${extension}`);
|
||||
return;
|
||||
}
|
||||
const cover = asset.character?.representations.find((item) => item.role === "turnaround_sheet") || asset.character?.representations.find((item) => item.role === "primary") || asset.character?.representations[0];
|
||||
if (cover) saveAs(resourceFileUrl(cover.resourceId), `${asset.title || "character"}.png`);
|
||||
else {
|
||||
const downloadPreviewAsset = async (asset: ProjectAsset) => {
|
||||
try {
|
||||
const personal = personalAssets.find((item) => item.id === asset.id);
|
||||
if (personal && (personal.kind === "image" || personal.kind === "video" || personal.kind === "audio" || personal.kind === "model")) {
|
||||
const url = personal.kind === "image" ? personal.data.dataUrl : personal.data.url;
|
||||
const extension = personal.kind === "model" ? personal.data.fileName.split(".").pop() || "glb" : personal.data.mimeType.split("/")[1] || "bin";
|
||||
await downloadBrowserMedia({ storageKey: personal.data.storageKey, url, fileName: `${asset.title || "asset"}.${extension}` });
|
||||
return;
|
||||
}
|
||||
const cover = asset.character?.representations.find((item) => item.role === "turnaround_sheet") || asset.character?.representations.find((item) => item.role === "primary") || asset.character?.representations[0];
|
||||
if (cover) {
|
||||
await downloadBrowserMedia({ storageKey: resourceStorageKey(cover.resourceId), fileName: `${asset.title || "character"}.png` });
|
||||
return;
|
||||
}
|
||||
const remoteUrl = projectAssetRemoteUrl(asset);
|
||||
if (remoteUrl) saveAs(remoteUrl, `${asset.title || "asset"}.${projectAssetFileExtension(asset.mediaType)}`);
|
||||
else message.warning("当前资产没有可下载的媒体文件");
|
||||
if (!remoteUrl) {
|
||||
message.warning("当前资产没有可下载的媒体文件");
|
||||
return;
|
||||
}
|
||||
await downloadBrowserMedia({ storageKey: asset.storageKey, url: remoteUrl, fileName: `${asset.title || "asset"}.${projectAssetFileExtension(asset.mediaType)}` });
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "下载失败");
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -22,6 +22,7 @@ import { formatVideoResolutionLabel } from "@/lib/video-generation-options";
|
||||
import { submitBackendGenerationTask } from "@/services/api/generation-task";
|
||||
import { quoteModel, type LogicalModelQuote } from "@/services/api/logical-models";
|
||||
import { type GenerationTask } from "@/services/api/task-center";
|
||||
import { downloadBrowserMedia } from "@/services/browser-download";
|
||||
import {
|
||||
createUnitWorkflow,
|
||||
deleteProjectShot,
|
||||
@@ -37,7 +38,7 @@ import {
|
||||
type ShotRevisionInput,
|
||||
type WorkflowStep,
|
||||
} from "@/services/api/projects";
|
||||
import { resourceFileUrl, resourceIdFromStorageKey } from "@/services/api/resources";
|
||||
import { resourceFileUrl, resourceIdFromStorageKey, resourceStorageKey } from "@/services/api/resources";
|
||||
import { skillRuntime } from "@/services/skill-runtime";
|
||||
import { configuredModelMatchesCapability, modelDisplayName, modelOptionName, resolveModelChannel, selectableModelsByCapability, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
@@ -757,15 +758,10 @@ function revisionInput(values: ShotEditorValues): ShotRevisionInput {
|
||||
async function downloadArtifact(artifact: ShotArtifact, shotTitle: string, onError: (content: string) => void) {
|
||||
if (!artifact.resourceId) return;
|
||||
try {
|
||||
const response = await fetch(resourceFileUrl(artifact.resourceId), { credentials: "include" });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = `${shotTitle || "shot"}-v${artifact.version}.${artifact.type === "video" ? "mp4" : "png"}`;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
await downloadBrowserMedia({
|
||||
storageKey: resourceStorageKey(artifact.resourceId),
|
||||
fileName: `${shotTitle || "shot"}-v${artifact.version}.${artifact.type === "video" ? "mp4" : "png"}`,
|
||||
});
|
||||
} catch (error) {
|
||||
onError(error instanceof Error ? `下载失败:${error.message}` : "下载失败");
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Eye, FileText, Image as ImageIcon, RotateCcw, Video } from "lucide-reac
|
||||
|
||||
import { MediaPreview } from "@/components/media-preview";
|
||||
import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/generation-error";
|
||||
import { generationTaskStatusLabel } from "@/lib/generation-task-display";
|
||||
import { generationTaskShowsProgress, generationTaskStageLabel, generationTaskStatusLabel } from "@/lib/generation-task-display";
|
||||
import type { GenerationTask } from "@/services/api/task-center";
|
||||
import { isTaskFailed, statusDotClassName, taskAttentionReason, TaskDate } from "./task-shared";
|
||||
import { TaskVideoThumbnail } from "./task-video-thumbnail";
|
||||
@@ -17,6 +17,8 @@ export function TaskGridCard({ task, actingId, onOpen, onRetry }: { task: Genera
|
||||
const isVideo = task.previewKind === "video";
|
||||
const thumbnailUrl = isVideo ? task.previewPosterUrl : task.previewUrl;
|
||||
const fallbackVideo = task.type.includes("video");
|
||||
const stageLabel = generationTaskStageLabel(task);
|
||||
const showsProgress = generationTaskShowsProgress(task);
|
||||
const Icon = fallbackVideo ? Video : task.type.includes("image") ? ImageIcon : FileText;
|
||||
return (
|
||||
<article className={`product-collection-card task-grid-card${isFailed ? " is-attention" : ""}`}>
|
||||
@@ -60,9 +62,9 @@ export function TaskGridCard({ task, actingId, onOpen, onRetry }: { task: Genera
|
||||
<TaskDate value={task.createdAt} />
|
||||
</span>
|
||||
</div>
|
||||
{isActive && task.mediaStage ? <p>{task.stage || "作品已生成,正在保存"}</p> : isActive ? (
|
||||
<div className="task-grid-progress" role="progressbar" aria-label={task.stage || "任务生成进度"} aria-valuemin={0} aria-valuemax={100} aria-valuenow={task.progress || 0}>
|
||||
<span>{task.stage || "正在生成"}</span>
|
||||
{isActive && !showsProgress ? <p>{stageLabel}</p> : isActive ? (
|
||||
<div className="task-grid-progress" role="progressbar" aria-label={stageLabel} aria-valuemin={0} aria-valuemax={100} aria-valuenow={task.progress || 0}>
|
||||
<span>{stageLabel}</span>
|
||||
<strong>{task.progress || 0}%</strong>
|
||||
<i><b style={{ width: `${task.progress || 0}%` }} /></i>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useState } from "react";
|
||||
|
||||
import { MediaPreview } from "@/components/media-preview";
|
||||
import { CONTENT_MODERATION_ERROR_CODE, generationErrorMessage, isContentModerationError } from "@/lib/generation-error";
|
||||
import { formatTaskKind, generationTaskStatusLabel } from "@/lib/generation-task-display";
|
||||
import { formatTaskKind, generationTaskShowsProgress, generationTaskStageLabel, generationTaskStatusLabel } from "@/lib/generation-task-display";
|
||||
import type { GenerationTask } from "@/services/api/task-center";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
import { formatModelName, getTaskCanvasContext, isTaskFailed, statusDotClassName, taskAttentionReason, TaskBilling, TaskDate } from "./task-shared";
|
||||
@@ -37,6 +37,8 @@ export function TaskListRow({
|
||||
const isActive = task.status === "queued" || task.status === "running";
|
||||
const isFailed = isTaskFailed(task);
|
||||
const retryDisabled = task.errorCode === CONTENT_MODERATION_ERROR_CODE || isContentModerationError(task.error);
|
||||
const stageLabel = generationTaskStageLabel(task);
|
||||
const showsProgress = generationTaskShowsProgress(task);
|
||||
return (
|
||||
<article className={`product-collection-card task-record-row group${isFailed ? " is-attention" : ""}`}>
|
||||
<TaskPreviewThumbnail task={task} onOpen={onPreview} />
|
||||
@@ -60,9 +62,9 @@ export function TaskListRow({
|
||||
{context.projectName ? ` · ${context.projectName}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
{isActive && task.mediaStage ? <p>{task.stage || "作品已生成,正在保存"}</p> : isActive ? (
|
||||
<div className="task-record-progress" role="progressbar" aria-label={task.stage || "任务生成进度"} aria-valuemin={0} aria-valuemax={100} aria-valuenow={task.progress || 0}>
|
||||
<span>{task.stage || "正在生成"}</span>
|
||||
{isActive && !showsProgress ? <p>{stageLabel}</p> : isActive ? (
|
||||
<div className="task-record-progress" role="progressbar" aria-label={stageLabel} aria-valuemin={0} aria-valuemax={100} aria-valuenow={task.progress || 0}>
|
||||
<span>{stageLabel}</span>
|
||||
<span>{task.progress || 0}%</span>
|
||||
<i>
|
||||
<b style={{ width: `${task.progress || 0}%` }} />
|
||||
|
||||
Reference in new issue
Block a user