优化复刻视频脚本和下拉框样式
This commit is contained in:
@@ -7,6 +7,7 @@ import type { NavigateFn } from "./route-config";
|
||||
import { money, pts, stageMeta, yuan } from "./stage-config";
|
||||
import { pageWindow } from "../components/pager";
|
||||
import { useBodyScrollLock, useOverlayTransition } from "../components/overlays";
|
||||
import { CustomSelect } from "../components/custom-select";
|
||||
|
||||
const ROLE_LABEL: Record<string, string> = { owner: "超管", admin: "团管", member: "成员", viewer: "访客" };
|
||||
const STATUS_LABEL: Record<string, string> = { active: "活跃", invited: "待激活", disabled: "已停用" };
|
||||
@@ -466,19 +467,31 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
|
||||
<div className={`tab-panel ${tab === "bills" ? "active" : ""}`}>
|
||||
<div className="filter-bar">
|
||||
<select value={billType} onChange={(e) => { setBillType(e.target.value); setBillPage(1); }} aria-label="按类型筛选">
|
||||
<option value="all">全部类型</option>
|
||||
<option value="charge">扣费</option>
|
||||
<option value="recharge">充值</option>
|
||||
<option value="reserve">预扣</option>
|
||||
<option value="release">释放</option>
|
||||
<option value="adjustment">调整</option>
|
||||
<option value="refund">退款</option>
|
||||
</select>
|
||||
<select value={billMember} onChange={(e) => { setBillMember(e.target.value); setBillPage(1); }} aria-label="按成员筛选">
|
||||
<option value="all">全部成员</option>
|
||||
{billMemberOptions.map((m) => <option key={m.id} value={m.id}>{m.label}</option>)}
|
||||
</select>
|
||||
<CustomSelect
|
||||
size="sm"
|
||||
aria-label="按类型筛选"
|
||||
value={billType}
|
||||
onChange={(next) => { setBillType(next); setBillPage(1); }}
|
||||
options={[
|
||||
{ value: "all", label: "全部类型" },
|
||||
{ value: "charge", label: "扣费" },
|
||||
{ value: "recharge", label: "充值" },
|
||||
{ value: "reserve", label: "预扣" },
|
||||
{ value: "release", label: "释放" },
|
||||
{ value: "adjustment", label: "调整" },
|
||||
{ value: "refund", label: "退款" },
|
||||
]}
|
||||
/>
|
||||
<CustomSelect
|
||||
size="sm"
|
||||
aria-label="按成员筛选"
|
||||
value={billMember}
|
||||
onChange={(next) => { setBillMember(next); setBillPage(1); }}
|
||||
options={[
|
||||
{ value: "all", label: "全部成员" },
|
||||
...billMemberOptions.map((m) => ({ value: m.id, label: m.label })),
|
||||
]}
|
||||
/>
|
||||
{billFiltered && (
|
||||
<button className="filter-reset" type="button" onClick={() => { setBillType("all"); setBillMember("all"); setBillPage(1); }}>清除筛选</button>
|
||||
)}
|
||||
@@ -539,12 +552,18 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
|
||||
<div className={`tab-panel ${tab === "by-project" ? "active" : ""}`}>
|
||||
<div className="filter-bar">
|
||||
<select value={projStatus} onChange={(e) => setProjStatus(e.target.value as typeof projStatus)} aria-label="按状态筛选">
|
||||
<option value="all">全部状态</option>
|
||||
<option value="wip">进行中</option>
|
||||
<option value="ok">已完成</option>
|
||||
<option value="fail">失败 · 待重跑</option>
|
||||
</select>
|
||||
<CustomSelect
|
||||
size="sm"
|
||||
aria-label="按状态筛选"
|
||||
value={projStatus}
|
||||
onChange={(next) => setProjStatus(next as typeof projStatus)}
|
||||
options={[
|
||||
{ value: "all", label: "全部状态" },
|
||||
{ value: "wip", label: "进行中" },
|
||||
{ value: "ok", label: "已完成" },
|
||||
{ value: "fail", label: "失败 · 待重跑" },
|
||||
]}
|
||||
/>
|
||||
{projFiltered && (
|
||||
<button className="filter-reset" type="button" onClick={() => setProjStatus("all")}>清除筛选</button>
|
||||
)}
|
||||
@@ -578,12 +597,18 @@ export function AccountPage({ billing, projects, team, onRecharge, onNotify }: {
|
||||
|
||||
<div className={`tab-panel ${tab === "by-member" ? "active" : ""}`}>
|
||||
<div className="filter-bar">
|
||||
<select value={memRole} onChange={(e) => setMemRole(e.target.value as typeof memRole)} aria-label="按角色筛选">
|
||||
<option value="all">全部角色</option>
|
||||
<option value="owner">超管</option>
|
||||
<option value="admin">团管</option>
|
||||
<option value="member">成员</option>
|
||||
</select>
|
||||
<CustomSelect
|
||||
size="sm"
|
||||
aria-label="按角色筛选"
|
||||
value={memRole}
|
||||
onChange={(next) => setMemRole(next as typeof memRole)}
|
||||
options={[
|
||||
{ value: "all", label: "全部角色" },
|
||||
{ value: "owner", label: "超管" },
|
||||
{ value: "admin", label: "团管" },
|
||||
{ value: "member", label: "成员" },
|
||||
]}
|
||||
/>
|
||||
{memFiltered && (
|
||||
<button className="filter-reset" type="button" onClick={() => setMemRole("all")}>清除筛选</button>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { adminApi } from "../../api";
|
||||
import { Pager } from "../../components/pager";
|
||||
import { IconKitSvg } from "../../components/IconKitSvg";
|
||||
import { SystemLoading } from "../../components/loading";
|
||||
import { CustomSelect } from "../../components/custom-select";
|
||||
import type { AdminLedger, AdminQuotaPolicy, AdminTeam } from "../../types";
|
||||
import { pts } from "../stage-config";
|
||||
|
||||
@@ -214,10 +215,13 @@ export function AdminLedgersPage({ notify }: { notify: Notify }) {
|
||||
<p className="admin-modal-desc">为团队手动加 / 减额度(争议补偿)。金额可为负;调额后余额不得为负。会落一条调额流水。</p>
|
||||
<div className="field">
|
||||
<label className="field-label">团队 <span className="req">*</span></label>
|
||||
<select className="select" value={form.team} onChange={(e) => setForm((f) => ({ ...f, team: e.target.value }))}>
|
||||
<option value="">选择团队…</option>
|
||||
{teams.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}
|
||||
</select>
|
||||
<CustomSelect
|
||||
fill
|
||||
value={form.team}
|
||||
placeholder="选择团队…"
|
||||
onChange={(next) => setForm((f) => ({ ...f, team: next }))}
|
||||
options={teams.map((t) => ({ value: t.id, label: t.name }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="field-label">金额 <span className="field-hint">正=加,负=减</span></label>
|
||||
@@ -372,10 +376,14 @@ export function AdminQuotaPage({ notify }: { notify: Notify }) {
|
||||
<p className="admin-modal-desc">各上限留空 = 该维度不限。任一上限触发即拦截生成(在原余额管控之上叠加)。</p>
|
||||
<div className="field">
|
||||
<label className="field-label">团队 <span className="req">*</span></label>
|
||||
<select className="select" value={editing.team} disabled={Boolean(editing.id)} onChange={(e) => setEditing((p) => ({ ...p, team: e.target.value }))}>
|
||||
<option value="">选择团队…</option>
|
||||
{teams.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}
|
||||
</select>
|
||||
<CustomSelect
|
||||
fill
|
||||
value={editing.team}
|
||||
disabled={Boolean(editing.id)}
|
||||
placeholder="选择团队…"
|
||||
onChange={(next) => setEditing((p) => ({ ...p, team: next }))}
|
||||
options={teams.map((t) => ({ value: t.id, label: t.name }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field-row">
|
||||
<div className="field">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Server, X } from "lucide-react";
|
||||
import { adminApi } from "../../api";
|
||||
import { Pager } from "../../components/pager";
|
||||
import { SystemLoading } from "../../components/loading";
|
||||
import { CustomSelect } from "../../components/custom-select";
|
||||
import type { AdminModel, AdminProvider } from "../../types";
|
||||
import { pts } from "../stage-config";
|
||||
|
||||
@@ -229,10 +230,14 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
|
||||
<div className="modal-b">
|
||||
<div className="field">
|
||||
<label className="field-label">供应商 <span className="req">*</span></label>
|
||||
<select className="select" value={modelModal.provider} disabled={Boolean(modelModal.id)} onChange={(e) => setModelModal((m) => m && ({ ...m, provider: e.target.value }))}>
|
||||
<option value="">选择供应商…</option>
|
||||
{providers.map((p) => <option key={p.id} value={p.id}>{p.display_name}</option>)}
|
||||
</select>
|
||||
<CustomSelect
|
||||
fill
|
||||
value={modelModal.provider}
|
||||
disabled={Boolean(modelModal.id)}
|
||||
placeholder="选择供应商…"
|
||||
onChange={(next) => setModelModal((m) => m && ({ ...m, provider: next }))}
|
||||
options={providers.map((p) => ({ value: p.id, label: p.display_name }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="field-row">
|
||||
<div className="field">
|
||||
@@ -241,9 +246,13 @@ export function AdminModelsPage({ notify }: { notify: Notify }) {
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="field-label">能力</label>
|
||||
<select className="select" value={modelModal.capability} disabled={Boolean(modelModal.id)} onChange={(e) => setModelModal((m) => m && ({ ...m, capability: e.target.value }))}>
|
||||
{CAPABILITIES.map((c) => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
<CustomSelect
|
||||
fill
|
||||
value={modelModal.capability}
|
||||
disabled={Boolean(modelModal.id)}
|
||||
onChange={(next) => setModelModal((m) => m && ({ ...m, capability: next }))}
|
||||
options={CAPABILITIES.map((c) => ({ value: c, label: c }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ConfirmModal, MediaLightbox, SuccessModal } from "../components/overlay
|
||||
import { useFileDrop } from "../components/use-file-drop";
|
||||
import { ReviewBadge, type ReviewStatus } from "../components/review-badge";
|
||||
import { ProductCreateDrawer } from "../components/product-create-drawer";
|
||||
import { CustomSelect } from "../components/custom-select";
|
||||
import {
|
||||
BUSINESS_TYPES,
|
||||
BUSINESS_TYPE_KEYS,
|
||||
@@ -527,15 +528,16 @@ export function ProductCreateUploadPage({ onCreate, onBack }: { onCreate: (paylo
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="field-label">品类<span className="req">*</span></label>
|
||||
<select className="select" value={category} onChange={(event) => setCategory(event.target.value)} required>
|
||||
<option value="">— 选择品类 —</option>
|
||||
<optgroup label="常用品类">
|
||||
{PC_CAT_PRIMARY.map((option) => <option key={option}>{option}</option>)}
|
||||
</optgroup>
|
||||
<optgroup label="更多品类">
|
||||
{PC_CAT_MORE.map((option) => <option key={option}>{option}</option>)}
|
||||
</optgroup>
|
||||
</select>
|
||||
<CustomSelect
|
||||
fill
|
||||
value={category}
|
||||
onChange={setCategory}
|
||||
placeholder="— 选择品类 —"
|
||||
options={[
|
||||
...PC_CAT_PRIMARY.map((option) => ({ value: option, label: option, group: "常用品类" })),
|
||||
...PC_CAT_MORE.map((option) => ({ value: option, label: option, group: "更多品类" })),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="field field-last">
|
||||
<label className="field-label">价格(元)</label>
|
||||
@@ -1069,23 +1071,31 @@ export function ProductDetailPage({ product, projects, initialTab = "assets", na
|
||||
<div className="k">类目</div>
|
||||
<div className="v">
|
||||
<span className="v-static">{BUSINESS_TYPES[realType]}</span>
|
||||
<select className="v-edit v-select" value={bizType} onChange={(event) => {
|
||||
const next = event.target.value as BusinessType;
|
||||
setBizType(next);
|
||||
const opts = pdCatChoices(next, "");
|
||||
if (!opts.includes(cat)) setCat(opts[0]);
|
||||
}}>
|
||||
{BUSINESS_TYPE_KEYS.map((key) => <option key={key} value={key}>{BUSINESS_TYPES[key]}</option>)}
|
||||
</select>
|
||||
<CustomSelect
|
||||
fill
|
||||
className="v-edit"
|
||||
value={bizType}
|
||||
onChange={(next) => {
|
||||
const typed = next as BusinessType;
|
||||
setBizType(typed);
|
||||
const opts = pdCatChoices(typed, "");
|
||||
if (!opts.includes(cat)) setCat(opts[0]);
|
||||
}}
|
||||
options={BUSINESS_TYPE_KEYS.map((key) => ({ value: key, label: BUSINESS_TYPES[key] }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row" data-field="cat">
|
||||
<div className="k">品类</div>
|
||||
<div className="v">
|
||||
<span className="v-static">{realCat}</span>
|
||||
<select className="v-edit v-select" value={cat} onChange={(event) => setCat(event.target.value)}>
|
||||
{pdCatChoices(bizType, cat).map((option) => <option key={option}>{option}</option>)}
|
||||
</select>
|
||||
<CustomSelect
|
||||
fill
|
||||
className="v-edit"
|
||||
value={cat}
|
||||
onChange={setCat}
|
||||
options={pdCatChoices(bizType, cat).map((option) => ({ value: option, label: option }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row" data-field="bullets">
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Asset, Product, Project, ScriptTemplate } from "../types";
|
||||
import { api } from "../api";
|
||||
import type { Page } from "./route-config";
|
||||
import { ConfirmModal, MediaLightbox } from "../components/overlays";
|
||||
import { CustomSelect } from "../components/custom-select";
|
||||
import { ProductCreateDrawer, type ProductCreatePayload } from "../components/product-create-drawer";
|
||||
import { isLocalLife } from "../product-business";
|
||||
import { Pager } from "../components/pager";
|
||||
@@ -325,12 +326,19 @@ export function ProjectWizardPage({ products, projects = [], preselectProductId,
|
||||
{templates.length > 0 && (
|
||||
<label className="nw-field">
|
||||
<span>套路模板<small>可选</small></span>
|
||||
<select value={templateId} onChange={(event) => setTemplateId(event.target.value)}>
|
||||
<option value="">不套用 · 让 AI 按这个商品从头设计</option>
|
||||
{templates.map((item) => (
|
||||
<option key={item.id} value={item.id}>{item.name} · {item.total_duration}s{item.usage_count ? ` · 用过 ${item.usage_count} 次` : ""}</option>
|
||||
))}
|
||||
</select>
|
||||
<CustomSelect
|
||||
fill
|
||||
value={templateId}
|
||||
onChange={setTemplateId}
|
||||
placeholder="不套用 · 让 AI 按这个商品从头设计"
|
||||
options={[
|
||||
{ value: "", label: "不套用 · 让 AI 按这个商品从头设计" },
|
||||
...templates.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.name} · ${item.total_duration}s${item.usage_count ? ` · 用过 ${item.usage_count} 次` : ""}`,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
{template && (
|
||||
<div className="nw-tpl">
|
||||
<strong>同套路重出</strong>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { api, ApiError, type QuickCreateJob } from "../api";
|
||||
import { ConfirmModal, OverlayPortal, useBodyScrollLock } from "../components/overlays";
|
||||
import { CustomSelect } from "../components/custom-select";
|
||||
import { useFileDrop } from "../components/use-file-drop";
|
||||
import { forgetQuickCreateJob, readQuickCreateJobId, rememberQuickCreateJob, stripQuickCreateSuffix } from "../quick-create-lock";
|
||||
import type { ModelConfig } from "../types";
|
||||
@@ -678,33 +679,44 @@ export function QuickCreatePage({
|
||||
<div className="quick-parameter-grid" aria-label="视频核心参数">
|
||||
<label className="quick-parameter-field">
|
||||
<span>视频比例</span>
|
||||
<select value={aspectRatio} onChange={(event) => setAspectRatio(event.target.value)} disabled={isGenerating}>
|
||||
{QUICK_RATIOS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
|
||||
</select>
|
||||
<CustomSelect fill value={aspectRatio} onChange={setAspectRatio} disabled={isGenerating} options={QUICK_RATIOS} />
|
||||
</label>
|
||||
<label className="quick-parameter-field">
|
||||
<span>分辨率</span>
|
||||
<select value={resolution} onChange={(event) => setResolution(event.target.value)} disabled={isGenerating}>
|
||||
{QUICK_RESOLUTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value} disabled={Boolean(supportedResolutions.length && !supportedResolutions.includes(option.value))}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<CustomSelect
|
||||
fill
|
||||
value={resolution}
|
||||
onChange={setResolution}
|
||||
disabled={isGenerating}
|
||||
options={QUICK_RESOLUTIONS.map((option) => ({
|
||||
...option,
|
||||
disabled: Boolean(supportedResolutions.length && !supportedResolutions.includes(option.value)),
|
||||
}))}
|
||||
/>
|
||||
</label>
|
||||
<label className="quick-parameter-field">
|
||||
<span>视频时长</span>
|
||||
<select value={totalDuration} onChange={(event) => setTotalDuration(Number(event.target.value))} disabled={isGenerating}>
|
||||
{QUICK_DURATIONS.map((duration) => <option key={duration} value={duration}>{duration / 15} 场({duration}s)</option>)}
|
||||
</select>
|
||||
<CustomSelect
|
||||
fill
|
||||
value={String(totalDuration)}
|
||||
onChange={(next) => setTotalDuration(Number(next))}
|
||||
disabled={isGenerating}
|
||||
options={QUICK_DURATIONS.map((duration) => ({ value: String(duration), label: `${duration / 15} 场(${duration}s)` }))}
|
||||
/>
|
||||
</label>
|
||||
<label className="quick-parameter-field">
|
||||
<span>视频模型</span>
|
||||
<select value={videoModelId} onChange={(event) => setVideoModelId(event.target.value)} disabled={isGenerating || !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
|
||||
value={videoModelId}
|
||||
onChange={setVideoModelId}
|
||||
disabled={isGenerating || !videoConfigs.length}
|
||||
placeholder="暂无可用视频模型"
|
||||
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>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { LoginSession, Team, User, UserPreference } from "../types";
|
||||
import { TeamModal } from "../components/overlays";
|
||||
import { CustomSelect } from "../components/custom-select";
|
||||
import { useFileDrop } from "../components/use-file-drop";
|
||||
|
||||
type SectionKey = "profile" | "security" | "notify" | "pref" | "display";
|
||||
@@ -620,23 +621,23 @@ export function SettingsPage({
|
||||
<div className="form-row">
|
||||
<div className="lbl">默认 BGM 库</div>
|
||||
<div className="val">
|
||||
<select className="select" value={draft.bgm} onChange={(event) => patchDraft("bgm", event.target.value)}>
|
||||
<option value="kapian">抖音 Top10 卡点曲库</option>
|
||||
<option value="emotion">情绪向 · 治愈/悬念</option>
|
||||
<option value="urban">都市电子 · 通勤场景</option>
|
||||
<option value="none">无 BGM</option>
|
||||
</select>
|
||||
<CustomSelect fill value={draft.bgm} onChange={(next) => patchDraft("bgm", next)} options={[
|
||||
{ value: "kapian", label: "抖音 Top10 卡点曲库" },
|
||||
{ value: "emotion", label: "情绪向 · 治愈/悬念" },
|
||||
{ value: "urban", label: "都市电子 · 通勤场景" },
|
||||
{ value: "none", label: "无 BGM" },
|
||||
]} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">默认转场</div>
|
||||
<div className="val">
|
||||
<select className="select" value={draft.transition} onChange={(event) => patchDraft("transition", event.target.value)}>
|
||||
<option value="none">无转场</option>
|
||||
<option value="fade">淡入淡出 · 0.3s</option>
|
||||
<option value="slide">滑动 · 0.3s</option>
|
||||
<option value="zoom">缩放 · 0.3s</option>
|
||||
</select>
|
||||
<CustomSelect fill value={draft.transition} onChange={(next) => patchDraft("transition", next)} options={[
|
||||
{ value: "none", label: "无转场" },
|
||||
{ value: "fade", label: "淡入淡出 · 0.3s" },
|
||||
{ value: "slide", label: "滑动 · 0.3s" },
|
||||
{ value: "zoom", label: "缩放 · 0.3s" },
|
||||
]} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
@@ -658,30 +659,30 @@ export function SettingsPage({
|
||||
<div className="form-row">
|
||||
<div className="lbl">外观</div>
|
||||
<div className="val">
|
||||
<select className="select" value={draft.appearance} onChange={(event) => patchDraft("appearance", event.target.value)}>
|
||||
<option value="system">跟随系统</option>
|
||||
<option value="light">浅色</option>
|
||||
<option value="dark" disabled>深色(V2)</option>
|
||||
</select>
|
||||
<CustomSelect fill value={draft.appearance} onChange={(next) => patchDraft("appearance", next)} options={[
|
||||
{ value: "system", label: "跟随系统" },
|
||||
{ value: "light", label: "浅色" },
|
||||
{ value: "dark", label: "深色(V2)", disabled: true },
|
||||
]} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">语言</div>
|
||||
<div className="val">
|
||||
<select className="select" value={draft.language} onChange={(event) => patchDraft("language", event.target.value)}>
|
||||
<option value="zh">简体中文</option>
|
||||
<option value="en" disabled>English(V2)</option>
|
||||
</select>
|
||||
<CustomSelect fill value={draft.language} onChange={(next) => patchDraft("language", next)} options={[
|
||||
{ value: "zh", label: "简体中文" },
|
||||
{ value: "en", label: "English(V2)", disabled: true },
|
||||
]} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="lbl">表格密度</div>
|
||||
<div className="val">
|
||||
<select className="select" value={draft.density} onChange={(event) => patchDraft("density", event.target.value)}>
|
||||
<option value="compact">紧凑</option>
|
||||
<option value="standard">标准</option>
|
||||
<option value="loose">宽松</option>
|
||||
</select>
|
||||
<CustomSelect fill value={draft.density} onChange={(next) => patchDraft("density", next)} options={[
|
||||
{ value: "compact", label: "紧凑" },
|
||||
{ value: "standard", label: "标准" },
|
||||
{ value: "loose", label: "宽松" },
|
||||
]} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { BillingSummary, BillingTrend, Invitation, Notification, Team, Team
|
||||
import type { Page } from "./route-config";
|
||||
import { money } from "./stage-config";
|
||||
import { ConfirmModal, TeamModal } from "../components/overlays";
|
||||
import { CustomSelect } from "../components/custom-select";
|
||||
import { Pager } from "../components/pager";
|
||||
|
||||
const MEMBERS_PER_PAGE = 10;
|
||||
@@ -608,10 +609,15 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
|
||||
<>
|
||||
<div className="field">
|
||||
<label className="field-label">成员角色</label>
|
||||
<select className="input" value={invRole} onChange={(e) => setInvRole(e.target.value)}>
|
||||
<option value="member">成员 · 可用生成,不可管理团队</option>
|
||||
<option value="admin">团队管理员 · 可管成员/额度/邀请</option>
|
||||
</select>
|
||||
<CustomSelect
|
||||
fill
|
||||
value={invRole}
|
||||
onChange={setInvRole}
|
||||
options={[
|
||||
{ value: "member", label: "成员 · 可用生成,不可管理团队" },
|
||||
{ value: "admin", label: "团队管理员 · 可管成员/额度/邀请" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="field" style={{ marginBottom: 0 }}>
|
||||
<label className="field-label">月限额(积分)<span className="lbl-note">(-1 为不限)</span></label>
|
||||
|
||||
Reference in New Issue
Block a user