feat(canvas): 添加视频模型配置和生成功能

- 在系统配置中新增 defaultVideoModel 字段用于设置默认视频模型
- 更新应用配置模态框以支持视频模型的选择和验证
- 修改画布节点逻辑以支持视频类型的生成配置
- 实现视频节点的生成和上传功能
- 更新模型选择器组件以支持视频模型选择
- 在管理后台设置页面添加视频模型配置选项
- 优化模型选择器的层级和交互体验
- 添加对 DeepSeek 和 GLM 模型图标的支持
This commit is contained in:
HouYunFei
2026-05-23 17:25:00 +08:00
parent 44c5825cb0
commit 370263cd83
14 changed files with 50 additions and 14 deletions
@@ -26,7 +26,7 @@ export function AppConfigModal() {
const finishConfig = () => {
setConfigDialogOpen(false);
if (effectiveMode === "local" && (!config.baseUrl.trim() || !config.apiKey.trim())) return;
if (!modelConfig.imageModel.trim() || !modelConfig.textModel.trim()) return;
if (!modelConfig.imageModel.trim() || !modelConfig.videoModel.trim() || !modelConfig.textModel.trim()) return;
if (!allowCustomChannel && config.channelMode !== "remote") updateConfig("channelMode", "remote");
message.success(shouldPromptContinue ? "配置已保存,请继续刚才的请求" : "配置已保存");
clearPromptContinue();
@@ -43,6 +43,7 @@ export function AppConfigModal() {
const models = await fetchImageModels(config);
updateConfig("models", models);
if (models.length && !models.includes(config.imageModel)) updateConfig("imageModel", models[0]);
if (models.length && !models.includes(config.videoModel)) updateConfig("videoModel", models[0]);
if (models.length && !models.includes(config.textModel)) updateConfig("textModel", models[0]);
message.success("模型列表已更新");
} catch (error) {
@@ -112,10 +113,13 @@ export function AppConfigModal() {
<div className="mt-1"> {modelChannel?.availableModels.length || 0} </div>
</div>
)}
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-4 md:grid-cols-3">
<Form.Item label="默认生图模型" className="mb-4">
<ModelPicker config={modelConfig} value={modelConfig.imageModel} onChange={(model) => updateConfig("imageModel", model)} fullWidth />
</Form.Item>
<Form.Item label="默认视频模型" className="mb-4">
<ModelPicker config={modelConfig} value={modelConfig.videoModel} onChange={(model) => updateConfig("videoModel", model)} fullWidth />
</Form.Item>
<Form.Item label="默认文本模型" className="mb-4">
<ModelPicker config={modelConfig} value={modelConfig.textModel} onChange={(model) => updateConfig("textModel", model)} fullWidth />
</Form.Item>
+16 -2
View File
@@ -1,6 +1,6 @@
"use client";
import { useMemo, useState } from "react";
import { useEffect, useId, useMemo, useState } from "react";
import { Cpu } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
@@ -18,10 +18,19 @@ type ModelPickerProps = {
};
export function ModelPicker({ config, value, onChange, className, fullWidth = false, placeholder = "选择模型", onMissingConfig }: ModelPickerProps) {
const pickerId = useId();
const [open, setOpen] = useState(false);
const options = useMemo(() => Array.from(new Set([value, ...config.models].filter(Boolean))), [config.models, value]);
const current = value || "";
useEffect(() => {
const closeOtherPicker = (event: Event) => {
if ((event as CustomEvent<string>).detail !== pickerId) setOpen(false);
};
window.addEventListener("model-picker-open", closeOtherPicker);
return () => window.removeEventListener("model-picker-open", closeOtherPicker);
}, [pickerId]);
return (
<Select
open={open}
@@ -31,6 +40,7 @@ export function ModelPicker({ config, value, onChange, className, fullWidth = fa
onMissingConfig?.();
return;
}
if (nextOpen) window.dispatchEvent(new CustomEvent("model-picker-open", { detail: pickerId }));
setOpen(nextOpen);
}}
onValueChange={onChange}
@@ -51,9 +61,11 @@ export function ModelPicker({ config, value, onChange, className, fullWidth = fa
</SelectTrigger>
<SelectContent
data-canvas-no-zoom
className="z-50 w-80 max-w-[calc(100vw-24px)] rounded-xl border border-border/70 bg-popover p-1 shadow-xl"
className="z-[1200] w-80 max-w-[calc(100vw-24px)] rounded-xl border border-border/70 bg-popover p-1 shadow-xl"
position="popper"
align="start"
side="bottom"
sideOffset={6}
onPointerDown={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
>
@@ -93,5 +105,7 @@ function resolveModelIcon(model: string) {
if (name.includes("gemini") || name.includes("google")) return "/icons/gemini.svg";
if (name.includes("gpt") || name.includes("openai")) return "/icons/openai.svg";
if (name.includes("grok") || name.includes("grok")) return "/icons/grok.svg";
if (name.includes("deepseek") || name.includes("deepseek")) return "/icons/deepseek.svg";
if (name.includes("glm") || name.includes("glm")) return "/icons/glm.svg";
return "";
}
+1 -1
View File
@@ -73,7 +73,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
data-align-trigger={position === "item-aligned"}
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
className={cn("relative z-50 max-h-[min(var(--radix-select-content-available-height),18rem)] min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
position={position}
align={align}
{...props}