From 5387d6f1d2e7b8b1c300be5e276e5faa29215bb5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 16 May 2026 15:06:12 +0800 Subject: [PATCH] feat: add style pro canvas workspace --- .../settings/admin-settings-client.tsx | 6 + app/(admin)/settings/style-pro/page.tsx | 16 + .../style-pro/style-pro-settings-client.tsx | 651 +++++++++++++ app/api/admin/settings/route.ts | 14 + app/api/agent-workflow/detail-stitch/route.ts | 42 +- .../agent-workflow/reference-search/route.ts | 1 + app/api/realtime/generate/route.ts | 150 ++- app/api/style-pro/config/route.ts | 30 + app/api/style-pro/generate/route.ts | 238 +++++ .../[id]/CanvasEditorLegacyPageClient.tsx | 36 + app/canvas/[id]/style-pro/page.tsx | 128 +++ .../AgentDetailReferenceStitchDialog.tsx | 252 ++++- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 68 +- .../agents/AgentPsdTemplateEditorDialog.tsx | 35 +- .../agents/AgentWorkflowPanelDarkV2.tsx | 64 +- components/canvas/EnhanceProWorkspace.tsx | 24 +- components/canvas/RealtimeCanvasDialog.tsx | 149 ++- components/style-pro/StyleProWorkspace.tsx | 886 ++++++++++++++++++ docs/更新日志-2026-05-07至2026-05-12.md | 73 ++ docs/更新日志-2026-05-10至2026-05-14.md | 237 +++++ lib/agents/mode-workspace-state.ts | 6 + lib/canvas-toolbar-config.ts | 6 + lib/style-pro/config.ts | 417 +++++++++ store/canvas/index.ts | 16 + types/agent.ts | 3 + types/canvas/index.ts | 2 + 26 files changed, 3451 insertions(+), 99 deletions(-) create mode 100644 app/(admin)/settings/style-pro/page.tsx create mode 100644 app/(admin)/settings/style-pro/style-pro-settings-client.tsx create mode 100644 app/api/style-pro/config/route.ts create mode 100644 app/api/style-pro/generate/route.ts create mode 100644 app/canvas/[id]/style-pro/page.tsx create mode 100644 components/style-pro/StyleProWorkspace.tsx create mode 100644 docs/更新日志-2026-05-07至2026-05-12.md create mode 100644 docs/更新日志-2026-05-10至2026-05-14.md create mode 100644 lib/style-pro/config.ts diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 614aaa515..d48c2d3a5 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -928,6 +928,7 @@ const CANVAS_TOOLBAR_FALLBACK_ICONS: Record = markerPanel: Tag, workflowConnection: GitBranch, styleMode: Wand2, + stylePro: Sparkles, batchProcessing: Layers, addImage: ImagePlus, addImageInput: ImagePlus, @@ -11356,6 +11357,11 @@ export function AdminSettingsClient() { 管理PSD模板 + diff --git a/app/(admin)/settings/style-pro/page.tsx b/app/(admin)/settings/style-pro/page.tsx new file mode 100644 index 000000000..8d954c355 --- /dev/null +++ b/app/(admin)/settings/style-pro/page.tsx @@ -0,0 +1,16 @@ +import { redirect } from "next/navigation"; +import { requireAdmin } from "@/lib/middleware/admin"; +import { StyleProSettingsClient } from "./style-pro-settings-client"; + +export const dynamic = "force-dynamic"; + +export default async function StyleProSettingsPage() { + try { + await requireAdmin(); + } catch { + redirect("/"); + } + + return ; +} + diff --git a/app/(admin)/settings/style-pro/style-pro-settings-client.tsx b/app/(admin)/settings/style-pro/style-pro-settings-client.tsx new file mode 100644 index 000000000..2ed6dda01 --- /dev/null +++ b/app/(admin)/settings/style-pro/style-pro-settings-client.tsx @@ -0,0 +1,651 @@ +"use client"; + +import Link from "next/link"; +import { ArrowLeft, Plus, Save, Trash2, Wand2 } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import useSWR from "swr"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { Textarea } from "@/components/ui/textarea"; +import { fetcher } from "@/lib/utils"; +import { + STYLE_PRO_CONFIG_KEY, + defaultStyleProConfig, + normalizeStyleProConfig, + type StyleProCategory, + type StyleProConfig, + type StyleProOption, +} from "@/lib/style-pro/config"; + +type SettingsResponse = { + setting?: { + value?: unknown; + }; +}; + +type AiModelOption = { + id: string; + modelId: string; + displayName: string; + supportsImageGen?: boolean; + isEnabled?: boolean; + isHidden?: boolean; +}; + +type AiModelsResponse = { + models?: AiModelOption[]; +}; + +const AUTO_MODEL_VALUE = "__auto__"; + +function getId(prefix: string) { + if (typeof crypto !== "undefined" && "randomUUID" in crypto) { + return `${prefix}-${crypto.randomUUID().slice(0, 8)}`; + } + return `${prefix}-${Date.now().toString(36)}`; +} + +function reindexCategories(categories: StyleProCategory[]) { + return categories.map((category, index) => ({ + ...category, + order: (index + 1) * 10, + options: category.options.map((option, optionIndex) => ({ + ...option, + order: (optionIndex + 1) * 10, + })), + })); +} + +function createCategory(): StyleProCategory { + const id = getId("category"); + return { + id, + title: "新分类", + description: "", + required: true, + minSelections: 1, + maxSelections: 1, + enabled: true, + order: 999, + options: [createOption(id)], + }; +} + +function createOption(categoryId: string): StyleProOption { + return { + id: getId("option"), + label: "新按钮", + description: "", + prompt: "请填写这个按钮对应的隐藏提示词。", + jsonKey: categoryId, + enabled: true, + order: 999, + }; +} + +function modelLabel(model: AiModelOption) { + const name = model.displayName || model.modelId || model.id; + const id = model.modelId && model.modelId !== name ? ` · ${model.modelId}` : ""; + return `${name}${id}`; +} + +export function StyleProSettingsClient() { + const { data, mutate } = useSWR( + `/api/admin/settings?key=${STYLE_PRO_CONFIG_KEY}`, + fetcher + ); + const { data: modelsData } = useSWR( + "/api/admin/ai-models", + fetcher + ); + const [config, setConfig] = useState(defaultStyleProConfig); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!data) return; + setConfig(normalizeStyleProConfig(data.setting?.value)); + }, [data]); + + const imageModels = useMemo( + () => + (modelsData?.models || []).filter( + (model) => + model.isEnabled !== false && + model.isHidden !== true && + model.supportsImageGen + ), + [modelsData?.models] + ); + + const updateConfig = (updates: Partial) => { + setConfig((current) => ({ ...current, ...updates })); + }; + + const updateCategory = ( + categoryId: string, + updates: Partial + ) => { + setConfig((current) => ({ + ...current, + categories: current.categories.map((category) => + category.id === categoryId ? { ...category, ...updates } : category + ), + })); + }; + + const updateOption = ( + categoryId: string, + optionId: string, + updates: Partial + ) => { + setConfig((current) => ({ + ...current, + categories: current.categories.map((category) => + category.id === categoryId + ? { + ...category, + options: category.options.map((option) => + option.id === optionId ? { ...option, ...updates } : option + ), + } + : category + ), + })); + }; + + const addCategory = () => { + setConfig((current) => ({ + ...current, + categories: reindexCategories([...current.categories, createCategory()]), + })); + }; + + const removeCategory = (categoryId: string) => { + setConfig((current) => ({ + ...current, + categories: reindexCategories( + current.categories.filter((category) => category.id !== categoryId) + ), + })); + }; + + const moveCategory = (categoryId: string, direction: -1 | 1) => { + setConfig((current) => { + const categories = [...current.categories]; + const index = categories.findIndex((category) => category.id === categoryId); + const nextIndex = index + direction; + if (index < 0 || nextIndex < 0 || nextIndex >= categories.length) { + return current; + } + const [category] = categories.splice(index, 1); + categories.splice(nextIndex, 0, category); + return { ...current, categories: reindexCategories(categories) }; + }); + }; + + const addOption = (categoryId: string) => { + setConfig((current) => ({ + ...current, + categories: current.categories.map((category) => + category.id === categoryId + ? { + ...category, + options: [ + ...category.options, + { + ...createOption(category.id), + order: (category.options.length + 1) * 10, + }, + ], + } + : category + ), + })); + }; + + const removeOption = (categoryId: string, optionId: string) => { + setConfig((current) => ({ + ...current, + categories: current.categories.map((category) => + category.id === categoryId + ? { + ...category, + options: category.options.filter((option) => option.id !== optionId), + } + : category + ), + })); + }; + + const save = async () => { + setSaving(true); + try { + const normalized = normalizeStyleProConfig(config); + const response = await fetch("/api/admin/settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + key: STYLE_PRO_CONFIG_KEY, + value: normalized, + }), + }); + const payload = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error(payload?.message || payload?.error || "保存失败"); + } + setConfig(normalized); + await mutate(); + toast.success("风格化 PRO 配置已保存"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "保存失败"); + } finally { + setSaving(false); + } + }; + + return ( +
+
+
+
+ +
+

风格化 PRO 设置

+

+ 配置无限画布里的风格化 PRO 子页面。用户只看到分类和按钮名称,隐藏提示词和模型只在服务端生成时使用。 +

+
+
+ + +
+
+ + + + 基础设置 + + 控制功能开关、最终生图模型、默认提示词和生成规格。 + + + +
+
+ +

+ 关闭后前台页面会提示管理员暂未启用,生成接口也会拒绝执行。 +

+
+ updateConfig({ enabled })} + /> +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ +