feat: refine asset media and project setup flows

This commit is contained in:
iye
2026-06-01 13:42:52 +08:00
parent e06a16e200
commit 2ba1058329
2 changed files with 552 additions and 68 deletions
+36 -65
View File
@@ -442,8 +442,16 @@
/* ── shared field styles ── */
.field { display: block; margin-bottom: 16px; }
.config-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: end; margin-bottom: 16px; }
.config-row.single { grid-template-columns: minmax(0, 1fr); }
.config-row .field { margin-bottom: 0; }
.duration-select { cursor: pointer; }
.duration-card-row { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.duration-card { width: 100%; min-height: 108px; text-align: left; font-family: inherit; }
.duration-card .duration-title { font-size: 14px; font-weight: 600; color: var(--accent-black); }
.duration-card .duration-meta { margin-top: 5px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.duration-card .duration-note { margin-top: auto; padding-top: 10px; font-size: 11.5px; color: var(--black-alpha-56); line-height: 1.45; }
.duration-card.selected .duration-title,
.duration-card.selected .duration-meta { color: var(--heat); }
@media (max-width: 1100px) { .duration-card-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 900px) { .config-row { grid-template-columns: 1fr; } }
.field-label { display: block; font-size: 12.5px; color: var(--black-alpha-56); font-weight: 500; margin-bottom: 6px; }
.field-label .req { color: var(--heat); margin-left: 2px; }
@@ -654,10 +662,10 @@
];
const DURATIONS = [
{ id: '0-10', label: '0-10 秒', shots: [3, 4], tag: '黄金完播', completion: 52, conversion: 1.6 },
{ id: '0-15', label: '0-15 秒', shots: [4, 5], tag: '完播率最佳', completion: 42, conversion: 1.8 },
{ id: '0-30', label: '0-30 秒', shots: [6, 8], tag: '卖点详解', completion: 32, conversion: 2.1 },
{ id: '0-60', label: '0-60 秒', shots: [10, 12], tag: '故事化', completion: 26, conversion: 2.4 },
{ id: '0-10', label: '0-10 秒', shots: [3, 4], tag: '快速种草' },
{ id: '0-15', label: '0-15 秒', shots: [4, 5], tag: '标准短片' },
{ id: '0-30', label: '0-30 秒', shots: [6, 8], tag: '卖点展开' },
{ id: '0-60', label: '0-60 秒', shots: [10, 12], tag: '故事化' },
];
const STYLES = [
@@ -775,7 +783,7 @@
if (s.id === 'manual') return state.manualScript.trim().length >= 20;
return true;
}
function canPass3() { return state.projectName.trim().length >= 2; }
function canPass3() { return state.projectName.trim().length >= 2 && !!state.duration; }
function canFinish() { return canPass1() && canPass2() && canPass3() && state.agreed && balanceAfter() >= 5; }
/* ---------- icons ---------- */
@@ -839,8 +847,8 @@
}
function startGenerate() {
const p = getProduct(), d = getDuration(), st = getStyle();
if (!p || !d || !st || state.projectName.trim().length < 2) return;
const p = getProduct(), d = getDuration();
if (!p || !d || state.projectName.trim().length < 2) return;
// 持久化项目, 让 projects.html 下次加载时自动 prepend 到列表
try {
const seconds = (d.id.split('-')[1] || '15');
@@ -1001,11 +1009,11 @@
============================================================ */
function railConfig() {
const p = getProduct(), du = getDuration(), st = getStyle();
const cfgReady = !!(du && st);
const p = getProduct(), du = getDuration();
const cfgReady = !!du && canPass3();
return [
{ n: 1, label: '选择商品', desc: p ? p.name : '未选择', done: canPass1() },
{ n: 2, label: '项目配置', desc: cfgReady ? (du.label + ' · ' + st.name) : '时长 · 风格 · 人物', done: cfgReady && canPass3() },
{ n: 2, label: '项目配置', desc: cfgReady ? du.label : '项目名 · 视频时长', done: cfgReady },
];
}
@@ -1221,67 +1229,31 @@
}
function renderStep3() {
const personaObj = getPersona(), durObj = getDuration(), styleObj = getStyle();
let showReco = false, recoDur = null, recoStyle = null;
if (personaObj && state.duration && state.scriptStyle) {
const recoMismatch = personaObj.defaults.duration !== state.duration || personaObj.defaults.style !== state.scriptStyle;
showReco = recoMismatch && !state.recoDismissed;
recoDur = DURATIONS.find(d => d.id === personaObj.defaults.duration);
recoStyle = STYLES.find(s => s.id === personaObj.defaults.style);
}
return `<div class="wiz-pane active" data-step="2">
<div class="wiz-step-h">
<h2>第 2 步 · 项目配置</h2>
<p>这些设置会影响 LLM 生成脚本的方向,确认后会进入流水线第 1 步(脚本生成)。</p>
<p>基础配置只保留项目名和成片时长。脚本来源、风格和人物设定已移到流水线第 1 步脚本助手引导。</p>
</div>
<div class="config-row">
<div class="config-row single">
<div class="field">
<label class="field-label">项目名称<span class="req">*</span></label>
<input class="input" value="${esc(state.projectName)}" oninput="_wiz.setName(this.value)">
</div>
<div class="field">
<label class="field-label">视频时长<span class="req">*</span></label>
<select class="input duration-select" onchange="_wiz.setDur(this.value)">
<option value="" disabled ${state.duration ? '' : 'selected'}>选择时长</option>
${DURATIONS.map(d => `<option value="${esc(d.id)}" ${state.duration === d.id ? 'selected' : ''}>${esc(d.label)} · ${d.shots[0]}-${d.shots[1]} 镜</option>`).join('')}
</select>
</div>
</div>
<div class="field">
<label class="field-label">脚本风格</label>
<div class="opt-row cols-4">
${STYLES.map(s => `<div class="opt-card${state.scriptStyle === s.id ? ' selected' : ''}" onclick="_wiz.setStyle('${s.id}')">
<h4>${esc(s.name)}</h4>
<div class="note">${esc(s.note)}</div>
${s.tag ? `<span class="badge">[ ${esc(s.tag)} ]</span>` : ''}
</div>`).join('')}
<label class="field-label">视频时长<span class="req">*</span></label>
<div class="opt-row duration-card-row">
${DURATIONS.map(d => `<button class="opt-card duration-card${state.duration === d.id ? ' selected' : ''}" type="button" data-duration="${esc(d.id)}" aria-pressed="${state.duration === d.id ? 'true' : 'false'}" onclick="_wiz.setDur('${esc(d.id)}')">
<span class="badge">[ ${esc(d.tag)} ]</span>
<span class="duration-title">${esc(d.label)}</span>
<span class="duration-meta">${d.shots[0]}-${d.shots[1]} 镜 · 9:16</span>
<span class="duration-note">适合${d.id === '0-10' ? '快速种草' : d.id === '0-15' ? '短平快投放' : d.id === '0-30' ? '卖点展开' : '故事化表达'}</span>
</button>`).join('')}
</div>
</div>
<div class="field">
<label class="field-label">人物设定</label>
<div class="opt-row cols-6">
${PERSONAS.map(p => `<div class="opt-card${state.persona === p.id ? ' selected' : ''}" onclick="_wiz.setPersona('${p.id}')">
<h4>${esc(p.name)}</h4>
<div class="sub">${esc(p.sub)}</div>
<div class="metric"><span class="val">${esc(p.metric)}</span></div>
</div>`).join('')}
</div>
${showReco ? `<div class="reco-bubble">
<span class="ic">${ICONS.bulb}</span>
<div class="txt">
<span>抖音同人设 TOP 视频更常用 <strong>${esc(recoDur.label)}</strong> + <strong>${esc(recoStyle.name)}</strong></span>
<span class="meta">当前 ${esc(durObj.label)} · ${esc(styleObj.name)} → 推荐换为同人设最优组合</span>
</div>
<button class="btn-apply" onclick="_wiz.applyPreset()">一键套用</button>
<button class="dismiss" onclick="_wiz.dismissReco()" aria-label="忽略">${ICONS.x}</button>
</div>` : ''}
</div>
${Object.keys(state.points).length > 0 ? `<div class="field" style="margin-bottom: 0;">
<label class="field-label">关键卖点(可勾选要重点突出的)</label>
<div class="theme-pill-row">
@@ -1292,7 +1264,7 @@
}
function renderStep4() {
const p = getProduct(), s = getSource(), personaObj = getPersona(), durObj = getDuration(), styleObj = getStyle();
const p = getProduct(), s = getSource(), durObj = getDuration();
const c = getCost();
const ba = balanceAfter();
const low = ba < 5;
@@ -1331,7 +1303,7 @@
<div class="cc-h"><span>// 项目配置</span><button class="cc-edit" onclick="_wiz.jumpTo(3)">修改</button></div>
<div class="cc-body">
<div style="font-weight:600; font-size:13px;">${esc(state.projectName)}</div>
<div class="ln"><b>${esc(styleObj.name)}</b> · ${esc(personaObj.name)} · ${esc(personaObj.sub)}</div>
<div class="ln"><b>${esc(durObj.label)}</b> · ${durObj.shots[0]}-${durObj.shots[1]} 镜 · 9:16</div>
<div class="ln" style="font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48);">卖点:${esc(pointsList)}</div>
</div>
</div>
@@ -1340,8 +1312,7 @@
<div class="cc-h"><span>// 输出参数</span></div>
<div class="cc-body">
<div class="ln"><b>${esc(durObj.label)}</b> · <b>${durObj.shots[0]}-${durObj.shots[1]} 镜</b> · 9:16</div>
<div class="ln">预估完播 <b>${durObj.completion}%</b> · 预估转化 <b>${durObj.conversion}%</b></div>
<div class="ln" style="font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48);">// 数据来源:抖音同品类 TOP 均值</div>
<div class="ln" style="font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48);">// 进入 Stage 1 后由脚本助手确认风格与人物设定</div>
</div>
</div>
</div>
@@ -1516,8 +1487,8 @@
<div class="pv-title">${esc(title)}</div>
<div class="pv-metrics">
<div class="pv-metric"><div class="l">镜头</div><div class="v">${shots}<small>镜</small></div></div>
<div class="pv-metric accent"><div class="l">预估完播</div><div class="v">${durObj.completion}<small>%</small></div></div>
<div class="pv-metric"><div class="l">预估转化</div><div class="v">${durObj.conversion}<small>%</small></div></div>
<div class="pv-metric accent"><div class="l">时长</div><div class="v">${esc(durObj.label)}</div></div>
<div class="pv-metric"><div class="l">比例</div><div class="v">9:16</div></div>
<div class="pv-metric"><div class="l">预估成本</div><div class="v">¥${c.total.toFixed(2)}</div></div>
</div>
${summaryBlock}
@@ -1573,8 +1544,8 @@
renderRail();
const body = $('#wiz-body');
// 单页式: 商品 (step1) + 项目配置 (原 step3, 现 step2),底部「开始」CTA
const p = getProduct(), du = getDuration(), st = getStyle();
const canStart = !!(p && du && st && state.projectName.trim().length >= 2);
const p = getProduct(), du = getDuration();
const canStart = !!(p && du && state.projectName.trim().length >= 2);
let html = '';
html += '<section id="step-pane-1" class="step-pane-wrap">' + renderStep1() + '</section>';
html += '<section id="step-pane-2" class="step-pane-wrap">' + renderStep3() + '</section>';