All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
页面 (电商AI平台/) - account / team / settings / index / products / projects: 累积迭代 - restraint.css: 设计 token 补充 - login.html / register.html: 新增登录注册页 - _ARCHIVE.md: 归档说明 Next.js 工程骨架 - app/ + components/: 新一代 SPA 雏形 (page / layout / sidebar / topbar / GridBg / Icon) - package.json / package-lock.json / next.config.mjs / tsconfig.json / postcss.config.mjs / next-env.d.ts 历史归档 / 文档 - v1/: 原 V1 静态稿镜像 (含 mockup-A/B/C) - PRD.md / deployment-guide.md / _check.html - ui参考/ / screenshots/ 杂项 - .gitignore 调整 - 删除根 README.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
984 lines
63 KiB
HTML
984 lines
63 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>新建项目 · 流·Studio</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="assets/restraint.css">
|
||
<style>
|
||
.wizard { display: grid; grid-template-columns: 200px minmax(0, 1fr) 300px; gap: 36px; align-items: start; max-width: 1400px; }
|
||
@media (max-width: 1180px) { .wizard { grid-template-columns: 200px minmax(0, 1fr); } .wiz-preview { display: none; } }
|
||
.steps { position: sticky; top: 24px; align-self: start; }
|
||
.step { display: flex; gap: 12px; padding: 12px 0; position: relative; }
|
||
.step:not(:last-child)::after { content: ''; position: absolute; left: 11px; top: 36px; width: 1px; height: calc(100% - 24px); background: var(--border-faint); }
|
||
.step .num { width: 24px; height: 24px; border: 1px solid var(--border-faint); border-radius: var(--r-sm); background: var(--surface); display: grid; place-items: center; font-size: 11px; font-weight: 600; color: var(--black-alpha-48); flex-shrink: 0; z-index: 1; font-family: var(--font-mono); }
|
||
.step.done .num { background: var(--accent-black); border-color: var(--accent-black); color: var(--accent-white); }
|
||
.step.active .num { background: var(--heat); border-color: var(--heat); color: var(--accent-white); }
|
||
.step .label { font-size: 13.5px; font-weight: 500; color: var(--black-alpha-56); padding-top: 2px; }
|
||
.step .desc { font-size: 11.5px; color: var(--black-alpha-48); padding-top: 3px; line-height: 1.4; font-family: var(--font-mono); letter-spacing: .02em; }
|
||
.step.active .label { color: var(--accent-black); font-weight: 600; }
|
||
.step.done .label { color: var(--black-alpha-56); }
|
||
.step.done:not(:last-child)::after { background: var(--accent-black); }
|
||
.step.clickable { cursor: pointer; }
|
||
.step.clickable:hover .label { color: var(--heat); }
|
||
.step.clickable:hover .num { border-color: var(--heat); }
|
||
|
||
.wiz-pane { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 22px 24px; margin-bottom: 14px; }
|
||
.wiz-pane.active { padding: 26px 28px; position: relative; }
|
||
.wiz-pane.active::before, .wiz-pane.active::after { content: ''; position: absolute; width: 14px; height: 14px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-size: contain; pointer-events: none; }
|
||
.wiz-pane.active::before { top: -7px; left: -7px; }
|
||
.wiz-pane.active::after { bottom: -7px; right: -7px; }
|
||
.wiz-pane.collapsed { padding: 16px 20px; }
|
||
.wiz-pane-h { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
|
||
.wiz-pane-h h3 { font-size: 14px; font-weight: 600; }
|
||
.wiz-step-h { margin-bottom: 18px; }
|
||
.wiz-step-h h2 { font-size: 20px; font-weight: 600; letter-spacing: -.015em; }
|
||
.wiz-step-h p { font-size: 13px; color: var(--black-alpha-56); margin-top: 6px; }
|
||
|
||
.opt-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
||
.opt-row.cols-4 { grid-template-columns: repeat(4, 1fr); }
|
||
.opt-row.cols-6 { grid-template-columns: repeat(3, 1fr); }
|
||
@media (min-width: 1280px) { .opt-row.cols-6 { grid-template-columns: repeat(6, 1fr); } }
|
||
.opt-card { border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 14px; background: var(--surface); cursor: pointer; position: relative; display: flex; flex-direction: column; min-width: 0; transition: background var(--t-base), border-color var(--t-base); }
|
||
.opt-card:hover { background: var(--background-lighter); }
|
||
.opt-card.selected { border-color: var(--heat); background: var(--heat-12); }
|
||
.opt-card.selected::after { content: ''; position: absolute; top: 8px; right: 10px; width: 16px; height: 16px; background-color: var(--heat); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: 10px 10px; border-radius: var(--r-sm); }
|
||
.opt-card h4 { font-size: 13px; font-weight: 600; }
|
||
.opt-card .sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-top: 3px; letter-spacing: .02em; }
|
||
.opt-card .note { font-size: 11.5px; color: var(--black-alpha-56); margin-top: 6px; line-height: 1.5; }
|
||
.opt-card .metric { margin-top: auto; padding-top: 10px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||
.opt-card .metric .val { color: var(--accent-black); font-weight: 500; }
|
||
.opt-card.selected .metric .val { color: var(--heat); }
|
||
.opt-card .badge { font-family: var(--font-mono); font-size: 9.5px; padding: 1px 6px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-48); display: inline-block; margin-top: 8px; letter-spacing: .04em; align-self: flex-start; }
|
||
.opt-card.selected .badge { color: var(--heat); border-color: var(--heat-20); }
|
||
|
||
.theme-pill { display: inline-flex; gap: 4px; align-items: center; height: 28px; padding: 0 12px; border: 1px solid var(--border-faint); border-radius: 999px; background: var(--surface); font-size: 12.5px; cursor: pointer; color: var(--black-alpha-56); transition: background var(--t-base), border-color var(--t-base); }
|
||
.theme-pill:hover { background: var(--background-lighter); }
|
||
.theme-pill.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); font-weight: 600; }
|
||
.theme-pill svg { width: 12px; height: 12px; }
|
||
|
||
.reco-bubble { position: relative; margin-top: 10px; padding: 10px 14px; background: var(--heat-12); border: 1px solid var(--heat-20); border-radius: var(--r-md); display: flex; align-items: center; gap: 12px; font-size: 12.5px; color: var(--accent-black); }
|
||
.reco-bubble::before { content: ''; position: absolute; top: -5px; left: 28px; width: 9px; height: 9px; background: var(--heat-12); border-left: 1px solid var(--heat-20); border-top: 1px solid var(--heat-20); transform: rotate(45deg); }
|
||
.reco-bubble .ic { color: var(--heat); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; }
|
||
.reco-bubble .ic svg, .reco-bubble .dismiss svg { display: block; }
|
||
.reco-bubble .txt { flex: 1; line-height: 1.5; }
|
||
.reco-bubble .txt strong { color: var(--heat); font-weight: 600; }
|
||
.reco-bubble .txt .meta { display: block; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); margin-top: 2px; letter-spacing: .02em; }
|
||
.reco-bubble .btn-apply { height: 28px; padding: 0 12px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: var(--r-md); font-size: 12px; font-weight: 600; cursor: pointer; flex-shrink: 0; box-shadow: var(--shadow-cta); transition: box-shadow var(--t-base); }
|
||
.reco-bubble .btn-apply:hover { box-shadow: var(--shadow-cta-hover); }
|
||
.reco-bubble .dismiss { background: transparent; color: var(--black-alpha-48); border: 0; width: 24px; height: 24px; padding: 0; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
|
||
.reco-bubble .dismiss:hover { color: var(--accent-black); }
|
||
|
||
.wiz-foot { display: flex; justify-content: space-between; align-items: center; margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--border-faint); }
|
||
.btn:disabled, .btn.disabled { opacity: .45; cursor: not-allowed; pointer-events: none; }
|
||
|
||
/* ── pick toolbar (Step 1) ── */
|
||
.pick-toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; }
|
||
.pick-toolbar .search-input { position: relative; flex: 1; max-width: 320px; min-width: 200px; }
|
||
.pick-toolbar .search-input svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--black-alpha-48); width: 14px; height: 14px; }
|
||
.pick-toolbar .search-input input { width: 100%; height: 32px; padding: 0 12px 0 34px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); font-size: 12.5px; color: var(--accent-black); font-family: inherit; transition: border-color var(--t-base); }
|
||
.pick-toolbar .search-input input:focus { outline: none; border-color: var(--heat); }
|
||
.cat-chip { height: 32px; padding: 0 12px; border: 1px solid var(--border-faint); background: var(--surface); border-radius: var(--r-md); font-size: 12.5px; color: var(--black-alpha-56); font-weight: 500; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; font-family: inherit; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }
|
||
.cat-chip:hover { background: var(--background-lighter); }
|
||
.cat-chip.active { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
|
||
|
||
.pick-section-h { display: flex; align-items: baseline; gap: 8px; margin: 14px 0 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; text-transform: uppercase; }
|
||
.pick-section-h .count { background: var(--background-lighter); border: 1px solid var(--border-faint); padding: 1px 6px; color: var(--black-alpha-48); font-size: 10px; }
|
||
|
||
/* ── Step 1 · product picker grid ── */
|
||
.product-pick-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
||
@media (max-width: 1100px) { .product-pick-grid { grid-template-columns: repeat(2, 1fr); } }
|
||
.product-pick { display: flex; gap: 12px; padding: 12px; border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); cursor: pointer; position: relative; transition: background var(--t-base), border-color var(--t-base); min-width: 0; }
|
||
.product-pick:hover { background: var(--background-lighter); }
|
||
.product-pick.selected { border-color: var(--heat); background: var(--heat-12); }
|
||
.product-pick.selected::after { content: ''; position: absolute; top: 8px; right: 10px; width: 16px; height: 16px; background-color: var(--heat); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: 10px 10px; border-radius: var(--r-sm); }
|
||
.product-pick .thumb { width: 56px; height: 72px; flex-shrink: 0; }
|
||
.product-pick .body { flex: 1; min-width: 0; padding-right: 18px; }
|
||
.product-pick .name { font-weight: 600; font-size: 13px; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||
.product-pick .meta { margin-top: 4px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||
.product-pick .meta b { color: var(--accent-black); font-weight: 500; }
|
||
.product-pick .tags { margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap; }
|
||
.product-pick .tag-s { font-size: 10.5px; color: var(--black-alpha-56); background: var(--background-lighter); padding: 1px 6px; border: 1px solid var(--border-faint); border-radius: var(--r-sm); }
|
||
.product-pick.selected .tag-s { background: var(--surface); border-color: var(--heat-20); color: var(--heat); }
|
||
|
||
.product-pick.add { display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 8px; border-style: dashed; color: var(--black-alpha-48); min-height: 96px; }
|
||
.product-pick.add:hover { color: var(--heat); border-color: var(--heat); background: var(--heat-12); }
|
||
.product-pick.add .pc { width: 32px; height: 32px; border: 1px solid currentColor; display: grid; place-items: center; border-radius: var(--r-sm); }
|
||
.product-pick.add svg { width: 16px; height: 16px; }
|
||
|
||
/* ── Step 2 · source-type cards ── */
|
||
.source-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
||
.source-card { display: flex; flex-direction: column; gap: 8px; padding: 16px 16px 14px; border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); cursor: pointer; position: relative; transition: background var(--t-base), border-color var(--t-base); min-height: 132px; }
|
||
.source-card:hover { background: var(--background-lighter); }
|
||
.source-card.selected { border-color: var(--heat); background: var(--heat-12); }
|
||
.source-card.selected::after { content: ''; position: absolute; top: 10px; right: 12px; width: 16px; height: 16px; background-color: var(--heat); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: 10px 10px; border-radius: var(--r-sm); }
|
||
.source-card .src-ic { width: 32px; height: 32px; background: var(--background-lighter); color: var(--black-alpha-56); border: 1px solid var(--border-faint); border-radius: var(--r-sm); display: grid; place-items: center; }
|
||
.source-card .src-ic svg { width: 16px; height: 16px; }
|
||
.source-card.selected .src-ic { background: var(--surface); color: var(--heat); border-color: var(--heat-20); }
|
||
.source-card h4 { font-size: 14px; font-weight: 600; }
|
||
.source-card .src-tag { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); letter-spacing: .06em; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); padding: 1px 6px; align-self: flex-start; }
|
||
.source-card.selected .src-tag { color: var(--heat); border-color: var(--heat-20); }
|
||
.source-card .src-desc { font-size: 12px; color: var(--black-alpha-56); line-height: 1.55; margin-top: auto; }
|
||
|
||
.source-detail { margin-top: 16px; padding: 18px 20px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); }
|
||
.source-detail .sd-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; text-transform: uppercase; margin-bottom: 10px; }
|
||
.source-detail .sd-h b { color: var(--accent-black); font-weight: 500; }
|
||
|
||
/* ── shared field styles ── */
|
||
.field { display: block; margin-bottom: 16px; }
|
||
.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; }
|
||
.field-hint { font-size: 11.5px; color: var(--black-alpha-48); margin-top: 4px; }
|
||
.input, .textarea { width: 100%; height: 36px; padding: 0 12px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); font-size: 13px; color: var(--accent-black); font-family: inherit; transition: border-color var(--t-base); }
|
||
.input:focus, .textarea:focus { outline: none; border-color: var(--heat); }
|
||
.textarea { height: auto; padding: 10px 12px; resize: vertical; min-height: 120px; line-height: 1.55; }
|
||
|
||
/* ── Step 4 · confirm grid / billing / balance / eta / tos ── */
|
||
.confirm-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 18px; }
|
||
.confirm-card { position: relative; border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); padding: 14px 16px; }
|
||
.confirm-card .cc-h { display: flex; align-items: center; justify-content: space-between; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; text-transform: uppercase; margin-bottom: 10px; }
|
||
.confirm-card .cc-edit { font-size: 11.5px; color: var(--black-alpha-56); letter-spacing: 0; font-family: var(--font-sans, 'Inter'); text-transform: none; padding: 2px 8px; border: 1px solid var(--border-faint); background: var(--surface); cursor: pointer; border-radius: var(--r-sm); }
|
||
.confirm-card .cc-edit:hover { color: var(--heat); border-color: var(--heat-20); }
|
||
.confirm-card .cc-body { font-size: 13px; color: var(--accent-black); }
|
||
.confirm-card .cc-body .ln { display: flex; align-items: center; gap: 8px; padding: 2px 0; font-size: 12.5px; color: var(--black-alpha-56); flex-wrap: wrap; }
|
||
.confirm-card .cc-body .ln b { color: var(--accent-black); font-weight: 500; }
|
||
|
||
.section-sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; text-transform: uppercase; margin: 18px 0 10px; }
|
||
|
||
.bill-list { border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); overflow: hidden; }
|
||
.bill-row { display: grid; grid-template-columns: 1fr auto 80px; align-items: baseline; gap: 12px; padding: 11px 16px; border-bottom: 1px solid var(--border-faint); }
|
||
.bill-row:last-child { border-bottom: 0; }
|
||
.bill-row .l { font-size: 12.5px; color: var(--accent-black); }
|
||
.bill-row .l .l-sub { color: var(--black-alpha-48); font-size: 11.5px; margin-left: 6px; }
|
||
.bill-row .qty { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; text-align: right; }
|
||
.bill-row .amt { font-family: var(--font-mono); font-size: 12.5px; color: var(--accent-black); font-variant-numeric: tabular-nums; text-align: right; }
|
||
.bill-row.subtotal { background: var(--background-lighter); }
|
||
.bill-row.subtotal .l { color: var(--black-alpha-56); font-size: 12px; }
|
||
.bill-row.total { background: var(--background-lighter); border-top: 1px solid var(--black-alpha-12); }
|
||
.bill-row.total .l { font-weight: 600; font-size: 13px; }
|
||
.bill-row.total .amt { font-size: 16px; font-weight: 600; color: var(--accent-black); }
|
||
.bill-row.total .amt small { font-size: 11px; color: var(--black-alpha-48); font-weight: 500; margin-left: 2px; }
|
||
|
||
.balance-row { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); margin-top: 10px; }
|
||
.balance-row .bl { display: flex; align-items: center; gap: 8px; flex: 1; flex-wrap: wrap; }
|
||
.balance-row .bl svg { width: 14px; height: 14px; color: var(--black-alpha-56); }
|
||
.balance-row .bl .lbl { font-size: 12.5px; color: var(--black-alpha-56); }
|
||
.balance-row .bl .val { font-family: var(--font-mono); font-size: 14px; color: var(--accent-black); font-variant-numeric: tabular-nums; font-weight: 500; }
|
||
.balance-row .bl .arrow { color: var(--black-alpha-48); font-family: var(--font-mono); }
|
||
.balance-row.low .bl .val.after { color: var(--accent-crimson); }
|
||
.balance-row .pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: 11.5px; font-weight: 500; border: 1px solid; white-space: nowrap; margin-left: auto; }
|
||
.balance-row .pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
||
.balance-row .pill.ok { background: var(--forest-bg); border-color: var(--forest-bd); color: var(--accent-forest); }
|
||
.balance-row .pill.err { background: var(--crimson-bg); border-color: var(--crimson-bd); color: var(--accent-crimson); }
|
||
.balance-row .pill.err a { margin-left: 4px; text-decoration: underline; cursor: pointer; }
|
||
|
||
.eta-block { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px; }
|
||
.eta-tile { padding: 14px 16px; border: 1px solid var(--border-faint); border-radius: var(--r-md); background: var(--surface); }
|
||
.eta-tile .lbl { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); letter-spacing: .08em; text-transform: uppercase; margin-bottom: 6px; }
|
||
.eta-tile .v { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
|
||
.eta-tile .v small { font-size: 11px; color: var(--black-alpha-48); font-weight: 500; margin-left: 2px; }
|
||
.eta-tile .desc { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-top: 6px; }
|
||
|
||
/* ── SVG checkbox · per design spec (no CSS hack) ── */
|
||
.check-row { display: flex; align-items: center; gap: 10px; padding: 6px 0; font-size: 12.5px; color: var(--black-alpha-56); cursor: pointer; user-select: none; }
|
||
.check-row:hover .check-box { border-color: var(--black-alpha-56); }
|
||
.check-box { width: 16px; height: 16px; background: var(--surface); border: 1px solid var(--black-alpha-24); flex-shrink: 0; border-radius: var(--r-sm); display: grid; place-items: center; transition: background var(--t-base), border-color var(--t-base); }
|
||
.check-row.on .check-box { background: var(--heat); border-color: var(--heat); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: 11px 11px; }
|
||
.check-row.on .lab { color: var(--accent-black); }
|
||
.check-row .lab b { color: var(--accent-black); font-weight: 500; }
|
||
.check-row .lab .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); margin-left: 6px; letter-spacing: .02em; }
|
||
|
||
.tos-row { display: flex; align-items: center; gap: 10px; padding: 14px 16px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); margin-top: 14px; cursor: pointer; font-size: 12.5px; color: var(--black-alpha-56); user-select: none; }
|
||
.tos-row:hover .check-box { border-color: var(--black-alpha-56); }
|
||
.tos-row.on { background: var(--heat-12); border-color: var(--heat-20); }
|
||
.tos-row.on .check-box { background: var(--heat); border-color: var(--heat); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center; background-size: 11px 11px; }
|
||
.tos-row.on .lab { color: var(--accent-black); }
|
||
.tos-row .lab a { color: var(--heat); text-decoration: underline; cursor: pointer; }
|
||
|
||
/* preview panel */
|
||
.wiz-preview { position: sticky; top: 24px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 18px; }
|
||
.wiz-preview::before, .wiz-preview::after { content: ''; position: absolute; width: 14px; height: 14px; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 22 21' fill='%23e8e8e8'%3E%3Cpath d='M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z'/%3E%3C/svg%3E") no-repeat center; background-size: contain; pointer-events: none; }
|
||
.wiz-preview::before { top: -7px; left: -7px; }
|
||
.wiz-preview::after { bottom: -7px; right: -7px; }
|
||
.pv-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; margin-bottom: 12px; text-transform: uppercase; display: flex; justify-content: space-between; }
|
||
.pv-h .live { display: inline-flex; align-items: center; gap: 5px; color: var(--heat); }
|
||
.pv-h .live::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--heat); animation: pulse 1.6s ease-in-out infinite; }
|
||
@keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: .35 } }
|
||
.pv-title { font-size: 14px; font-weight: 600; line-height: 1.3; margin-bottom: 14px; word-break: break-all; }
|
||
.pv-metrics { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border-faint); border: 1px solid var(--border-faint); margin-bottom: 14px; }
|
||
.pv-metric { padding: 10px 12px; background: var(--surface); }
|
||
.pv-metric .l { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
|
||
.pv-metric .v { font-size: 18px; font-weight: 600; margin-top: 3px; font-variant-numeric: tabular-nums; color: var(--accent-black); }
|
||
.pv-metric .v small { font-size: 11px; color: var(--black-alpha-48); font-weight: 500; }
|
||
.pv-metric.accent .v { color: var(--heat); }
|
||
.pv-section { margin-top: 14px; }
|
||
.pv-section .lbl { font-family: var(--font-mono); font-size: 9.5px; color: var(--black-alpha-48); letter-spacing: .06em; text-transform: uppercase; margin-bottom: 8px; }
|
||
.pv-flow { display: flex; flex-wrap: wrap; gap: 4px 0; font-size: 11.5px; color: var(--black-alpha-56); align-items: center; line-height: 1.7; }
|
||
.pv-flow .node { padding: 2px 7px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--accent-black); font-weight: 500; }
|
||
.pv-flow .arrow { color: var(--heat); margin: 0 5px; display: inline-flex; align-items: center; }
|
||
.pv-flow .arrow svg { display: block; }
|
||
.pv-list { list-style: none; padding: 0; margin: 0; }
|
||
.pv-list li { font-size: 11.5px; color: var(--black-alpha-56); padding: 4px 0; display: flex; align-items: center; gap: 6px; }
|
||
.pv-list li::before { content: ''; width: 11px; height: 11px; flex-shrink: 0; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23FA5D19' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12l5 5L20 6'/%3E%3C/svg%3E") no-repeat center; background-size: contain; }
|
||
.pv-foot { margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--border-faint); font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); display: flex; justify-content: space-between; }
|
||
.pv-foot strong { color: var(--accent-black); font-weight: 500; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="page">
|
||
|
||
<div class="page-head">
|
||
<div>
|
||
<h1>新建项目</h1>
|
||
<div class="sub"><span class="mono">// 商品 → 脚本来源 → 配置 → 确认 · 4 步开始生成</span></div>
|
||
</div>
|
||
<div class="actions">
|
||
<a class="btn btn-ghost" href="projects.html">退出</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wizard">
|
||
<nav class="steps" id="rail"></nav>
|
||
<div id="wiz-body"></div>
|
||
<aside class="wiz-preview" id="preview"></aside>
|
||
</div>
|
||
|
||
</div>
|
||
<script src="assets/shell.js"></script>
|
||
<script src="assets/new-product-drawer.js"></script>
|
||
<script>Shell.render({ active: 'projects', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '视频项目', href: 'projects.html' }, { label: '新建项目' }] });</script>
|
||
|
||
<script>
|
||
/* ============================================================
|
||
新建项目 · 4 步动态向导 (vanilla JS state machine)
|
||
============================================================ */
|
||
(function () {
|
||
'use strict';
|
||
|
||
/* ---------- data ---------- */
|
||
|
||
const PRODUCTS = [
|
||
{ id: 'mask', name: '透真玻尿酸补水面膜', cat: '美妆个护', price: 39.9, imgs: 3, points: ['透明质酸 + B5', '30g 大容量精华', '0 香精 0 酒精'], tags: ['熬夜党', '敏感肌'] },
|
||
{ id: 'earphone', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', price: 199, imgs: 5, points: ['主动降噪', '32 小时续航', 'IP55 防水'], tags: ['通勤', '运动'] },
|
||
{ id: 'noodle', name: '滋啦速食牛肉面 · 6 桶装', cat: '食品饮料', price: 49.9, imgs: 4, points: ['3 分钟出餐', '真材实料牛肉', '0 防腐剂'], tags: ['加班', '独居'] },
|
||
{ id: 'sun', name: '透真清透物理防晒霜', cat: '美妆个护', price: 69, imgs: 4, points: ['SPF50 PA+++', '纯物理防晒', '不泛白不假面'], tags: ['SPF50', '通勤'] },
|
||
{ id: 'coffee', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', price: 89, imgs: 6, points: ['冷热水秒溶', '意式深烘', '24 颗轻便装'], tags: ['提神', '早八'] },
|
||
{ id: 'fryer', name: '小熊 4L 可视空气炸锅', cat: '家居家电', price: 159, imgs: 5, points: ['可视化窗口', '4L 大容量', '低脂少油'], tags: ['小户型', '健康'] },
|
||
{ id: 'yoga', name: '露露同款裸感瑜伽裤', cat: '运动户外', price: 119, imgs: 8, points: ['裸感面料', '高弹回弹', '随心动随心穿'], tags: ['健身房', '通勤'] },
|
||
];
|
||
|
||
const RECENT_IDS = ['mask', 'sun', 'coffee', 'earphone'];
|
||
const CATS = ['全部', '美妆个护', '数码 3C', '食品饮料', '家居家电', '运动户外'];
|
||
|
||
const SOURCES = [
|
||
{ id: 'ai', name: 'AI 全生', tag: '最常用', desc: 'LLM 全权决定脚本走向,最省事。后续仍可在故事板阶段微调。',
|
||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>' },
|
||
{ id: 'theme', name: '一句话主题', tag: '轻引导', desc: '你给一句切入主题,AI 按此扩写。推荐 5–30 字。',
|
||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>' },
|
||
{ id: 'manual', name: '自带脚本', tag: '我已有稿', desc: '粘贴或上传完整脚本,系统按镜头自动切分并适配商品。',
|
||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M9 13h6M9 17h6"/></svg>' },
|
||
];
|
||
|
||
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 },
|
||
];
|
||
|
||
const STYLES = [
|
||
{ id: 'pain', name: '痛点种草', note: '用户痛点切入,以「我懂你」的口吻引出产品。', tag: '最常用', flow: ['痛点', '共鸣', '产品', '效果', '引导'] },
|
||
{ id: 'review', name: '开箱测评', note: '朋友式分享,从开箱到使用感受娓娓道来。', flow: ['开箱', '首印象', '试用', '对比', '结论'] },
|
||
{ id: 'compare', name: '对比展示', note: '「用前 vs 用后 / 同类 vs 本品」直观呈现。', flow: ['对照', '差距', '本品', '数据', '购买'] },
|
||
];
|
||
|
||
const PERSONAS = [
|
||
{ id: 'urban', name: '都市白领女性', sub: '25-30 岁', metric: '大盘消费力', defaults: { duration: '0-15', style: 'pain' } },
|
||
{ id: 'bestie', name: '闺蜜种草', sub: '邻家女孩', metric: '复购最高', defaults: { duration: '0-15', style: 'pain' } },
|
||
{ id: 'ceo', name: '总裁亲选', sub: '创始人 IP', metric: '30 万销额案例', defaults: { duration: '0-30', style: 'pain' } },
|
||
{ id: 'reviewer', name: '专业测评师', sub: '垂类达人', metric: '互动 +30%', defaults: { duration: '0-30', style: 'review' } },
|
||
{ id: 'mom', name: '实用宝妈', sub: '家庭决策者', metric: '母婴/家清稳', defaults: { duration: '0-30', style: 'pain' } },
|
||
{ id: 'genz', name: '学生党', sub: 'Z 世代 18-24', metric: '平价快消', defaults: { duration: '0-10', style: 'compare' } },
|
||
];
|
||
|
||
const USER_EMAIL = 'li@shop.com';
|
||
const ACCOUNT_BALANCE = 327.40;
|
||
|
||
/* ---------- state ---------- */
|
||
|
||
const state = {
|
||
currentStep: 1,
|
||
productId: null,
|
||
pickSearch: '',
|
||
pickCat: '全部',
|
||
sourceId: null,
|
||
themeText: '',
|
||
manualScript: '',
|
||
projectName: '',
|
||
duration: '0-15',
|
||
scriptStyle: 'pain',
|
||
persona: 'urban',
|
||
points: {},
|
||
recoDismissed: false,
|
||
notifyEmail: true,
|
||
notifyWeChat: false,
|
||
agreed: false,
|
||
};
|
||
|
||
/* ---------- helpers ---------- */
|
||
|
||
function $(sel) { return document.querySelector(sel); }
|
||
function esc(s) {
|
||
return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
}
|
||
function getProduct() { return PRODUCTS.find(p => p.id === state.productId) || null; }
|
||
function getSource() { return SOURCES.find(s => s.id === state.sourceId) || null; }
|
||
function getPersona() { return PERSONAS.find(p => p.id === state.persona); }
|
||
function getDuration() { return DURATIONS.find(d => d.id === state.duration); }
|
||
function getStyle() { return STYLES.find(s => s.id === state.scriptStyle); }
|
||
function getShots() { const d = getDuration(); return (d.shots[0] + d.shots[1]) / 2; }
|
||
function getCost() {
|
||
const p = getProduct();
|
||
const script = 0.20;
|
||
const sb = 0.40;
|
||
const assets = p ? p.imgs * 0.30 : 0;
|
||
const render = getShots() * 0.30;
|
||
const subtotal = script + sb + assets + render;
|
||
const fee = +(subtotal * 0.05).toFixed(2);
|
||
return {
|
||
script: script.toFixed(2),
|
||
sb: sb.toFixed(2),
|
||
assets: assets.toFixed(2),
|
||
render: render.toFixed(2),
|
||
subtotal: subtotal.toFixed(2),
|
||
fee: fee.toFixed(2),
|
||
total: +(subtotal + fee).toFixed(2),
|
||
};
|
||
}
|
||
function balanceAfter() { return +(ACCOUNT_BALANCE - getCost().total).toFixed(2); }
|
||
function etaMinutes() {
|
||
const p = getProduct();
|
||
return Math.max(3, Math.round(2 + getShots() * 0.4 + (p ? p.imgs * 0.2 : 0)));
|
||
}
|
||
function canPass1() { return !!state.productId; }
|
||
function canPass2() {
|
||
const s = getSource(); if (!s) return false;
|
||
if (s.id === 'theme') return state.themeText.trim().length >= 4;
|
||
if (s.id === 'manual') return state.manualScript.trim().length >= 20;
|
||
return true;
|
||
}
|
||
function canPass3() { return state.projectName.trim().length >= 2; }
|
||
function canFinish() { return state.agreed && balanceAfter() >= 5; }
|
||
|
||
/* ---------- icons ---------- */
|
||
|
||
const ICONS = {
|
||
check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>',
|
||
search: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>',
|
||
plus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>',
|
||
x: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l14 14M19 5L5 19"/></svg>',
|
||
bulb: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>',
|
||
arrow: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>',
|
||
wallet: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 14h2"/></svg>',
|
||
};
|
||
|
||
/* ---------- actions ---------- */
|
||
|
||
function selectProduct(id) {
|
||
state.productId = id;
|
||
const p = getProduct();
|
||
if (!state.projectName) {
|
||
state.projectName = p.name.split(' ')[0] + ' · 痛点种草 · v1';
|
||
}
|
||
state.points = {};
|
||
p.points.forEach((pt, i) => { state.points[pt] = i < 2; });
|
||
render();
|
||
}
|
||
|
||
function selectSource(id) {
|
||
state.sourceId = id;
|
||
render();
|
||
}
|
||
|
||
function goNext() {
|
||
if (state.currentStep === 1 && !canPass1()) return;
|
||
if (state.currentStep === 2 && !canPass2()) return;
|
||
if (state.currentStep === 3 && !canPass3()) return;
|
||
if (state.currentStep < 4) state.currentStep++;
|
||
render();
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
function goPrev() {
|
||
if (state.currentStep > 1) state.currentStep--;
|
||
render();
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
function jumpTo(n) {
|
||
if (n < state.currentStep) {
|
||
state.currentStep = n;
|
||
render();
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
}
|
||
|
||
function applyPreset() {
|
||
const p = getPersona();
|
||
state.duration = p.defaults.duration;
|
||
state.scriptStyle = p.defaults.style;
|
||
state.recoDismissed = false;
|
||
render();
|
||
}
|
||
|
||
function startGenerate() {
|
||
if (!canFinish()) return;
|
||
if (typeof Shell !== 'undefined' && Shell.toast) {
|
||
Shell.toast('开始生成项目', '扣款 ¥' + getCost().total.toFixed(2) + ' · pipeline#stage-1');
|
||
}
|
||
setTimeout(() => { location.href = 'pipeline.html#stage-1'; }, 600);
|
||
}
|
||
|
||
function openNewProduct() {
|
||
if (!window.NewProductDrawer) {
|
||
if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('Drawer 未加载', '检查 new-product-drawer.js');
|
||
return;
|
||
}
|
||
window.NewProductDrawer.open({
|
||
onSave: function (p) {
|
||
// p = { id, name, cat, target, points: string[], images: [...], imgs: N }
|
||
// 适配 wizard 数据结构
|
||
const product = {
|
||
id: p.id,
|
||
name: p.name,
|
||
cat: p.cat,
|
||
price: null, // 表单暂未收集价格,显示时跳过
|
||
imgs: p.imgs,
|
||
points: p.points,
|
||
tags: p.target ? p.target.split(/[ ,、、]+/).filter(Boolean).slice(0, 3) : [],
|
||
};
|
||
// 置顶插入,让用户立刻看到
|
||
PRODUCTS.unshift(product);
|
||
// 自动选中(同时种子项目名 / 卖点)
|
||
selectProduct(product.id);
|
||
}
|
||
});
|
||
}
|
||
|
||
// expose for inline onclick
|
||
window._wiz = {
|
||
selectProduct, selectSource, goNext, goPrev, jumpTo, applyPreset, startGenerate, openNewProduct,
|
||
setSearch: v => { state.pickSearch = v; renderStep1Only(); },
|
||
setCat: v => { state.pickCat = v; renderStep1Only(); },
|
||
setTheme: v => { state.themeText = v; updateFootOnly(); updatePreviewLive(); },
|
||
setScript: v => { state.manualScript = v; updateFootOnly(); },
|
||
setName: v => { state.projectName = v; updatePreviewLive(); updateFootOnly(); updateRailOnly(); },
|
||
setDur: v => { state.duration = v; render(); },
|
||
setStyle: v => { state.scriptStyle = v; render(); },
|
||
setPersona:v => { state.persona = v; state.recoDismissed = false; render(); },
|
||
togglePt: k => { state.points[k] = !state.points[k]; render(); },
|
||
dismissReco: () => { state.recoDismissed = true; render(); },
|
||
toggleEmail: () => { state.notifyEmail = !state.notifyEmail; render(); },
|
||
toggleWeChat: () => { state.notifyWeChat = !state.notifyWeChat; render(); },
|
||
toggleTos: () => { state.agreed = !state.agreed; render(); },
|
||
};
|
||
|
||
/* ============================================================
|
||
RENDER
|
||
============================================================ */
|
||
|
||
function railConfig() {
|
||
const p = getProduct(), s = getSource(), pe = getPersona(), du = getDuration(), st = getStyle();
|
||
return [
|
||
{ n: 1, label: '选择商品', desc: p ? p.name : '未选择' },
|
||
{ n: 2, label: '脚本来源', desc: s ? s.name : '未选择' },
|
||
{ n: 3, label: '项目配置', desc: state.currentStep >= 3 ? (du.label + ' · ' + st.name) : '时长 · 风格 · 人设' },
|
||
{ n: 4, label: '确认与计费', desc: '预估 ¥' + getCost().total.toFixed(2) },
|
||
];
|
||
}
|
||
|
||
function renderRail() {
|
||
const cfg = railConfig();
|
||
const html = cfg.map(s => {
|
||
const stt = s.n < state.currentStep ? 'done'
|
||
: s.n === state.currentStep ? 'active' : '';
|
||
const clickable = s.n < state.currentStep;
|
||
const numContent = stt === 'done' ? ICONS.check : s.n;
|
||
return `<div class="step ${stt}${clickable ? ' clickable' : ''}" ${clickable ? 'onclick="_wiz.jumpTo(' + s.n + ')"' : ''}>
|
||
<div class="num">${numContent}</div>
|
||
<div>
|
||
<div class="label">${esc(s.label)}</div>
|
||
<div class="desc">${esc(s.desc)}</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
$('#rail').innerHTML = html;
|
||
}
|
||
|
||
function productPickHTML(p) {
|
||
const selected = state.productId === p.id;
|
||
return `<div class="product-pick${selected ? ' selected' : ''}" onclick="_wiz.selectProduct('${p.id}')">
|
||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||
<div class="body">
|
||
<div class="name">${esc(p.name)}</div>
|
||
<div class="meta">${esc(p.cat)}${p.price != null ? ' · <b>¥' + p.price + '</b>' : ''} · ${p.imgs} 张图</div>
|
||
<div class="tags">${p.tags.map(t => `<span class="tag-s">${esc(t)}</span>`).join('')}</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderStep1() {
|
||
const q = state.pickSearch.trim();
|
||
const filtered = PRODUCTS.filter(p => {
|
||
if (state.pickCat !== '全部' && p.cat !== state.pickCat) return false;
|
||
if (q && !p.name.includes(q)) return false;
|
||
return true;
|
||
});
|
||
const recent = RECENT_IDS.map(id => PRODUCTS.find(p => p.id === id)).filter(Boolean);
|
||
const showRecent = state.pickCat === '全部' && !q;
|
||
|
||
return `<div class="wiz-pane active" data-step="1">
|
||
<div class="wiz-step-h">
|
||
<h2>第 1 步 · 选择商品</h2>
|
||
<p>从商品库选一个 SKU。它的主图与卖点会被 LLM 作为脚本/资产生成的素材。</p>
|
||
</div>
|
||
|
||
<div class="pick-toolbar">
|
||
<div class="search-input">
|
||
${ICONS.search}
|
||
<input type="text" placeholder="搜索商品名称、品牌" value="${esc(state.pickSearch)}" oninput="_wiz.setSearch(this.value)">
|
||
</div>
|
||
${CATS.map(c => `<button class="cat-chip${state.pickCat === c ? ' active' : ''}" onclick="_wiz.setCat('${esc(c)}')">${esc(c)}</button>`).join('')}
|
||
</div>
|
||
|
||
${showRecent ? `
|
||
<div class="pick-section-h"><span>最近使用</span><span class="count">${recent.length}</span></div>
|
||
<div class="product-pick-grid">
|
||
${recent.map(productPickHTML).join('')}
|
||
</div>
|
||
` : ''}
|
||
|
||
<div class="pick-section-h">
|
||
<span>${showRecent ? '全部商品' : '搜索结果'}</span>
|
||
<span class="count">${filtered.length}</span>
|
||
</div>
|
||
<div class="product-pick-grid">
|
||
${filtered.map(productPickHTML).join('')}
|
||
<div class="product-pick add" onclick="_wiz.openNewProduct()">
|
||
<div class="pc">${ICONS.plus}</div>
|
||
<div>新建商品</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderStep2() {
|
||
const s = getSource();
|
||
let detail = '';
|
||
if (s) {
|
||
if (s.id === 'ai') {
|
||
detail = `<div class="source-detail">
|
||
<div class="sd-h">// 已选 · <b>${esc(s.name)}</b></div>
|
||
<div class="field-hint" style="font-size: 12.5px; color: var(--black-alpha-72);">AI 全生模式无需额外输入。下一步选定时长 / 风格 / 人设后,LLM 会自动决定切入点和卖点权重。</div>
|
||
</div>`;
|
||
} else if (s.id === 'theme') {
|
||
detail = `<div class="source-detail">
|
||
<div class="sd-h">// 已选 · <b>${esc(s.name)}</b></div>
|
||
<div class="field" style="margin-bottom: 0;">
|
||
<label class="field-label">一句话主题<span class="req">*</span></label>
|
||
<input class="input" placeholder="例:熬夜党的急救面膜 / 加班吃啥不内疚" value="${esc(state.themeText)}" oninput="_wiz.setTheme(this.value)">
|
||
<div class="field-hint">推荐 5–30 字。这句话会作为 LLM 扩写的锚点,越具体越聚焦。</div>
|
||
</div>
|
||
</div>`;
|
||
} else if (s.id === 'manual') {
|
||
detail = `<div class="source-detail">
|
||
<div class="sd-h">// 已选 · <b>${esc(s.name)}</b></div>
|
||
<div class="field" style="margin-bottom: 0;">
|
||
<label class="field-label">粘贴脚本内容<span class="req">*</span></label>
|
||
<textarea class="textarea" placeholder="粘贴你的脚本内容(旁白 / 镜头描述均可,系统会自动切分镜头)" oninput="_wiz.setScript(this.value)">${esc(state.manualScript)}</textarea>
|
||
<div class="field-hint">最少 20 字。镜头数由你的脚本自然段落决定,时长 / 风格仍会影响后期渲染节奏。</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
}
|
||
return `<div class="wiz-pane active" data-step="2">
|
||
<div class="wiz-step-h">
|
||
<h2>第 2 步 · 脚本来源</h2>
|
||
<p>决定 LLM 如何获得初稿脚本。三种方式由「最省事」到「最保真原意」。</p>
|
||
</div>
|
||
<div class="source-row">
|
||
${SOURCES.map(s => `<div class="source-card${state.sourceId === s.id ? ' selected' : ''}" onclick="_wiz.selectSource('${s.id}')">
|
||
<span class="src-ic">${s.icon}</span>
|
||
<h4>${esc(s.name)}</h4>
|
||
<span class="src-tag">[ ${esc(s.tag)} ]</span>
|
||
<p class="src-desc">${esc(s.desc)}</p>
|
||
</div>`).join('')}
|
||
</div>
|
||
${detail}
|
||
</div>`;
|
||
}
|
||
|
||
function renderStep3() {
|
||
const personaObj = getPersona(), durObj = getDuration(), styleObj = getStyle();
|
||
const recoMismatch = personaObj.defaults.duration !== state.duration || personaObj.defaults.style !== state.scriptStyle;
|
||
const showReco = recoMismatch && !state.recoDismissed;
|
||
const recoDur = DURATIONS.find(d => d.id === personaObj.defaults.duration);
|
||
const recoStyle = STYLES.find(s => s.id === personaObj.defaults.style);
|
||
|
||
return `<div class="wiz-pane active" data-step="3">
|
||
<div class="wiz-step-h">
|
||
<h2>第 3 步 · 项目配置</h2>
|
||
<p>这些设置会影响 LLM 生成脚本的方向,确认后会进入流水线第 1 步(脚本生成)。</p>
|
||
</div>
|
||
|
||
<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>
|
||
<div class="opt-row cols-4">
|
||
${DURATIONS.map(d => `<div class="opt-card${state.duration === d.id ? ' selected' : ''}" onclick="_wiz.setDur('${d.id}')">
|
||
<h4>${esc(d.label)}</h4>
|
||
<div class="sub">${d.shots[0]}-${d.shots[1]} 镜</div>
|
||
<div class="note">${esc(d.tag)}</div>
|
||
<div class="metric">完播 <span class="val">${d.completion}%</span></div>
|
||
</div>`).join('')}
|
||
</div>
|
||
<div class="field-hint">数据来源:抖音同品类 TOP 视频均值 · 实际镜头数由 LLM 决定</div>
|
||
</div>
|
||
|
||
<div class="field">
|
||
<label class="field-label">脚本风格</label>
|
||
<div class="opt-row">
|
||
${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('')}
|
||
</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 style="display:flex; gap:6px; flex-wrap:wrap;">
|
||
${Object.entries(state.points).map(([k, v]) => `<span class="theme-pill${v ? ' active' : ''}" onclick="_wiz.togglePt('${esc(k).replace(/'/g, "\\'")}')">${v ? ICONS.check : ICONS.plus}<span>${esc(k)}</span></span>`).join('')}
|
||
</div>
|
||
</div>` : ''}
|
||
</div>`;
|
||
}
|
||
|
||
function renderStep4() {
|
||
const p = getProduct(), s = getSource(), personaObj = getPersona(), durObj = getDuration(), styleObj = getStyle();
|
||
const c = getCost();
|
||
const ba = balanceAfter();
|
||
const low = ba < 5;
|
||
const eta = etaMinutes();
|
||
const pointsList = Object.entries(state.points).filter(([, v]) => v).map(([k]) => k).join(' / ') || '未选';
|
||
|
||
return `<div class="wiz-pane active" data-step="4">
|
||
<div class="wiz-step-h">
|
||
<h2>第 4 步 · 确认与计费</h2>
|
||
<p>核对前 3 步的选择 + 计费明细。点击「开始生成」会立刻扣款并进入流水线。</p>
|
||
</div>
|
||
|
||
<div class="confirm-grid">
|
||
<div class="confirm-card">
|
||
<div class="cc-h"><span>// 商品</span><button class="cc-edit" onclick="_wiz.jumpTo(1)">修改</button></div>
|
||
${p ? `<div style="display:flex; gap:12px; align-items:flex-start;">
|
||
<div class="placeholder" style="width:44px; height:56px; flex-shrink:0;"><span class="ph-frame">9:16</span></div>
|
||
<div class="cc-body" style="min-width:0;">
|
||
<div style="font-weight:600; font-size:13px;">${esc(p.name)}</div>
|
||
<div class="ln">${esc(p.cat)}${p.price != null ? ' <span style="color: var(--black-alpha-32);">·</span> <b>¥' + p.price + '</b>' : ''} <span style="color: var(--black-alpha-32);">·</span> ${p.imgs} 张图</div>
|
||
</div>
|
||
</div>` : '<div class="cc-body">未选择</div>'}
|
||
</div>
|
||
|
||
<div class="confirm-card">
|
||
<div class="cc-h"><span>// 脚本来源</span><button class="cc-edit" onclick="_wiz.jumpTo(2)">修改</button></div>
|
||
${s ? `<div class="cc-body">
|
||
<div style="font-weight:600; font-size:13px;">${esc(s.name)}</div>
|
||
<div class="ln">${s.id === 'ai' ? 'LLM 全权 · 走向由 Step 3 决定'
|
||
: s.id === 'theme' ? '主题:<b style="margin-left:4px;">' + esc(state.themeText || '(未填)') + '</b>'
|
||
: '<b>' + state.manualScript.length + '</b> 字 · 自动切镜'}</div>
|
||
</div>` : '<div class="cc-body">未选择</div>'}
|
||
</div>
|
||
|
||
<div class="confirm-card">
|
||
<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" style="font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48);">卖点:${esc(pointsList)}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="confirm-card">
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-sub">计费明细 · 按量计费</div>
|
||
<div class="bill-list">
|
||
<div class="bill-row"><div class="l">脚本生成 <span class="l-sub">LLM · 1 稿</span></div><div class="qty">× 1</div><div class="amt">¥${c.script}</div></div>
|
||
<div class="bill-row"><div class="l">故事板生成 <span class="l-sub">含分镜画面描述</span></div><div class="qty">× 1</div><div class="amt">¥${c.sb}</div></div>
|
||
<div class="bill-row"><div class="l">资产生成 <span class="l-sub">主图 → 镜头素材</span></div><div class="qty">× ${p ? p.imgs : 0} 张</div><div class="amt">¥${c.assets}</div></div>
|
||
<div class="bill-row"><div class="l">视频渲染 <span class="l-sub">合成 · 配乐 · 字幕</span></div><div class="qty">× ${getShots()} 镜</div><div class="amt">¥${c.render}</div></div>
|
||
<div class="bill-row subtotal"><div class="l">小计</div><div class="qty"></div><div class="amt">¥${c.subtotal}</div></div>
|
||
<div class="bill-row subtotal"><div class="l">平台服务费 <span class="l-sub">5%</span></div><div class="qty"></div><div class="amt">¥${c.fee}</div></div>
|
||
<div class="bill-row total"><div class="l">合计</div><div class="qty"></div><div class="amt">¥${Math.floor(c.total)}<small>.${c.total.toFixed(2).split('.')[1]}</small></div></div>
|
||
</div>
|
||
|
||
<div class="balance-row${low ? ' low' : ''}">
|
||
<div class="bl">
|
||
${ICONS.wallet}
|
||
<span class="lbl">账户余额</span>
|
||
<span class="val">¥${ACCOUNT_BALANCE.toFixed(2)}</span>
|
||
<span class="arrow">→</span>
|
||
<span class="lbl">扣款后</span>
|
||
<span class="val after">¥${ba.toFixed(2)}</span>
|
||
</div>
|
||
${low
|
||
? `<span class="pill err"><span class="dot"></span>余额不足 · <a>去充值</a></span>`
|
||
: `<span class="pill ok"><span class="dot"></span>余额充足</span>`}
|
||
</div>
|
||
|
||
<div class="section-sub">预估耗时 · 通知</div>
|
||
<div class="eta-block">
|
||
<div class="eta-tile">
|
||
<div class="lbl">预估出片</div>
|
||
<div class="v">~ ${eta}<small>分钟</small></div>
|
||
<div class="desc">// pipeline 5 阶段累计 · 不含人工审核</div>
|
||
</div>
|
||
<div class="eta-tile">
|
||
<div class="lbl">完成后通知</div>
|
||
<div class="check-row${state.notifyEmail ? ' on' : ''}" onclick="_wiz.toggleEmail()" style="padding:4px 0;">
|
||
<span class="check-box"></span>
|
||
<span class="lab">邮件 <span class="mono">${esc(USER_EMAIL)}</span></span>
|
||
</div>
|
||
<div class="check-row${state.notifyWeChat ? ' on' : ''}" onclick="_wiz.toggleWeChat()" style="padding:4px 0;">
|
||
<span class="check-box"></span>
|
||
<span class="lab">微信 <span class="mono">未绑定 · 去绑定</span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tos-row${state.agreed ? ' on' : ''}" onclick="_wiz.toggleTos()">
|
||
<span class="check-box"></span>
|
||
<span class="lab">我已阅读并同意 <a>《按量计费协议》</a> 与 <a>《商品素材使用授权》</a></span>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderCollapsedStep(n) {
|
||
const p = getProduct(), s = getSource();
|
||
let title = '', body = '';
|
||
if (n === 1) {
|
||
title = '第 1 步 · 选择商品';
|
||
body = p
|
||
? `<div style="display:flex; gap:12px; align-items:flex-start;">
|
||
<div class="placeholder" style="width:44px; height:56px; flex-shrink:0;"><span class="ph-frame">9:16</span></div>
|
||
<div style="min-width:0;">
|
||
<div style="font-weight:600; font-size:13.5px;">${esc(p.name)}</div>
|
||
<div class="mono" style="font-size:11.5px; color: var(--black-alpha-48); margin-top:3px; letter-spacing:.02em;">${esc(p.cat)}${p.price != null ? ' · ¥' + p.price : ''} · ${p.imgs} 张图 · ${p.points.length} 个卖点</div>
|
||
</div>
|
||
</div>`
|
||
: '<span class="mono" style="color: var(--black-alpha-48); font-size: 11.5px;">未选择</span>';
|
||
} else if (n === 2) {
|
||
title = '第 2 步 · 脚本来源';
|
||
if (s) {
|
||
let extra = '';
|
||
if (s.id === 'theme' && state.themeText) {
|
||
extra = `<span class="muted" style="color: var(--black-alpha-56);">主题:</span><span style="font-size: 13px;">${esc(state.themeText)}</span>`;
|
||
} else if (s.id === 'manual') {
|
||
extra = `<span class="muted" style="color: var(--black-alpha-56);">脚本:</span><span style="font-size: 13px;">${state.manualScript.length} 字</span>`;
|
||
} else {
|
||
extra = `<span class="mono" style="font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em;">// 走向由 Step 3 决定</span>`;
|
||
}
|
||
body = `<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
|
||
<span class="pill info" style="display:inline-flex; align-items:center; gap:6px; padding: 4px 10px; border-radius: 999px; font-size: 11.5px; background: var(--heat-12); color: var(--heat); border: 1px solid var(--heat-20); font-weight: 500;"><span class="dot" style="width:6px;height:6px;border-radius:50%;background:currentColor;"></span>${esc(s.name)}</span>
|
||
${extra}
|
||
</div>`;
|
||
} else {
|
||
body = '<span class="mono" style="color: var(--black-alpha-48); font-size: 11.5px;">未选择</span>';
|
||
}
|
||
}
|
||
return `<div class="wiz-pane collapsed">
|
||
<div class="wiz-pane-h">
|
||
<h3>${esc(title)}</h3>
|
||
<span style="flex:1"></span>
|
||
<button class="btn btn-ghost btn-sm" onclick="_wiz.jumpTo(${n})">修改</button>
|
||
</div>
|
||
${body}
|
||
</div>`;
|
||
}
|
||
|
||
function renderFoot() {
|
||
const cfg = railConfig();
|
||
const last = state.currentStep === 4;
|
||
const passOk = state.currentStep === 1 ? canPass1()
|
||
: state.currentStep === 2 ? canPass2()
|
||
: state.currentStep === 3 ? canPass3()
|
||
: canFinish();
|
||
const nextLabel = last ? '开始生成 →' : '下一步 →';
|
||
const hint = last
|
||
? `// 扣款 ¥${getCost().total.toFixed(2)} · 进入 pipeline`
|
||
: `// 下一步:${cfg[state.currentStep].label}`;
|
||
const action = last ? '_wiz.startGenerate()' : '_wiz.goNext()';
|
||
return `<div class="wiz-foot">
|
||
<button class="btn btn-ghost"${state.currentStep === 1 ? ' disabled' : ''} onclick="_wiz.goPrev()">← 上一步</button>
|
||
<div style="display:flex; align-items:center; gap:12px;">
|
||
<span class="mono" style="font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em;">${esc(hint)}</span>
|
||
<button class="btn btn-primary btn-lg${!passOk ? ' disabled' : ''}" onclick="${action}">${nextLabel}</button>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderPreview() {
|
||
const p = getProduct(), personaObj = getPersona(), durObj = getDuration(), styleObj = getStyle(), c = getCost();
|
||
const shots = getShots();
|
||
const pointsOn = Object.entries(state.points).filter(([, v]) => v).map(([k]) => k);
|
||
const title = state.projectName || (p ? p.name + ' · 待命名' : '未命名项目');
|
||
|
||
const arrows = '<span class="arrow">' + ICONS.arrow + '</span>';
|
||
|
||
const productSection = p ? `
|
||
<div class="pv-section">
|
||
<div class="lbl">// 商品</div>
|
||
<ul class="pv-list">
|
||
<li>${esc(p.name)}</li>
|
||
<li>${esc(p.cat)}${p.price != null ? ' · ¥' + p.price : ''}</li>
|
||
</ul>
|
||
</div>
|
||
<div class="pv-section">
|
||
<div class="lbl">// 人设 · 风格</div>
|
||
<ul class="pv-list">
|
||
<li>${esc(personaObj.name)} · ${esc(personaObj.sub)}</li>
|
||
<li>${esc(styleObj.name)} · ${esc(durObj.tag)}</li>
|
||
</ul>
|
||
</div>
|
||
<div class="pv-section">
|
||
<div class="lbl">// 脚本走向</div>
|
||
<div class="pv-flow">
|
||
${styleObj.flow.map((n, i) => `<span style="display:inline-flex; align-items:center;"><span class="node">${esc(n)}</span>${i < styleObj.flow.length - 1 ? arrows : ''}</span>`).join('')}
|
||
</div>
|
||
</div>
|
||
<div class="pv-section">
|
||
<div class="lbl">// 突出卖点</div>
|
||
<ul class="pv-list">
|
||
${pointsOn.length ? pointsOn.map(k => `<li>${esc(k)}</li>`).join('') : '<li style="color: var(--black-alpha-48);">未选 · 由 LLM 自动权衡</li>'}
|
||
</ul>
|
||
</div>
|
||
` : `
|
||
<div class="pv-section">
|
||
<div class="lbl">// 待选择</div>
|
||
<ul class="pv-list" style="opacity: .6;">
|
||
<li style="color: var(--black-alpha-48);">先选一个商品</li>
|
||
<li style="color: var(--black-alpha-48);">预估指标会自动填充</li>
|
||
</ul>
|
||
</div>
|
||
`;
|
||
|
||
const footState = state.currentStep < 4 ? '进行中'
|
||
: canFinish() ? '就绪'
|
||
: (balanceAfter() < 5 ? '余额不足' : '待确认');
|
||
|
||
$('#preview').innerHTML = `
|
||
<div class="pv-h"><span>实时预估</span><span class="live">LIVE</span></div>
|
||
<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"><div class="l">预估成本</div><div class="v">¥${c.total.toFixed(2)}</div></div>
|
||
</div>
|
||
${productSection}
|
||
<div class="pv-foot">
|
||
<span>Step ${state.currentStep} / 4 · Restraint</span>
|
||
<strong>${footState}</strong>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/* ---------- partial updates (to keep inputs from losing focus) ---------- */
|
||
|
||
function renderStep1Only() {
|
||
// when user types in search or clicks cat chip — only re-render Step 1 main area
|
||
if (state.currentStep !== 1) return;
|
||
const body = $('#wiz-body');
|
||
const active = body.querySelector('.wiz-pane.active');
|
||
if (active) {
|
||
const tmp = document.createElement('div');
|
||
tmp.innerHTML = renderStep1();
|
||
active.replaceWith(tmp.firstElementChild);
|
||
}
|
||
// refocus search input
|
||
const inp = body.querySelector('.search-input input');
|
||
if (inp && document.activeElement !== inp) {
|
||
inp.focus();
|
||
const v = inp.value;
|
||
inp.setSelectionRange(v.length, v.length);
|
||
}
|
||
}
|
||
|
||
function updatePreviewLive() { renderPreview(); }
|
||
function updateFootOnly() {
|
||
const body = $('#wiz-body');
|
||
const foot = body.querySelector('.wiz-foot');
|
||
if (foot) {
|
||
const tmp = document.createElement('div');
|
||
tmp.innerHTML = renderFoot();
|
||
foot.replaceWith(tmp.firstElementChild);
|
||
}
|
||
}
|
||
function updateRailOnly() { renderRail(); }
|
||
|
||
/* ---------- main render ---------- */
|
||
|
||
function render() {
|
||
renderRail();
|
||
const body = $('#wiz-body');
|
||
let html = '';
|
||
if (state.currentStep >= 2) html += renderCollapsedStep(1);
|
||
if (state.currentStep >= 3) html += renderCollapsedStep(2);
|
||
if (state.currentStep === 1) html += renderStep1();
|
||
else if (state.currentStep === 2) html += renderStep2();
|
||
else if (state.currentStep === 3) html += renderStep3();
|
||
else html += renderStep4();
|
||
html += renderFoot();
|
||
body.innerHTML = html;
|
||
renderPreview();
|
||
}
|
||
|
||
// initial render
|
||
render();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|