From 37b7e58f2a9cf669aadf28711ed51fc74c9df691 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 19 Apr 2026 08:52:38 +0800 Subject: [PATCH] Refine video canvas function replica --- app/(admin)/settings/video-canvas/page.tsx | 15 + .../video-canvas-settings-client.tsx | 242 +++++++ app/api/admin/settings/route.ts | 8 + app/api/video-canvas/config/route.ts | 17 + .../canvas/LiblibVideoCanvasReplica.tsx | 675 ++++++++++++++++-- docs/video-canvas-official-function-map.md | 68 ++ lib/video-canvas/config.ts | 313 ++++++++ 7 files changed, 1280 insertions(+), 58 deletions(-) create mode 100644 app/(admin)/settings/video-canvas/page.tsx create mode 100644 app/(admin)/settings/video-canvas/video-canvas-settings-client.tsx create mode 100644 app/api/video-canvas/config/route.ts create mode 100644 docs/video-canvas-official-function-map.md create mode 100644 lib/video-canvas/config.ts diff --git a/app/(admin)/settings/video-canvas/page.tsx b/app/(admin)/settings/video-canvas/page.tsx new file mode 100644 index 000000000..d65dc7b5b --- /dev/null +++ b/app/(admin)/settings/video-canvas/page.tsx @@ -0,0 +1,15 @@ +import { redirect } from "next/navigation"; +import { requireAdmin } from "@/lib/middleware/admin"; +import { VideoCanvasSettingsClient } from "./video-canvas-settings-client"; + +export const dynamic = "force-dynamic"; + +export default async function VideoCanvasSettingsPage() { + try { + await requireAdmin(); + } catch { + redirect("/"); + } + + return ; +} diff --git a/app/(admin)/settings/video-canvas/video-canvas-settings-client.tsx b/app/(admin)/settings/video-canvas/video-canvas-settings-client.tsx new file mode 100644 index 000000000..f4282fe52 --- /dev/null +++ b/app/(admin)/settings/video-canvas/video-canvas-settings-client.tsx @@ -0,0 +1,242 @@ +"use client"; + +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 { Switch } from "@/components/ui/switch"; +import { Textarea } from "@/components/ui/textarea"; +import { fetcher } from "@/lib/utils"; +import { + type VideoCanvasFunctionConfig, + type VideoCanvasFunctionDefinition, + VIDEO_CANVAS_FUNCTION_CONFIG_KEY, + defaultVideoCanvasFunctionConfig, + normalizeVideoCanvasFunctionConfig, +} from "@/lib/video-canvas/config"; + +type SettingsResponse = { + setting?: { + value?: unknown; + }; +}; + +const functionKindLabels: Record = { + audio: "音频", + image: "图片生成", + "image-edit": "图片编辑", + multimodal: "多模态", + utility: "工具", + video: "视频生成", + workflow: "工作流", +}; + +export function VideoCanvasSettingsClient() { + const { data, isLoading, mutate } = useSWR( + `/api/admin/settings?key=${VIDEO_CANVAS_FUNCTION_CONFIG_KEY}`, + fetcher, + ); + const [config, setConfig] = useState(defaultVideoCanvasFunctionConfig); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!data) return; + setConfig(normalizeVideoCanvasFunctionConfig(data.setting?.value)); + }, [data]); + + const functions = useMemo( + () => + Object.values(config.functions).sort((a, b) => { + const kindCompare = a.kind.localeCompare(b.kind); + return kindCompare || a.label.localeCompare(b.label); + }), + [config.functions], + ); + + const updateRoot = (updates: Partial) => { + setConfig((current) => ({ ...current, ...updates })); + }; + + const updateFunction = ( + key: string, + updates: Partial, + ) => { + setConfig((current) => ({ + ...current, + functions: { + ...current.functions, + [key]: { + ...current.functions[key], + ...updates, + }, + }, + })); + }; + + const save = async () => { + setSaving(true); + try { + const response = await fetch("/api/admin/settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + key: VIDEO_CANVAS_FUNCTION_CONFIG_KEY, + value: config, + }), + }); + const payload = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error(payload?.message || payload?.error || "保存失败"); + } + toast.success("视频画布功能配置已保存"); + await mutate(); + } catch (error) { + toast.error(error instanceof Error ? error.message : "保存失败"); + } finally { + setSaving(false); + } + }; + + const resetToDefault = () => { + setConfig(defaultVideoCanvasFunctionConfig); + toast.info("已恢复默认配置,保存后生效"); + }; + + return ( +
+
+
+

视频画布功能设置

+

+ 这里集中配置复刻画布里的工具能力、预设提示词、默认模型和扣点。前端优先按复刻目标触发,后端缺口可以先留空或后续替换。 +

+
+
+ + +
+
+ + + +
+ 全局模型与开关 + + 默认模型可填写现有后端的模型标识;保持 auto 时由各接口按当前可用模型选择。 + +
+
+ + updateRoot({ enabled: checked })} + /> +
+
+ +
+ + updateRoot({ defaultImageModel: event.target.value })} + value={config.defaultImageModel} + /> +
+
+ + updateRoot({ defaultVideoModel: event.target.value })} + value={config.defaultVideoModel} + /> +
+
+ + updateRoot({ defaultMultimodalModel: event.target.value })} + value={config.defaultMultimodalModel} + /> +
+
+
+ +
+ {functions.map((item) => ( + + +
+
+ + {item.label} + + {functionKindLabels[item.kind]} + + + {item.route} + + + {item.description} +
+
+ + updateFunction(item.key, { enabled: checked })} + /> +
+
+
+
+ + updateFunction(item.key, { model: event.target.value })} + placeholder="留空使用全局默认模型" + value={item.model || ""} + /> +
+
+ + + updateFunction(item.key, { creditCost: Number(event.target.value || 0) }) + } + type="number" + value={item.creditCost ?? 0} + /> +
+
+
+ + +