// 自由创作·输入条工具栏:模型/模式/比例/分辨率/时长/种子 下拉 + 预估消耗 + 清空 + 生成。 // 约束联动(与后端校验一致):1080P/4K 仅标准档;切非标准档时分辨率自动回落 720P。 import { useEffect, useRef, useState } from "react"; import type { ModelConfig } from "../../types"; import { DEFAULT_BILLING_RATES, FC_DURATIONS, FC_MODELS, FC_RATIOS, FC_RESOLUTIONS, FC_STANDARD_MODEL, MODE_LABELS, estimateCost, type BillingRates, type FreeMode, type LocalRef } from "./constants"; type MenuItem = { value: string; label: string; desc?: string; disabled?: boolean; hint?: string }; function FcDropdown({ label, display, items, onSelect, disabled }: { label: string; display: string; items: MenuItem[]; onSelect: (value: string) => void; disabled?: boolean; }) { const [open, setOpen] = useState(false); const wrapRef = useRef(null); useEffect(() => { if (!open) return; const onDown = (event: MouseEvent) => { if (!wrapRef.current?.contains(event.target as Node)) setOpen(false); }; const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") setOpen(false); }; document.addEventListener("mousedown", onDown); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDown); document.removeEventListener("keydown", onKey); }; }, [open]); return (
{open && (
{items.map((item) => ( ))}
)}
); } export function FreeToolbar({ mode, model, ratio, resolution, duration, seed, refs, videoConfigs, billingRates, hasPrompt, submitting, onModeChange, onModelChange, onRatioChange, onResolutionChange, onDurationChange, onSeedChange, onClear, onSend }: { mode: FreeMode; model: string; ratio: string; resolution: string; duration: number; seed: number; refs: LocalRef[]; videoConfigs: ModelConfig[]; billingRates?: BillingRates; hasPrompt: boolean; submitting: boolean; onModeChange: (mode: FreeMode) => void; onModelChange: (model: string) => void; onRatioChange: (ratio: string) => void; onResolutionChange: (resolution: string) => void; onDurationChange: (duration: number) => void; onSeedChange: (seed: number) => void; onClear: () => void; onSend: () => void; }) { const isStandard = model === FC_STANDARD_MODEL; const config = videoConfigs.find((c) => c.name === model); const { tokens, points } = estimateCost(config, { ratio, resolution, duration, refs }, billingRates || DEFAULT_BILLING_RATES); const [seedOpen, setSeedOpen] = useState(false); const seedRef = useRef(null); useEffect(() => { if (!seedOpen) return; const onDown = (event: MouseEvent) => { if (!seedRef.current?.contains(event.target as Node)) setSeedOpen(false); }; document.addEventListener("mousedown", onDown); return () => document.removeEventListener("mousedown", onDown); }, [seedOpen]); const uploading = refs.some((r) => r.uploading); const canSend = hasPrompt && !submitting && !uploading; return (
m.name === model)?.label || model} items={FC_MODELS.map((m) => ({ value: m.name, label: m.label, desc: m.desc }))} onSelect={(value) => { onModelChange(value); // 非标准档不支持 1080P/4K:自动回落 720P(与 jimeng 行为一致) if (value !== FC_STANDARD_MODEL && (resolution === "1080p" || resolution === "4k")) onResolutionChange("720p"); }} /> onModeChange(value as FreeMode)} /> ({ value: r, label: r }))} onSelect={onRatioChange} /> ({ value: r, label: r.toUpperCase(), disabled: (r === "1080p" || r === "4k") && !isStandard, hint: "仅 Seedance 2.0 标准档支持" }))} onSelect={onResolutionChange} /> ({ value: String(d), label: `${d}s` }))} onSelect={(value) => onDurationChange(Number(value))} />
{seedOpen && (
{ const v = parseInt(event.target.value, 10); onSeedChange(Number.isNaN(v) ? -1 : v); }} />
// -1 = 随机;相同种子可复现相似结果
)}
≈ {tokens.toLocaleString()} tokens · {points.toLocaleString()} 积分
); }