优化复刻视频脚本和下拉框样式

This commit is contained in:
Azmat@qq.com
2026-08-31 10:59:54 +08:00
parent 284748eeff
commit f59e9d0418
20 changed files with 775 additions and 212 deletions
+61 -33
View File
@@ -9,6 +9,7 @@ import { isPublicGenerationError, presentGenerationError } from "../generation-e
import type { Notice, Page } from "./route-config";
import { stageOrder, statusPill } from "./stage-config";
import { ConfirmModal, MediaLightbox, TeamModal, useBodyScrollLock } from "../components/overlays";
import { CustomSelect } from "../components/custom-select";
import { DEFAULT_BILLING_RATES, estimateCost, FC_MODELS, modelLabel } from "../components/free-create/constants";
import { ModelLibrary } from "../components/model-library";
import { ReviewBadge, type ReviewStatus } from "../components/review-badge";
@@ -1198,25 +1199,33 @@ export function PipelinePage(props: {
return (
<div className="as-spec-fields" aria-label="成片规格">
<label className="as-spec-field">
<select className="select" value={outputAspect} onChange={(event) => changeOutputSpec({ aspect_ratio: event.target.value })}>
{OUTPUT_RATIOS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
</select>
<CustomSelect fill size="sm" value={outputAspect} onChange={(next) => changeOutputSpec({ aspect_ratio: next })} options={OUTPUT_RATIOS} />
</label>
<label className="as-spec-field">
<select className="select" value={outputResolution} onChange={(event) => changeOutputSpec({ resolution: event.target.value })}>
{OUTPUT_RESOLUTIONS.map((option) => (
<option key={option.value} value={option.value} disabled={Boolean(supportedResolutions.length && !supportedResolutions.includes(option.value))}>
{option.label}
</option>
))}
</select>
<CustomSelect
fill
size="sm"
value={outputResolution}
onChange={(next) => changeOutputSpec({ resolution: next })}
options={OUTPUT_RESOLUTIONS.map((option) => ({
...option,
disabled: Boolean(supportedResolutions.length && !supportedResolutions.includes(option.value)),
}))}
/>
</label>
<label className="as-spec-field">
<select className="select" value={outputModelId} onChange={(event) => changeOutputSpec({ video_model_config_id: event.target.value })} disabled={!videoConfigs.length}>
{videoConfigs.length ? videoConfigs.map((config) => (
<option key={config.id} value={config.id}>{FC_MODELS.find((item) => item.name === config.name)?.label || config.display_name || modelLabel(config.name)}</option>
)) : <option value="">暂无模型</option>}
</select>
<CustomSelect
fill
size="sm"
value={outputModelId}
disabled={!videoConfigs.length}
placeholder="暂无模型"
onChange={(next) => changeOutputSpec({ video_model_config_id: next })}
options={videoConfigs.map((config) => ({
value: config.id,
label: FC_MODELS.find((item) => item.name === config.name)?.label || config.display_name || modelLabel(config.name),
}))}
/>
</label>
</div>
);
@@ -3222,27 +3231,42 @@ export function PipelinePage(props: {
沿用模板的镜数、每镜作用和节奏,文案、画面、模特与故事板都会按{setupProduct?.title || "当前商品"}重新生成 —— 不是把上一条片子换个商品名。
</div>
)}
<label className="setup-field">
<div className="setup-field">
<span className="sf-k">视频结构</span>
<select className="setup-select" value={setupStructure} onChange={(e) => { setupTouched.current = true; setSetupStructure(e.target.value as VideoStructure); }}>
{structureOptions.map((k) => <option key={k} value={k}>{VIDEO_STRUCTURES[k]}</option>)}
</select>
</label>
<CustomSelect
fill
size="sm"
className="setup-select"
value={setupStructure}
onChange={(next) => { setupTouched.current = true; setSetupStructure(next as VideoStructure); }}
options={structureOptions.map((k) => ({ value: k, label: VIDEO_STRUCTURES[k] }))}
/>
</div>
<div className="setup-rec">{STRUCTURE_HINT[setupStructure]}</div>
<label className="setup-field">
<div className="setup-field">
<span className="sf-k">人物</span>
<select className="setup-select" value={setupPersona} onChange={(e) => { setupTouched.current = true; setSetupPersona(e.target.value); }}>
{SETUP_PERSONA_KEYS.map((k) => <option key={k} value={k}>{WIZ_PERSONA_LABEL[k]}</option>)}
</select>
</label>
<CustomSelect
fill
size="sm"
className="setup-select"
value={setupPersona}
onChange={(next) => { setupTouched.current = true; setSetupPersona(next); }}
options={SETUP_PERSONA_KEYS.map((k) => ({ value: k, label: WIZ_PERSONA_LABEL[k] }))}
/>
</div>
<div className="setup-rec">{recommended.reason} · {WIZ_PERSONA_LABEL[recommended.persona] || recommended.persona}</div>
{/* 时长:15/30/45/60;每镜固定 15 秒 */}
<label className="setup-field">
<div className="setup-field">
<span className="sf-k">时长</span>
<select className="setup-select" value={setupDuration} onChange={(e) => { setupTouched.current = true; setSetupDuration(clampDuration(Number(e.target.value))); }}>
{DURATION_OPTIONS.map((s) => <option key={s} value={s}>{s} 秒</option>)}
</select>
</label>
<CustomSelect
fill
size="sm"
className="setup-select"
value={String(setupDuration)}
onChange={(next) => { setupTouched.current = true; setSetupDuration(clampDuration(Number(next))); }}
options={DURATION_OPTIONS.map((s) => ({ value: String(s), label: `${s} 秒` }))}
/>
</div>
<div className={`setup-rec${durationHint ? " warn" : ""}`}>{durationHint || `口播 × ${VIDEO_STRUCTURES[setupStructure]}建议 ${durationSuggest} 秒左右`}</div>
<div className="setup-foot">
{/* ← 返回:收起设定卡,回到「脚本辅助生成 / 上传脚本」入口页 */}
@@ -4101,9 +4125,13 @@ export function PipelinePage(props: {
{voStale && <div style={{ fontSize: "12px", color: "#B45309", marginBottom: 6 }}>字幕文本已修改,与现有配音不一致,建议重新生成</div>}
<div className="props-row" style={{ marginBottom: 6 }}>
<span className="k">音色</span>
<select value={voVoicePick || voInfo?.voice_type || VO_VOICES[0].key} onChange={(e) => setVoVoicePick(e.target.value)} style={{ flex: 1, fontSize: "12px", padding: "4px 6px", border: "1px solid var(--border-faint)", borderRadius: "6px", background: "var(--surface)", color: "var(--accent-black)" }}>
{VO_VOICES.map((v) => <option key={v.key} value={v.key}>{v.label}</option>)}
</select>
<CustomSelect
fill
size="sm"
value={voVoicePick || voInfo?.voice_type || VO_VOICES[0].key}
onChange={setVoVoicePick}
options={VO_VOICES.map((v) => ({ value: v.key, label: v.label }))}
/>
</div>
<div style={{ display: "flex", gap: 6 }}>
<button className="btn btn-sm" type="button" disabled={loading || !edState.clips.some((c) => c.subtitle.trim())} onClick={() => onGenerateVoiceover({ items: edState.clips.map((c, idx) => ({ index: idx, text: c.subtitle.trim() })).filter((item) => item.text), voice_type: voVoicePick || voInfo?.voice_type || VO_VOICES[0].key })}>{voInfo ? "重新生成配音" : `生成配音 · ${pts(10)} 积分/500字`}</button>