613 lines
24 KiB
TypeScript
613 lines
24 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuTrigger,
|
||
} from "@/components/ui/dropdown-menu";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogTrigger,
|
||
} from "@/components/ui/dialog";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Switch } from "@/components/ui/switch";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Separator } from "@/components/ui/separator";
|
||
import {
|
||
Video,
|
||
Check,
|
||
ChevronDown,
|
||
ChevronUp,
|
||
Sparkles,
|
||
Camera,
|
||
Clock,
|
||
Settings2,
|
||
Maximize2,
|
||
Ratio,
|
||
} from "lucide-react";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
// 视频生成模式类型
|
||
type VideoGenerationMode =
|
||
| "text-to-video"
|
||
| "first-frame"
|
||
| "first-last-frame"
|
||
| "reference-images";
|
||
|
||
// 运镜模式类型(与官方一致)
|
||
type CameraMove =
|
||
| "none" // 智能运镜
|
||
| "static" // 固定镜头
|
||
| "hitchcock-push" // 希区柯克推进
|
||
| "hitchcock-pull" // 希区柯克拉远
|
||
| "mechanical-arm" // 机械臂
|
||
| "dynamic-orbit" // 动感环绕
|
||
| "center-orbit" // 中心环绕
|
||
| "crane" // 起重机
|
||
| "super-pull" // 超级拉远
|
||
| "ccw-rotate" // 逆时针回旋
|
||
| "cw-rotate" // 顺时针回旋
|
||
| "handheld" // 手持运镜
|
||
| "fast-push-pull"; // 快速推拉
|
||
|
||
// 视频参数接口
|
||
export interface VideoGenerationParams {
|
||
mode: VideoGenerationMode;
|
||
resolution: "480p" | "720p" | "1080p";
|
||
ratio: "16:9" | "4:3" | "1:1" | "3:4" | "9:16" | "21:9" | "adaptive";
|
||
duration: number;
|
||
watermark: boolean;
|
||
cameraFixed: boolean;
|
||
cameraMove?: CameraMove; // 新增:运镜模式
|
||
fps?: number;
|
||
seed?: number;
|
||
returnLastFrame?: boolean;
|
||
}
|
||
|
||
interface VideoParamsBarProps {
|
||
modelName?: string;
|
||
initialParams?: Partial<VideoGenerationParams>;
|
||
onParamsChange?: (params: VideoGenerationParams) => void;
|
||
inline?: boolean; // 内联模式:只显示参数按钮,不显示外层容器
|
||
}
|
||
|
||
const MODE_LABELS: Record<VideoGenerationMode, string> = {
|
||
"text-to-video": "文生视频",
|
||
"first-last-frame": "首尾帧",
|
||
"reference-images": "智能多帧",
|
||
"first-frame": "主体参考",
|
||
};
|
||
|
||
const RATIO_OPTIONS = [
|
||
{ value: "21:9", label: "21:9" },
|
||
{ value: "16:9", label: "16:9" },
|
||
{ value: "4:3", label: "4:3" },
|
||
{ value: "1:1", label: "1:1" },
|
||
{ value: "3:4", label: "3:4" },
|
||
{ value: "9:16", label: "9:16" },
|
||
] as const;
|
||
|
||
const RESOLUTION_OPTIONS = [
|
||
{ value: "480p", label: "480P" },
|
||
{ value: "720p", label: "720P" },
|
||
{ value: "1080p", label: "1080P" },
|
||
] as const;
|
||
|
||
// 🎬 运镜模式选项(与官方一致)
|
||
const CAMERA_MOVE_OPTIONS = [
|
||
{ value: "none", label: "智能运镜" },
|
||
{ value: "static", label: "固定镜头" },
|
||
{ value: "hitchcock-push", label: "希区柯克推进" },
|
||
{ value: "hitchcock-pull", label: "希区柯克拉远" },
|
||
{ value: "mechanical-arm", label: "机械臂" },
|
||
{ value: "dynamic-orbit", label: "动感环绕" },
|
||
{ value: "center-orbit", label: "中心环绕" },
|
||
{ value: "crane", label: "起重机" },
|
||
{ value: "super-pull", label: "超级拉远" },
|
||
{ value: "ccw-rotate", label: "逆时针回旋" },
|
||
{ value: "cw-rotate", label: "顺时针回旋" },
|
||
{ value: "handheld", label: "手持运镜" },
|
||
{ value: "fast-push-pull", label: "快速推拉" },
|
||
] as const;
|
||
|
||
// 帧率选项
|
||
const FPS_OPTIONS = [
|
||
{ value: 24, label: "24fps" },
|
||
{ value: 30, label: "30fps" },
|
||
{ value: 60, label: "60fps" },
|
||
] as const;
|
||
|
||
export function VideoParamsBar({
|
||
modelName = "视频 3.0",
|
||
initialParams,
|
||
onParamsChange,
|
||
inline = false,
|
||
}: VideoParamsBarProps) {
|
||
const [params, setParams] = useState<VideoGenerationParams>({
|
||
mode: "text-to-video",
|
||
resolution: "720p",
|
||
ratio: "16:9",
|
||
duration: 5,
|
||
watermark: false, // 默认关闭水印
|
||
cameraFixed: false,
|
||
cameraMove: "none",
|
||
fps: 24,
|
||
...initialParams,
|
||
});
|
||
|
||
const [collapsed, setCollapsed] = useState(false);
|
||
|
||
// 获取当前运镜模式的标签
|
||
const getCurrentCameraLabel = () => {
|
||
if (params.cameraMove) {
|
||
const option = CAMERA_MOVE_OPTIONS.find(opt => opt.value === params.cameraMove);
|
||
return option ? option.label : "智能运镜";
|
||
}
|
||
return "智能运镜";
|
||
};
|
||
|
||
// 🆕 根据模式获取图片上传配置(根据火山引擎官方文档)
|
||
const getImageUploadConfig = () => {
|
||
switch (params.mode) {
|
||
case "text-to-video":
|
||
return { min: 0, max: 0, label: "文生视频(无需图片)" };
|
||
case "first-last-frame":
|
||
return { min: 2, max: 2, label: "首尾帧(各 1 张)" };
|
||
case "first-frame":
|
||
return { min: 1, max: 1, label: "首帧模式(1 张)" };
|
||
case "reference-images":
|
||
return { min: 1, max: 4, label: "参考图(1-4 张)" };
|
||
default:
|
||
return { min: 0, max: 0, label: "" };
|
||
}
|
||
};
|
||
|
||
const imageConfig = getImageUploadConfig();
|
||
|
||
const updateParam = <K extends keyof VideoGenerationParams>(
|
||
key: K,
|
||
value: VideoGenerationParams[K]
|
||
) => {
|
||
const newParams = { ...params, [key]: value };
|
||
setParams(newParams);
|
||
onParamsChange?.(newParams);
|
||
};
|
||
|
||
// 🆕 内联模式:只返回参数按钮,嵌入工具栏
|
||
if (inline) {
|
||
return (
|
||
<div className="flex items-center ml-1 border-l border-border/50 pl-1">
|
||
{/* 模式选择 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" className="h-8 px-2 gap-1 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
<Maximize2 size={14} />
|
||
<span className="font-medium text-xs">{MODE_LABELS[params.mode]}</span>
|
||
<ChevronDown size={14} />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="start" className="w-36">
|
||
<DropdownMenuItem onClick={() => updateParam("mode", "text-to-video")}>
|
||
<Check className={cn("mr-2 h-4 w-4", params.mode === "text-to-video" ? "opacity-100" : "opacity-0")} />
|
||
文生视频
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => updateParam("mode", "first-last-frame")}>
|
||
<Check className={cn("mr-2 h-4 w-4", params.mode === "first-last-frame" ? "opacity-100" : "opacity-0")} />
|
||
首尾帧
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => updateParam("mode", "reference-images")}>
|
||
<Check className={cn("mr-2 h-4 w-4", params.mode === "reference-images" ? "opacity-100" : "opacity-0")} />
|
||
智能多帧
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => updateParam("mode", "first-frame")}>
|
||
<Check className={cn("mr-2 h-4 w-4", params.mode === "first-frame" ? "opacity-100" : "opacity-0")} />
|
||
主体参考
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 宽高比 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" className="h-8 px-2 gap-1 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
<Ratio size={14} />
|
||
<span className="font-medium text-xs">{params.ratio}</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="w-48">
|
||
<div className="p-1.5 grid grid-cols-3 gap-1">
|
||
{RATIO_OPTIONS.map((option) => (
|
||
<Button
|
||
key={option.value}
|
||
variant={params.ratio === option.value ? "secondary" : "ghost"}
|
||
size="sm"
|
||
className="h-7 text-xs"
|
||
onClick={() => updateParam("ratio", option.value)}
|
||
>
|
||
{option.label}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 分辨率 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" className="h-8 px-2 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
<span className="font-medium text-xs">{params.resolution}</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="min-w-[100px]">
|
||
{RESOLUTION_OPTIONS.map((option) => (
|
||
<DropdownMenuItem key={option.value} onClick={() => updateParam("resolution", option.value)}>
|
||
<Check className={cn("mr-2 h-4 w-4", params.resolution === option.value ? "opacity-100" : "opacity-0")} />
|
||
{option.label}
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 时长 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" className="h-8 px-2 gap-1 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
<Clock size={14} />
|
||
<span className="font-medium text-xs">{params.duration}s</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="min-w-[80px]">
|
||
{[5, 10].map((duration) => (
|
||
<DropdownMenuItem key={duration} onClick={() => updateParam("duration", duration)}>
|
||
<Check className={cn("mr-2 h-4 w-4", params.duration === duration ? "opacity-100" : "opacity-0")} />
|
||
{duration}s
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 帧率 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" className="h-8 px-2 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
<span className="font-medium text-xs">{params.fps || 24}fps</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="min-w-[90px]">
|
||
{FPS_OPTIONS.map((option) => (
|
||
<DropdownMenuItem key={option.value} onClick={() => updateParam("fps", option.value as VideoGenerationParams["fps"])}>
|
||
<Check className={cn("mr-2 h-4 w-4", (params.fps || 24) === option.value ? "opacity-100" : "opacity-0")} />
|
||
{option.label}
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 运镜(改为下拉菜单,和其他按钮一致) */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" className="h-8 px-2 gap-1 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
<Camera size={14} />
|
||
<span className="font-medium text-xs">{getCurrentCameraLabel()}</span>
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="min-w-[100px]">
|
||
{CAMERA_MOVE_OPTIONS.map((option) => (
|
||
<DropdownMenuItem
|
||
key={option.value}
|
||
onClick={() => {
|
||
updateParam("cameraMove", option.value);
|
||
updateParam("cameraFixed", option.value === "static");
|
||
}}
|
||
>
|
||
<Check className={cn("mr-2 h-4 w-4", params.cameraMove === option.value ? "opacity-100" : "opacity-0")} />
|
||
{option.label}
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 高级设置 */}
|
||
<Dialog>
|
||
<DialogTrigger asChild>
|
||
<Button variant="ghost" className="h-8 w-8 px-0 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
<Settings2 size={14} />
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent className="sm:max-w-[280px]">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-sm">高级设置</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="space-y-4 py-2">
|
||
<div className="flex items-center justify-between">
|
||
<Label className="text-xs">返回尾帧</Label>
|
||
<Switch
|
||
checked={params.returnLastFrame || false}
|
||
onCheckedChange={(checked) => updateParam("returnLastFrame", checked)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<Label className="text-xs">随机种子</Label>
|
||
<Input
|
||
type="number"
|
||
placeholder="留空则随机"
|
||
value={params.seed || ""}
|
||
onChange={(e) => updateParam("seed", Number(e.target.value) || undefined)}
|
||
className="h-8 text-xs"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 非内联模式(独立显示)
|
||
if (collapsed) {
|
||
return (
|
||
<div className="flex items-center justify-between py-2 px-4 border-t bg-muted/20">
|
||
<span className="text-sm text-muted-foreground">
|
||
视频参数:{MODE_LABELS[params.mode]} | {params.ratio} | {params.resolution} | {params.duration}s
|
||
</span>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => setCollapsed(false)}
|
||
>
|
||
<ChevronDown size={16} />
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="bg-background/95 backdrop-blur-sm border-t-0 p-2">
|
||
{/* 🆕 图片上传区域(根据模式动态显示) */}
|
||
{imageConfig.max > 0 && (
|
||
<div className="pb-2 flex justify-center">
|
||
<div className="flex items-center gap-2">
|
||
<Label className="text-xs text-muted-foreground mr-2">{imageConfig.label}</Label>
|
||
{params.mode === "first-last-frame" && (
|
||
<div className="flex gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="gap-1.5 h-14 w-20 flex-col border-dashed hover:border-solid hover:bg-accent/50"
|
||
>
|
||
<span className="text-xl">+</span>
|
||
<span className="text-[10px]">首帧</span>
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="gap-1.5 h-14 w-20 flex-col border-dashed hover:border-solid hover:bg-accent/50"
|
||
>
|
||
<span className="text-xl">➡</span>
|
||
<span className="text-[10px]">尾帧</span>
|
||
</Button>
|
||
</div>
|
||
)}
|
||
{params.mode === "first-frame" && (
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
className="gap-1.5 h-14 w-20 flex-col border-dashed hover:border-solid hover:bg-accent/50"
|
||
>
|
||
<span className="text-xl">+</span>
|
||
<span className="text-[10px]">图帧</span>
|
||
</Button>
|
||
)}
|
||
{params.mode === "reference-images" && (
|
||
<div className="flex gap-2">
|
||
{[1, 2, 3, 4].slice(0, imageConfig.max).map((idx) => (
|
||
<Button
|
||
key={idx}
|
||
variant="outline"
|
||
size="sm"
|
||
className="gap-1.5 h-14 w-14 flex-col border-dashed hover:border-solid hover:bg-accent/50"
|
||
>
|
||
<span className="text-xl">+</span>
|
||
<span className="text-[10px]">{idx}</span>
|
||
</Button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 参数配置栏 - 移除背景色和顶部边框,适应嵌入式布局 */}
|
||
<div className="flex items-center justify-between gap-2 overflow-x-auto scrollbar-hide">
|
||
<div className="flex items-center gap-2">
|
||
{/* 模式选择(无边框,更简洁) */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" size="sm" className="gap-1 px-2 h-8 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
<Maximize2 size={14} />
|
||
{MODE_LABELS[params.mode]}
|
||
<ChevronDown size={12} className="opacity-50" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="start" className="w-40">
|
||
<DropdownMenuItem onClick={() => updateParam("mode", "text-to-video")}>
|
||
<Check className={cn("mr-2 h-4 w-4", params.mode === "text-to-video" ? "opacity-100" : "opacity-0")} />
|
||
文生视频
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => updateParam("mode", "first-last-frame")}>
|
||
<Check className={cn("mr-2 h-4 w-4", params.mode === "first-last-frame" ? "opacity-100" : "opacity-0")} />
|
||
首尾帧
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => updateParam("mode", "first-frame")}>
|
||
<Check className={cn("mr-2 h-4 w-4", params.mode === "first-frame" ? "opacity-100" : "opacity-0")} />
|
||
智能多帧
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem onClick={() => updateParam("mode", "reference-images")}>
|
||
<Check className={cn("mr-2 h-4 w-4", params.mode === "reference-images" ? "opacity-100" : "opacity-0")} />
|
||
主体参考
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
<Separator orientation="vertical" className="h-4" />
|
||
|
||
{/* 宽高比 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" size="sm" className="gap-1 px-2 h-8 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
<Ratio size={14} />
|
||
{params.ratio}
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="w-64">
|
||
<div className="p-2 grid grid-cols-3 gap-1">
|
||
{RATIO_OPTIONS.map((option) => (
|
||
<Button
|
||
key={option.value}
|
||
variant={params.ratio === option.value ? "secondary" : "ghost"}
|
||
size="sm"
|
||
className="h-7 text-xs"
|
||
onClick={() => updateParam("ratio", option.value)}
|
||
>
|
||
{option.label}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 分辨率 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" size="sm" className="gap-1 px-2 h-8 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
{params.resolution}
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="min-w-[100px]">
|
||
{RESOLUTION_OPTIONS.map((option) => (
|
||
<DropdownMenuItem key={option.value} onClick={() => updateParam("resolution", option.value)}>
|
||
<Check className={cn("mr-2 h-4 w-4", params.resolution === option.value ? "opacity-100" : "opacity-0")} />
|
||
{option.label}
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 时长 */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" size="sm" className="gap-1 px-2 h-8 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
<Clock size={14} />
|
||
{params.duration}s
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="min-w-[80px]">
|
||
{[5, 10].map((duration) => (
|
||
<DropdownMenuItem key={duration} onClick={() => updateParam("duration", duration)}>
|
||
<Check className={cn("mr-2 h-4 w-4", params.duration === duration ? "opacity-100" : "opacity-0")} />
|
||
{duration}s
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 帧率 (新增) */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" size="sm" className="gap-1 px-2 h-8 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
{params.fps || 24}fps
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="min-w-[100px]">
|
||
{FPS_OPTIONS.map((option) => (
|
||
<DropdownMenuItem
|
||
key={option.value}
|
||
onClick={() => updateParam("fps", option.value as VideoGenerationParams["fps"])}
|
||
>
|
||
<Check
|
||
className={cn(
|
||
"mr-2 h-4 w-4",
|
||
(params.fps || 24) === option.value ? "opacity-100" : "opacity-0"
|
||
)}
|
||
/>
|
||
{option.label}
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
|
||
{/* 运镜(改为下拉菜单,与官方一致) */}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="ghost" size="sm" className="gap-1 px-2 h-8 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
<Camera size={14} />
|
||
{getCurrentCameraLabel()}
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="center" className="min-w-[140px]">
|
||
{CAMERA_MOVE_OPTIONS.map((option) => (
|
||
<DropdownMenuItem
|
||
key={option.value}
|
||
onSelect={() => {
|
||
updateParam("cameraMove", option.value);
|
||
updateParam("cameraFixed", option.value === "static");
|
||
}}
|
||
>
|
||
<Check
|
||
className={cn("mr-2 h-4 w-4", params.cameraMove === option.value ? "opacity-100" : "opacity-0")}
|
||
/>
|
||
{option.label}
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
|
||
{/* 高级选项(隐藏水印,默认关闭) */}
|
||
<Dialog>
|
||
<DialogTrigger asChild>
|
||
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground/70 focus-visible:ring-0 focus-visible:ring-offset-0">
|
||
<Settings2 size={14} />
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent className="sm:max-w-[300px]">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-sm">高级设置</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="space-y-4 py-2">
|
||
<div className="flex items-center justify-between">
|
||
<Label className="text-xs">返回尾帧</Label>
|
||
<Switch
|
||
checked={params.returnLastFrame || false}
|
||
onCheckedChange={(checked) => updateParam("returnLastFrame", checked)}
|
||
className="scale-75"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<Label className="text-xs">随机种子</Label>
|
||
<Input
|
||
type="number"
|
||
placeholder="留空则随机"
|
||
value={params.seed || ""}
|
||
onChange={(e) =>
|
||
updateParam("seed", Number(e.target.value) || undefined)
|
||
}
|
||
className="h-8 text-xs"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|