2026-06-02 12:38:01 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ArrowLeftOutlined,
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
FileTextOutlined,
|
|
|
|
|
|
LoginOutlined,
|
|
|
|
|
|
PictureOutlined,
|
|
|
|
|
|
UploadOutlined,
|
|
|
|
|
|
} from "@ant-design/icons";
|
2026-06-08 21:30:48 +08:00
|
|
|
|
import { useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent } from "react";
|
2026-06-02 12:38:01 +08:00
|
|
|
|
import { aiGenerationClient } from "../../api/aiGenerationClient";
|
|
|
|
|
|
import { communityClient } from "../../api/communityClient";
|
|
|
|
|
|
import WorkspacePageShell from "../../components/WorkspacePageShell";
|
2026-06-08 16:32:16 +08:00
|
|
|
|
import "../../styles/pages/compliance.css";
|
2026-06-02 12:38:01 +08:00
|
|
|
|
import type { WebCanvasWorkflow, WebUserSession } from "../../types";
|
|
|
|
|
|
import { getWorkflowCoverUrl, isCanvasWorkflow } from "../community/communityCaseUtils";
|
|
|
|
|
|
import { canManageCommunityCases } from "./communityPermissions";
|
|
|
|
|
|
|
|
|
|
|
|
interface CommunityCaseAddPageProps {
|
|
|
|
|
|
session: WebUserSession | null;
|
|
|
|
|
|
onOpenLogin: () => void;
|
|
|
|
|
|
onOpenReview: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type CaseTarget = "generation" | "canvas";
|
|
|
|
|
|
|
|
|
|
|
|
const RATIO_OPTIONS = ["16:9", "9:16", "1:1", "4:5", "3:4", "21:9"];
|
|
|
|
|
|
|
|
|
|
|
|
function readFileAsDataUrl(file: File): Promise<string> {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = () => resolve(String(reader.result || ""));
|
|
|
|
|
|
reader.onerror = () => reject(reader.error || new Error("图片读取失败"));
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function readFileAsText(file: File): Promise<string> {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = () => resolve(String(reader.result || ""));
|
|
|
|
|
|
reader.onerror = () => reject(reader.error || new Error("JSON 读取失败"));
|
|
|
|
|
|
reader.readAsText(file);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function textToDataUrl(text: string, mimeType: string): string {
|
|
|
|
|
|
const bytes = new TextEncoder().encode(text);
|
|
|
|
|
|
let binary = "";
|
|
|
|
|
|
bytes.forEach((byte) => {
|
|
|
|
|
|
binary += String.fromCharCode(byte);
|
|
|
|
|
|
});
|
|
|
|
|
|
return `data:${mimeType};base64,${btoa(binary)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseWorkflowText(text: string): WebCanvasWorkflow {
|
|
|
|
|
|
const parsed = JSON.parse(text) as unknown;
|
|
|
|
|
|
if (!isCanvasWorkflow(parsed)) {
|
|
|
|
|
|
throw new Error("JSON 需要包含完整画布工作流:nodes、edges 和 settings。");
|
|
|
|
|
|
}
|
|
|
|
|
|
return parsed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function shortDescription(value: string): string {
|
|
|
|
|
|
return value.trim().replace(/\s+/g, " ").slice(0, 120);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ratioToCssAspectRatio(value: string): string {
|
|
|
|
|
|
const [width, height] = value.split(":").map((item) => Number(item));
|
|
|
|
|
|
return width > 0 && height > 0 ? `${width} / ${height}` : "16 / 9";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function CommunityCaseAddPage({ session, onOpenLogin, onOpenReview }: CommunityCaseAddPageProps) {
|
|
|
|
|
|
const allowed = canManageCommunityCases(session);
|
|
|
|
|
|
const imageInputRef = useRef<HTMLInputElement | null>(null);
|
|
|
|
|
|
const workflowInputRef = useRef<HTMLInputElement | null>(null);
|
2026-06-08 21:30:48 +08:00
|
|
|
|
const [isImageDragging, setIsImageDragging] = useState(false);
|
|
|
|
|
|
const [isWorkflowDragging, setIsWorkflowDragging] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const handleImageDragOver = (e: DragEvent) => { e.preventDefault(); if (e.dataTransfer.types.includes("Files")) setIsImageDragging(true); };
|
|
|
|
|
|
const handleImageDragLeave = (e: DragEvent) => { e.preventDefault(); if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsImageDragging(false); };
|
|
|
|
|
|
const handleImageDrop = (e: DragEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
setIsImageDragging(false);
|
|
|
|
|
|
if (e.dataTransfer.files.length) {
|
|
|
|
|
|
void handleImageChange({ target: { files: e.dataTransfer.files } } as ChangeEvent<HTMLInputElement>);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleWorkflowDragOver = (e: DragEvent) => { e.preventDefault(); if (e.dataTransfer.types.includes("Files")) setIsWorkflowDragging(true); };
|
|
|
|
|
|
const handleWorkflowDragLeave = (e: DragEvent) => { e.preventDefault(); if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsWorkflowDragging(false); };
|
|
|
|
|
|
const handleWorkflowDrop = (e: DragEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
setIsWorkflowDragging(false);
|
|
|
|
|
|
if (e.dataTransfer.files.length) {
|
|
|
|
|
|
void handleWorkflowChange({ target: { files: e.dataTransfer.files } } as ChangeEvent<HTMLInputElement>);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-02 12:38:01 +08:00
|
|
|
|
const [target, setTarget] = useState<CaseTarget>("generation");
|
|
|
|
|
|
const [title, setTitle] = useState("");
|
|
|
|
|
|
const [description, setDescription] = useState("");
|
|
|
|
|
|
const [category, setCategory] = useState("图片案例");
|
|
|
|
|
|
const [ratio, setRatio] = useState("16:9");
|
|
|
|
|
|
const [prompt, setPrompt] = useState("");
|
|
|
|
|
|
const [imageUrl, setImageUrl] = useState("");
|
|
|
|
|
|
const [imageDataUrl, setImageDataUrl] = useState("");
|
|
|
|
|
|
const [imageFileName, setImageFileName] = useState("");
|
|
|
|
|
|
const [workflowText, setWorkflowText] = useState("");
|
|
|
|
|
|
const [workflowFileName, setWorkflowFileName] = useState("");
|
|
|
|
|
|
const [workflow, setWorkflow] = useState<WebCanvasWorkflow | null>(null);
|
|
|
|
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
|
|
const [notice, setNotice] = useState<string | null>(null);
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const previewUrl = imageDataUrl || imageUrl.trim();
|
|
|
|
|
|
const workflowCoverUrl = useMemo(() => getWorkflowCoverUrl(workflow), [workflow]);
|
|
|
|
|
|
const targetCopy = target === "generation" ? "生成页面社区" : "画布页面社区";
|
|
|
|
|
|
const previewAspectRatio = target === "generation" ? ratioToCssAspectRatio(ratio) : "16 / 10";
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setCategory((current) => {
|
|
|
|
|
|
if (target === "generation" && current === "工作流案例") return "图片案例";
|
|
|
|
|
|
if (target === "canvas" && current === "图片案例") return "工作流案例";
|
|
|
|
|
|
return current;
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [target]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleImageChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
|
const file = event.target.files?.[0];
|
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
setImageFileName(file.name);
|
|
|
|
|
|
setImageDataUrl(await readFileAsDataUrl(file));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const applyWorkflowText = (text: string, fileName = "") => {
|
|
|
|
|
|
setWorkflowText(text);
|
|
|
|
|
|
setWorkflowFileName(fileName);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsedWorkflow = parseWorkflowText(text);
|
|
|
|
|
|
setWorkflow(parsedWorkflow);
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
if (!title.trim()) setTitle(parsedWorkflow.title || "画布社区案例");
|
|
|
|
|
|
if (!description.trim()) setDescription(parsedWorkflow.description || "");
|
|
|
|
|
|
if (!category.trim()) setCategory("工作流案例");
|
|
|
|
|
|
} catch (parseError) {
|
|
|
|
|
|
setWorkflow(null);
|
|
|
|
|
|
setError(parseError instanceof Error ? parseError.message : "JSON 解析失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleWorkflowChange = async (event: ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
|
const file = event.target.files?.[0];
|
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
applyWorkflowText(await readFileAsText(file), file.name);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
|
if (submitting) return;
|
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
setNotice(null);
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const cleanTitle = title.trim();
|
|
|
|
|
|
if (!cleanTitle) throw new Error("请填写案例标题。");
|
|
|
|
|
|
|
|
|
|
|
|
if (target === "generation") {
|
|
|
|
|
|
if (!prompt.trim()) throw new Error("请填写生成页面展示的提示词。");
|
|
|
|
|
|
if (!previewUrl) throw new Error("请上传案例图片或填写图片 URL。");
|
|
|
|
|
|
|
|
|
|
|
|
let coverUrl = imageUrl.trim();
|
|
|
|
|
|
let ossKey: string | undefined;
|
|
|
|
|
|
if (imageDataUrl) {
|
|
|
|
|
|
const uploaded = await aiGenerationClient.uploadAsset({
|
|
|
|
|
|
dataUrl: imageDataUrl,
|
|
|
|
|
|
name: imageFileName || `${cleanTitle}.png`,
|
|
|
|
|
|
scope: "community-case-cover",
|
|
|
|
|
|
});
|
|
|
|
|
|
coverUrl = uploaded.url || imageDataUrl;
|
|
|
|
|
|
ossKey = uploaded.ossKey;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await communityClient.publishCase({
|
|
|
|
|
|
title: cleanTitle,
|
|
|
|
|
|
description: description.trim() || shortDescription(prompt),
|
|
|
|
|
|
coverUrl,
|
|
|
|
|
|
tags: [category.trim() || "图片案例", "生成页面社区"],
|
|
|
|
|
|
metadata: {
|
|
|
|
|
|
source: "admin-case-add",
|
|
|
|
|
|
communitySurface: "generation",
|
|
|
|
|
|
prompt: prompt.trim(),
|
|
|
|
|
|
ratio,
|
|
|
|
|
|
category: category.trim() || "图片案例",
|
|
|
|
|
|
},
|
|
|
|
|
|
assets: [
|
|
|
|
|
|
{
|
|
|
|
|
|
assetType: "cover",
|
|
|
|
|
|
title: cleanTitle,
|
|
|
|
|
|
url: coverUrl,
|
|
|
|
|
|
ossKey,
|
|
|
|
|
|
metadata: { prompt: prompt.trim(), ratio },
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (!workflow) throw new Error("请上传或粘贴有效的画布工作流 JSON。");
|
|
|
|
|
|
const workflowJson = JSON.stringify(workflow);
|
|
|
|
|
|
const uploadedWorkflow = await aiGenerationClient.uploadAsset({
|
|
|
|
|
|
dataUrl: textToDataUrl(workflowJson, "application/json"),
|
|
|
|
|
|
name: workflowFileName || `${cleanTitle}.json`,
|
|
|
|
|
|
mimeType: "application/json",
|
|
|
|
|
|
scope: "community-case-workflow",
|
|
|
|
|
|
});
|
|
|
|
|
|
const coverUrl = workflowCoverUrl || imageUrl.trim();
|
|
|
|
|
|
await communityClient.publishCase({
|
|
|
|
|
|
title: cleanTitle,
|
|
|
|
|
|
description: description.trim() || workflow.description || "管理员添加的画布工作流案例。",
|
|
|
|
|
|
coverUrl,
|
|
|
|
|
|
tags: [category.trim() || "工作流案例", "画布页面社区"],
|
|
|
|
|
|
metadata: {
|
|
|
|
|
|
source: "admin-case-add",
|
|
|
|
|
|
communitySurface: "canvas",
|
|
|
|
|
|
category: category.trim() || "工作流案例",
|
|
|
|
|
|
workflow,
|
|
|
|
|
|
workflowOssKey: uploadedWorkflow.ossKey || null,
|
|
|
|
|
|
workflowUrl: uploadedWorkflow.url || null,
|
|
|
|
|
|
},
|
|
|
|
|
|
assets: [
|
|
|
|
|
|
{
|
|
|
|
|
|
assetType: "workflow",
|
|
|
|
|
|
title: cleanTitle,
|
|
|
|
|
|
url: uploadedWorkflow.url,
|
|
|
|
|
|
ossKey: uploadedWorkflow.ossKey,
|
|
|
|
|
|
metadata: { workflow, fileName: workflowFileName || null, contentType: "application/json" },
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setNotice(`已提交到${targetCopy}审核列表,审核通过后会展示到对应社区。`);
|
|
|
|
|
|
} catch (submitError) {
|
|
|
|
|
|
setError(submitError instanceof Error ? submitError.message : "案例提交失败");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!session) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<WorkspacePageShell title="添加案例" fullWidth className="community-review-page page-motion">
|
|
|
|
|
|
<section className="community-review-access">
|
|
|
|
|
|
<LoginOutlined />
|
|
|
|
|
|
<h1>请登录工作人员账号</h1>
|
|
|
|
|
|
<p>添加社区案例需要登录后访问。</p>
|
|
|
|
|
|
<button type="button" onClick={onOpenLogin}>登录 / 注册</button>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</WorkspacePageShell>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!allowed) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<WorkspacePageShell title="添加案例" fullWidth className="community-review-page page-motion">
|
|
|
|
|
|
<section className="community-review-access">
|
|
|
|
|
|
<FileTextOutlined />
|
|
|
|
|
|
<h1>当前账号没有管理权限</h1>
|
|
|
|
|
|
<p>请切换到 admin 角色账号后再添加案例。</p>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</WorkspacePageShell>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<WorkspacePageShell title="添加案例" fullWidth className="community-review-page page-motion">
|
|
|
|
|
|
<div className="community-review-page__inner">
|
|
|
|
|
|
<section className="community-review-toolbar">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<span>内部案例录入</span>
|
|
|
|
|
|
<h1>添加社区案例</h1>
|
|
|
|
|
|
<p>管理员录入的案例会先进入社区审核,审核通过后才展示给用户。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="community-review-toolbar__actions">
|
|
|
|
|
|
<button type="button" onClick={onOpenReview}>
|
|
|
|
|
|
<ArrowLeftOutlined />
|
|
|
|
|
|
返回审核
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="community-review-tabs community-case-add-targets" role="tablist" aria-label="案例展示位置">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
role="tab"
|
|
|
|
|
|
aria-selected={target === "generation"}
|
|
|
|
|
|
className={target === "generation" ? "is-active" : ""}
|
|
|
|
|
|
onClick={() => setTarget("generation")}
|
|
|
|
|
|
>
|
|
|
|
|
|
<PictureOutlined />
|
|
|
|
|
|
生成页面社区
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
role="tab"
|
|
|
|
|
|
aria-selected={target === "canvas"}
|
|
|
|
|
|
className={target === "canvas" ? "is-active" : ""}
|
|
|
|
|
|
onClick={() => setTarget("canvas")}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FileTextOutlined />
|
|
|
|
|
|
画布页面社区
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{error ? <p className="community-review-error">{error}</p> : null}
|
|
|
|
|
|
{notice ? (
|
|
|
|
|
|
<p className="community-case-add-success">
|
|
|
|
|
|
<CheckCircleOutlined />
|
|
|
|
|
|
{notice}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
<section className="community-case-add-layout">
|
|
|
|
|
|
<form className="community-case-add-form" onSubmit={(event) => event.preventDefault()}>
|
|
|
|
|
|
<div className="community-case-add-form__grid">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<span>案例标题</span>
|
|
|
|
|
|
<input value={title} onChange={(event) => setTitle(event.target.value)} placeholder="例如:横版商品主图" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<span>分类标签</span>
|
|
|
|
|
|
<input value={category} onChange={(event) => setCategory(event.target.value)} placeholder="图片案例 / 工作流案例" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
{target === "generation" ? (
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<span>展示比例</span>
|
|
|
|
|
|
<select value={ratio} onChange={(event) => setRatio(event.target.value)}>
|
|
|
|
|
|
{RATIO_OPTIONS.map((option) => (
|
|
|
|
|
|
<option key={option} value={option}>{option}</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<span>案例简介</span>
|
|
|
|
|
|
<textarea value={description} onChange={(event) => setDescription(event.target.value)} placeholder="审核页和社区卡片使用的简短说明" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
{target === "generation" ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<span>提示词</span>
|
|
|
|
|
|
<textarea value={prompt} onChange={(event) => setPrompt(event.target.value)} placeholder="输入用户点击案例后可套用的提示词" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div className="community-case-add-upload-row">
|
|
|
|
|
|
<input ref={imageInputRef} type="file" accept="image/*" hidden onChange={handleImageChange} />
|
2026-06-08 21:30:48 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className={isImageDragging ? "is-dragging" : ""}
|
|
|
|
|
|
onClick={() => imageInputRef.current?.click()}
|
|
|
|
|
|
onDragOver={handleImageDragOver}
|
|
|
|
|
|
onDragLeave={handleImageDragLeave}
|
|
|
|
|
|
onDrop={handleImageDrop}
|
|
|
|
|
|
>
|
2026-06-02 12:38:01 +08:00
|
|
|
|
<UploadOutlined />
|
|
|
|
|
|
上传图片
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<span>或填写图片 URL</span>
|
|
|
|
|
|
<input value={imageUrl} onChange={(event) => setImageUrl(event.target.value)} placeholder="https://..." />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="community-case-add-upload-row">
|
|
|
|
|
|
<input ref={workflowInputRef} type="file" accept="application/json,.json" hidden onChange={handleWorkflowChange} />
|
2026-06-08 21:30:48 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className={isWorkflowDragging ? "is-dragging" : ""}
|
|
|
|
|
|
onClick={() => workflowInputRef.current?.click()}
|
|
|
|
|
|
onDragOver={handleWorkflowDragOver}
|
|
|
|
|
|
onDragLeave={handleWorkflowDragLeave}
|
|
|
|
|
|
onDrop={handleWorkflowDrop}
|
|
|
|
|
|
>
|
2026-06-02 12:38:01 +08:00
|
|
|
|
<UploadOutlined />
|
|
|
|
|
|
上传 JSON
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<span>可选封面 URL</span>
|
|
|
|
|
|
<input value={imageUrl} onChange={(event) => setImageUrl(event.target.value)} placeholder="未填写时从节点预览图提取" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<span>画布工作流 JSON</span>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
value={workflowText}
|
|
|
|
|
|
onChange={(event) => applyWorkflowText(event.target.value, workflowFileName)}
|
|
|
|
|
|
placeholder='{"id":"...","version":1,"title":"...","nodes":[...],"edges":[...],"settings":{...}}'
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="community-case-add-actions">
|
|
|
|
|
|
<button type="button" onClick={() => void handleSubmit()} disabled={submitting}>
|
|
|
|
|
|
<CheckCircleOutlined />
|
|
|
|
|
|
{submitting ? "提交中..." : "提交审核"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" onClick={onOpenReview}>
|
|
|
|
|
|
查看审核列表
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
|
|
<aside className="community-case-add-preview">
|
|
|
|
|
|
<span>{targetCopy}</span>
|
|
|
|
|
|
<strong>{title.trim() || "未命名案例"}</strong>
|
|
|
|
|
|
<p>{description.trim() || (target === "generation" ? shortDescription(prompt) : workflow?.description) || "提交前可在这里预览案例信息。"}</p>
|
|
|
|
|
|
{target === "generation" ? (
|
|
|
|
|
|
previewUrl ? (
|
|
|
|
|
|
<img src={previewUrl} alt="" style={{ aspectRatio: previewAspectRatio }} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="community-case-add-preview__empty" style={{ aspectRatio: previewAspectRatio }}>等待图片</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
) : workflowCoverUrl || imageUrl.trim() ? (
|
|
|
|
|
|
<img src={workflowCoverUrl || imageUrl.trim()} alt="" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="community-case-add-preview__empty">等待工作流封面</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<dl>
|
|
|
|
|
|
<dt>目标</dt>
|
|
|
|
|
|
<dd>{targetCopy}</dd>
|
|
|
|
|
|
<dt>{target === "generation" ? "比例" : "节点"}</dt>
|
|
|
|
|
|
<dd>{target === "generation" ? ratio : workflow ? workflow.nodes.length : 0}</dd>
|
|
|
|
|
|
<dt>文件</dt>
|
|
|
|
|
|
<dd>{target === "generation" ? imageFileName || "未上传" : workflowFileName || "未上传"}</dd>
|
|
|
|
|
|
</dl>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</WorkspacePageShell>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|