feat: refine asset media and project setup flows
This commit is contained in:
+36
-65
@@ -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>';
|
||||
|
||||
Reference in New Issue
Block a user