diff --git a/app/(admin)/settings/video-canvas/video-canvas-settings-client.tsx b/app/(admin)/settings/video-canvas/video-canvas-settings-client.tsx index e90fc5b98..c79fba2c7 100644 --- a/app/(admin)/settings/video-canvas/video-canvas-settings-client.tsx +++ b/app/(admin)/settings/video-canvas/video-canvas-settings-client.tsx @@ -1,5 +1,7 @@ "use client"; +import Link from "next/link"; +import { ArrowLeft, Plus, Trash2 } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import useSWR from "swr"; import { toast } from "sonner"; @@ -12,6 +14,7 @@ import { CardTitle, } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; import { Select, SelectContent, @@ -30,6 +33,9 @@ import { getVideoCanvasFunctionBillingCopy, type VideoCanvasFunctionConfig, type VideoCanvasFunctionDefinition, + type VideoCanvasEffectPreset, + type VideoCanvasMotionPreset, + type VideoCanvasRolePreset, VIDEO_CANVAS_FUNCTION_CONFIG_KEY, defaultVideoCanvasFunctionConfig, normalizeVideoCanvasFunctionConfig, @@ -199,8 +205,9 @@ export function VideoCanvasSettingsClient() { const functions = useMemo( () => Object.values(config.functions).sort((a, b) => { - const kindCompare = a.kind.localeCompare(b.kind); - return kindCompare || a.label.localeCompare(b.label); + const compare = (left: string, right: string) => + left === right ? 0 : left < right ? -1 : 1; + return compare(a.kind, b.kind) || compare(a.label, b.label); }), [config.functions], ); @@ -225,6 +232,135 @@ export function VideoCanvasSettingsClient() { })); }; + const updateEffectPreset = ( + index: number, + updates: Partial, + ) => { + setConfig((current) => ({ + ...current, + effectPresets: current.effectPresets.map((preset, presetIndex) => + presetIndex === index ? { ...preset, ...updates } : preset, + ), + })); + }; + + const updateMotionPreset = ( + index: number, + updates: Partial, + ) => { + setConfig((current) => ({ + ...current, + motionPresets: current.motionPresets.map((preset, presetIndex) => + presetIndex === index ? { ...preset, ...updates } : preset, + ), + })); + }; + + const addMotionPreset = () => { + const createdAt = Date.now(); + setConfig((current) => ({ + ...current, + motionPresets: [ + ...current.motionPresets, + { + id: `motion-${createdAt}`, + name: "新运镜预设", + previewUrl: "/video-canvas/effects/multi-angle.webp", + prompt: "镜头平稳移动,保持主体和空间关系清晰。", + }, + ], + })); + }; + + const removeMotionPreset = (index: number) => { + setConfig((current) => ({ + ...current, + motionPresets: current.motionPresets.filter( + (_, presetIndex) => presetIndex !== index, + ), + })); + }; + + const addEffectPreset = () => { + setConfig((current) => ({ + ...current, + effectPresets: [ + ...current.effectPresets, + { + id: `effect-${Date.now()}`, + title: "新视频特效", + author: "官方预设", + usageCount: 0, + model: "Seedance 2.0", + thumbnailUrl: "/video-canvas/effects/product-light-sweep.webp", + prompt: "保持参考主体稳定,应用自然、连续的视频特效和镜头运动。", + commercial: true, + }, + ], + })); + }; + + const removeEffectPreset = (index: number) => { + setConfig((current) => ({ + ...current, + effectPresets: current.effectPresets.filter((_, presetIndex) => presetIndex !== index), + })); + }; + + const updateRolePreset = ( + index: number, + updates: Partial, + ) => { + setConfig((current) => ({ + ...current, + rolePresets: current.rolePresets.map((preset, presetIndex) => + presetIndex === index ? { ...preset, ...updates } : preset, + ), + })); + }; + + const addRolePreset = () => { + const createdAt = Date.now(); + setConfig((current) => ({ + ...current, + rolePresets: [ + ...current.rolePresets, + { + id: `role-${createdAt}`, + assetId: `public-role:role-${createdAt}`, + name: "新角色预设", + accent: "#525252", + description: "管理员配置的公共角色素材。", + prompt: "保持角色身份、五官、发型、服装和体型在所有视频镜头中一致。", + tags: "角色", + compliant: true, + images: { + expression: "", + face: "", + full: "/video-canvas/effects/product-light-sweep.webp", + threeView: "", + }, + filters: { + age: "青年", + build: "匀称", + culture: "东亚", + era: "现代", + gender: "中性", + hairColor: "黑发", + species: "人类", + }, + }, + ], + })); + }; + + const removeRolePreset = (index: number) => { + setConfig((current) => ({ + ...current, + rolePresets: current.rolePresets.filter((_, presetIndex) => presetIndex !== index), + })); + }; + const save = async () => { setSaving(true); try { @@ -255,11 +391,17 @@ export function VideoCanvasSettingsClient() { }; return ( -
-
+
+
-

视频画布功能设置

-

+ +

视频画布功能设置

+

这里集中配置复刻画布里的工具能力、预设提示词和默认模型。积分与扣费统一跟随全站模型/功能配置,不再在视频画布里单独维护一套价格。

@@ -321,6 +463,354 @@ export function VideoCanvasSettingsClient() { +
+
+
+

视频运镜素材预设

+

+ 视频节点“运镜广场”的名称、预览图和插入提示词都从这里读取。点击运镜卡会按竞品规则把提示词追加到用户输入框,允许组合和重复使用。 +

+
+ +
+ +
+ {config.motionPresets.map((preset, index) => ( +
+ + +
+
{preset.name}
+
+ {preset.prompt || "未设置插入提示词"} +
+
+ +
+ +
+
+ + + updateMotionPreset(index, { name: event.target.value }) + } + value={preset.name} + /> +
+
+ + + updateMotionPreset(index, { previewUrl: event.target.value }) + } + value={preset.previewUrl} + /> +
+
+ +