+
setSettingsOpen(false)}>
+
@@ -372,24 +372,17 @@ export default function VideoPage() {
}
function GenerationSettings({ config, model, updateConfig, openConfigDialog }: { config: AiConfig; model: string; updateConfig: UpdateAiConfig; openConfigDialog: (shouldPromptContinue?: boolean) => void }) {
+ const theme = canvasThemes[useThemeStore((state) => state.theme)];
+
return (
<>
-
-
-
+
+ updateConfig(key, value)} theme={theme} showTitle={false} className="space-y-4" />
+
>
);
}
@@ -582,13 +575,13 @@ function buildVideoConfig(config: AiConfig, model: string): AiConfig {
function normalizeVideoSeconds(value: string) {
const seconds = Math.floor(Number(value) || 6);
- return String([6, 10, 12, 16, 20].includes(seconds) ? seconds : 6);
+ return String(Math.max(1, Math.min(20, seconds)));
}
function normalizeVideoSize(value: string) {
- return /^\d+x\d+$/.test(value || "") ? value : "1280x720";
+ return normalizeVideoSizeValue(value);
}
function normalizeResolution(value: string) {
- return value.replace(/p$/i, "") || "720";
+ return normalizeVideoResolutionValue(value);
}
diff --git a/web/src/components/image-settings-panel.tsx b/web/src/components/image-settings-panel.tsx
new file mode 100644
index 0000000..66025ce
--- /dev/null
+++ b/web/src/components/image-settings-panel.tsx
@@ -0,0 +1,209 @@
+"use client";
+
+import { type ReactNode } from "react";
+import { ConfigProvider } from "antd";
+
+import { type CanvasTheme } from "@/lib/canvas-theme";
+import type { AiConfig } from "@/stores/use-config-store";
+
+const qualityOptions = [
+ { value: "auto", label: "自动" },
+ { value: "high", label: "高" },
+ { value: "medium", label: "中" },
+ { value: "low", label: "低" },
+];
+
+const aspectOptions = [
+ { value: "1:1", label: "1:1", width: 1024, height: 1024, icon: "square" },
+ { value: "3:2", label: "3:2", width: 1536, height: 1024, icon: "landscape" },
+ { value: "2:3", label: "2:3", width: 1024, height: 1536, icon: "portrait" },
+ { value: "4:3", label: "4:3", width: 1344, height: 1024, icon: "landscape" },
+ { value: "3:4", label: "3:4", width: 1024, height: 1344, icon: "portrait" },
+ { value: "9:16", label: "9:16", width: 1024, height: 1792, icon: "portrait" },
+ { value: "1:1-2k", label: "1:1(2k)", size: "2048x2048", width: 2048, height: 2048, icon: "square" },
+ { value: "16:9-2k", label: "16:9(2k)", size: "2048x1152", width: 2048, height: 1152, icon: "landscape" },
+ { value: "9:16-2k", label: "9:16(2k)", size: "1152x2048", width: 1152, height: 2048, icon: "portrait" },
+ { value: "16:9-4k", label: "16:9(4k)", size: "3840x2160", width: 3840, height: 2160, icon: "landscape" },
+ { value: "9:16-4k", label: "9:16(4k)", size: "2160x3840", width: 2160, height: 3840, icon: "portrait" },
+ { value: "auto", label: "auto", width: 0, height: 0, icon: "auto" },
+];
+
+type ImageSettingsPanelProps = {
+ config: AiConfig;
+ onConfigChange: (key: "quality" | "size" | "count", value: string) => void;
+ theme: CanvasTheme;
+ showTitle?: boolean;
+ className?: string;
+ maxCount?: number;
+ quickCount?: number;
+};
+
+export function ImageSettingsPanel({ config, onConfigChange, theme, showTitle = true, className = "w-[320px] space-y-4 rounded-2xl px-1 py-0.5", maxCount = 15, quickCount = 10 }: ImageSettingsPanelProps) {
+ const quality = config.quality || "auto";
+ const count = Math.max(1, Math.min(maxCount, Math.floor(Math.abs(Number(config.count)) || 1)));
+ const activeSize = config.size || "auto";
+ const selectedAspect = aspectOptions.find((item) => (item.size || item.value) === activeSize || item.value === activeSize);
+ const dimensions = readSizeDimensions(activeSize, selectedAspect || aspectOptions[0]);
+ const selectAspect = (value: string) => {
+ const option = aspectOptions.find((item) => item.value === value);
+ onConfigChange("size", option?.size || option?.value || "auto");
+ };
+ const updateDimension = (key: "width" | "height", value: number | null) => {
+ const next = Math.max(1, Math.floor(value || dimensions[key] || 1024));
+ onConfigChange("size", `${key === "width" ? next : dimensions.width}x${key === "height" ? next : dimensions.height}`);
+ };
+
+ return (
+
+ event.stopPropagation()}>
+ {showTitle ?
图像设置
: null}
+
+
质量
+
+ {qualityOptions.map((item) => (
+ onConfigChange("quality", item.value)}>
+ {item.label}
+
+ ))}
+
+
+
+
尺寸
+
+ updateDimension("width", value)} />
+ ↔
+ updateDimension("height", value)} />
+
+
+
+
宽高比
+
+ {aspectOptions.map((item) => (
+
+ ))}
+
+
+
+
生成张数
+
+ {Array.from({ length: quickCount }, (_, index) => index + 1).map((value) => (
+ onConfigChange("count", String(value))}>
+ {value} 张
+
+ ))}
+ onConfigChange("count", String(value || 1))} />
+
+
+
+
+ );
+}
+
+export function ImageSettingsTheme({ theme, children }: { theme: CanvasTheme; children: ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function imageQualityLabel(value: string) {
+ return ({ auto: "自动", high: "高", medium: "中", low: "低" } as Record
)[value] || value;
+}
+
+export function imageSizeLabel(size: string) {
+ return aspectOptions.find((item) => (item.size || item.value) === size || item.value === size)?.label || size;
+}
+
+function OptionPill({ selected, theme, onClick, children }: { selected: boolean; theme: CanvasTheme; onClick: () => void; children: ReactNode }) {
+ return (
+
+ );
+}
+
+function DimensionInput({ prefix, value, disabled, theme, onChange }: { prefix: string; value: number; disabled: boolean; theme: CanvasTheme; onChange: (value: number | null) => void }) {
+ return (
+
+ );
+}
+
+function CountInput({ value, max, theme, onChange }: { value: number; max: number; theme: CanvasTheme; onChange: (value: number | null) => void }) {
+ return (
+
+ );
+}
+
+function AspectIcon({ type, width, height, color }: { type: string; width: number; height: number; color: string }) {
+ if (type === "auto") return null;
+ const ratio = width / Math.max(1, height);
+ const boxWidth = ratio >= 1 ? 24 : Math.max(10, 24 * ratio);
+ const boxHeight = ratio >= 1 ? Math.max(10, 24 / ratio) : 24;
+ return (
+
+
+
+ );
+}
+
+function SettingTitle({ children, color }: { children: string; color: string }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function readSizeDimensions(size: string, fallback: { width: number; height: number }) {
+ const match = size?.match(/^(\d+)x(\d+)$/);
+ return {
+ width: match ? Number(match[1]) : fallback.width,
+ height: match ? Number(match[2]) : fallback.height,
+ };
+}
diff --git a/web/src/components/video-settings-panel.tsx b/web/src/components/video-settings-panel.tsx
new file mode 100644
index 0000000..cae6b3f
--- /dev/null
+++ b/web/src/components/video-settings-panel.tsx
@@ -0,0 +1,181 @@
+"use client";
+
+import { type ReactNode } from "react";
+
+import { ImageSettingsTheme } from "@/components/image-settings-panel";
+import { type CanvasTheme } from "@/lib/canvas-theme";
+import type { AiConfig } from "@/stores/use-config-store";
+
+const resolutionOptions = [
+ { value: "720", label: "720p" },
+ { value: "480", label: "480p" },
+];
+
+const sizeOptions = [
+ { value: "1280x720", label: "横屏", width: 1280, height: 720 },
+ { value: "720x1280", label: "竖屏", width: 720, height: 1280 },
+ { value: "1024x1024", label: "方形", width: 1024, height: 1024 },
+ { value: "1792x1024", label: "宽屏", width: 1792, height: 1024 },
+ { value: "1024x1792", label: "长图", width: 1024, height: 1792 },
+ { value: "auto", label: "auto", width: 0, height: 0 },
+];
+
+const secondOptions = [6, 10, 12, 16, 20];
+
+type VideoSettingsPanelProps = {
+ config: AiConfig;
+ onConfigChange: (key: "vquality" | "size" | "videoSeconds", value: string) => void;
+ theme: CanvasTheme;
+ showTitle?: boolean;
+ className?: string;
+};
+
+export function VideoSettingsPanel({ config, onConfigChange, theme, showTitle = true, className = "w-[320px] space-y-4 rounded-2xl px-1 py-0.5" }: VideoSettingsPanelProps) {
+ const seconds = config.videoSeconds || "6";
+ const size = normalizeVideoSizeValue(config.size);
+ const dimensions = readSizeDimensions(size);
+ const resolution = normalizeVideoResolutionValue(config.vquality);
+ const updateDimension = (key: "width" | "height", value: number | null) => {
+ const next = Math.max(1, Math.floor(value || dimensions[key] || 720));
+ onConfigChange("size", `${key === "width" ? next : dimensions.width}x${key === "height" ? next : dimensions.height}`);
+ };
+
+ return (
+
+ event.stopPropagation()}>
+ {showTitle ?
视频设置
: null}
+
+
+ {resolutionOptions.map((item) => (
+ onConfigChange("vquality", item.value)}>
+ {item.label}
+
+ ))}
+ onConfigChange("vquality", value)} />
+
+
+
+
+ updateDimension("width", value)} />
+ ↔
+ updateDimension("height", value)} />
+
+
+ {sizeOptions.map((item) => (
+
+ ))}
+
+
+
+
+ {secondOptions.map((value) => (
+ onConfigChange("videoSeconds", String(value))}>
+ {value}s
+
+ ))}
+ onConfigChange("videoSeconds", value)} />
+
+
+
+
+ );
+}
+
+export function videoResolutionLabel(value: string) {
+ return `${normalizeVideoResolutionValue(value)}p`;
+}
+
+export function videoSizeLabel(value: string) {
+ const size = normalizeVideoSizeValue(value);
+ return sizeOptions.find((item) => item.value === size)?.label || size;
+}
+
+export function videoSecondsLabel(value: string) {
+ return `${value || "6"}s`;
+}
+
+export function normalizeVideoSizeValue(value: string) {
+ if (value === "auto") return "auto";
+ if (/^\d+x\d+$/.test(value || "")) return value;
+ return ["9:16", "2:3", "3:4"].includes(value) ? "720x1280" : "1280x720";
+}
+
+export function normalizeVideoResolutionValue(value: string) {
+ if (value === "480p" || value === "low") return "480";
+ if (value === "720p" || value === "auto" || value === "high" || value === "medium") return "720";
+ return value.replace(/p$/i, "") || "720";
+}
+
+function OptionPill({ selected, theme, onClick, children }: { selected: boolean; theme: CanvasTheme; onClick: () => void; children: ReactNode }) {
+ return (
+
+ );
+}
+
+function SettingGroup({ title, color, children }: { title: string; color: string; children: ReactNode }) {
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+}
+
+function ResolutionInput({ value, theme, onChange }: { value: string; theme: CanvasTheme; onChange: (value: string) => void }) {
+ return (
+
+ );
+}
+
+function DimensionInput({ prefix, value, disabled, theme, onChange }: { prefix: string; value: number; disabled: boolean; theme: CanvasTheme; onChange: (value: number | null) => void }) {
+ return (
+
+ );
+}
+
+function NumberInput({ value, min, max, theme, onChange }: { value: string; min: number; max: number; theme: CanvasTheme; onChange: (value: string) => void }) {
+ return onChange(event.target.value)} onMouseDown={(event) => event.stopPropagation()} />;
+}
+
+function SizePreview({ width, height, color }: { width: number; height: number; color: string }) {
+ if (!width || !height) return null;
+ const longSide = Math.max(width, height);
+ const previewWidth = Math.max(10, Math.round((width / longSide) * 26));
+ const previewHeight = Math.max(10, Math.round((height / longSide) * 26));
+ return ;
+}
+
+function readSizeDimensions(size: string) {
+ if (size === "auto") return { width: 0, height: 0 };
+ const match = size.match(/^(\d+)x(\d+)$/);
+ return { width: Number(match?.[1]) || 1280, height: Number(match?.[2]) || 720 };
+}
diff --git a/web/src/services/api/video.ts b/web/src/services/api/video.ts
index ef78daa..863bcab 100644
--- a/web/src/services/api/video.ts
+++ b/web/src/services/api/video.ts
@@ -53,10 +53,11 @@ export async function requestVideoGeneration(config: AiConfig, prompt: string, r
function normalizeVideoSeconds(value: string) {
const seconds = Math.floor(Number(value) || 6);
- return String([6, 10, 12, 16, 20].includes(seconds) ? seconds : 6);
+ return String(Math.max(1, Math.min(20, seconds)));
}
function normalizeVideoSize(value: string) {
+ if (value === "auto") return null;
const size = value || "1280x720";
if (/^\d+x\d+$/.test(size)) return size;
return ["9:16", "2:3", "3:4"].includes(size) ? "720x1280" : "1280x720";