import { ArrowLeftOutlined, CheckCircleOutlined, FileTextOutlined, LoginOutlined, PictureOutlined, UploadOutlined, } from "@ant-design/icons"; import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react"; import { aiGenerationClient } from "../../api/aiGenerationClient"; import { communityClient } from "../../api/communityClient"; import WorkspacePageShell from "../../components/WorkspacePageShell"; import "../../styles/pages/compliance.css"; 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 { 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 { 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(null); const workflowInputRef = useRef(null); const [target, setTarget] = useState("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(null); const [submitting, setSubmitting] = useState(false); const [notice, setNotice] = useState(null); const [error, setError] = useState(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) => { 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) => { 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 (

请登录工作人员账号

添加社区案例需要登录后访问。

); } if (!allowed) { return (

当前账号没有管理权限

请切换到 admin 角色账号后再添加案例。

); } return (
内部案例录入

添加社区案例

管理员录入的案例会先进入社区审核,审核通过后才展示给用户。

{error ?

{error}

: null} {notice ? (

{notice}

) : null}
event.preventDefault()}>
{target === "generation" ? ( ) : null}