AirShelf/v2/projects-new.html
UI 设计 e293aa43be
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
feat(v2): 添加 V2.1 设计稿目录 · 团队/设置页 · pipeline 多项 mock 优化
2026-05-21 16:18:28 +08:00

1232 lines
80 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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); gap: 36px; align-items: start; max-width: 1400px; }
@media (max-width: 1180px) { .wizard { grid-template-columns: 200px minmax(0, 1fr); } }
.steps { position: sticky; top: 24px; align-self: start; max-height: calc(100vh - 48px); overflow-y: auto; }
.wiz-preview { display: none !important; }
/* 顶部胶囊 + 商品横向单行 */
.pick-actions { display: flex; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
.cap-pill { height: 30px; padding: 0 14px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: 999px; font-size: 12.5px; color: var(--accent-black); display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-family: inherit; transition: background var(--t-base), border-color var(--t-base), color var(--t-base); }
.cap-pill:hover { background: var(--background-lighter); border-color: var(--heat-20); color: var(--heat); }
.cap-pill svg { width: 13px; height: 13px; }
.product-pick-row { display: flex; gap: 10px; overflow-x: auto; overflow-y: hidden; padding: 2px 2px 12px; scrollbar-width: thin; }
.product-pick-row::-webkit-scrollbar { height: 8px; }
.product-pick-row::-webkit-scrollbar-thumb { background: var(--border-faint); border-radius: 4px; }
.product-pick-row .product-pick { flex: 0 0 240px; min-width: 240px; }
.product-pick-row .product-pick.lib { flex: 0 0 152px; min-width: 152px; min-height: 96px; }
/* 底部开始 CTA */
.wiz-start-bar { display: flex; justify-content: center; padding: 20px 0 8px; }
.wiz-start-bar .btn-start { height: 44px; padding: 0 36px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: 999px; font-size: 14px; font-weight: 600; cursor: pointer; box-shadow: var(--shadow-cta); display: inline-flex; align-items: center; gap: 8px; font-family: inherit; transition: box-shadow var(--t-base), opacity var(--t-base); }
.wiz-start-bar .btn-start:hover:not(.disabled) { box-shadow: var(--shadow-cta-hover); }
.wiz-start-bar .btn-start.disabled { opacity: .4; cursor: not-allowed; }
.wiz-start-bar .btn-start svg { width: 14px; height: 14px; }
/* 单页式: 所有 step pane 同时显示, 不再切换 */
#wiz-body { display: flex; flex-direction: column; gap: 14px; }
.wiz-pane.active::before, .wiz-pane.active::after { display: none; } /* 去掉 corner-mark, 不需要 "当前激活" 视觉强突出 */
.wiz-pane.active { padding: 22px 24px; }
.wiz-foot { display: none !important; } /* 底部上一步/下一步彻底隐藏 */
/* 计费明细 */
.pv-list.pv-bill li {
display: flex; justify-content: space-between;
padding: 5px 0;
font-size: 12px;
color: var(--black-alpha-72);
}
.pv-list.pv-bill li .ai { color: var(--accent-black); font-family: var(--font-mono); font-size: 11.5px; }
.pv-list.pv-bill .pv-bill-total {
border-top: 1px solid var(--border-faint);
margin-top: 6px; padding-top: 8px;
font-weight: 600; color: var(--accent-black);
}
.pv-list.pv-bill .pv-bill-total .ai b { color: var(--heat); font-size: 14px; font-weight: 700; }
.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: 6px; 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; }
.product-pick.add .hint { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-48); letter-spacing: .02em; }
.product-pick.add.lib:hover { color: var(--heat); border-color: var(--heat); background: var(--heat-12); }
/* ===== 商品库全屏弹窗 (单选,与 model-photo/platform-cover 风格统一) ===== */
.pl-modal-bg { position: fixed; inset: 0; background: var(--surface); z-index: 998; display: none; }
.pl-modal-bg.show { display: flex; }
.pl-modal { margin: 0; flex: 1; background: var(--surface); overflow: hidden; display: flex; flex-direction: column; }
.pl-modal-h { display: flex; align-items: center; gap: 14px; padding: 14px 28px; border-bottom: 1px solid var(--border-faint); flex-shrink: 0; }
.pl-modal-h h2 { font-size: 16px; font-weight: 600; }
.pl-modal-h .ct { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
.pl-modal-h .actions { margin-left: auto; display: flex; gap: 10px; }
.pl-modal-h .x { width: 32px; height: 32px; display: grid; place-items: center; background: transparent; border: 0; border-radius: var(--r-sm); cursor: pointer; color: var(--black-alpha-56); }
.pl-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }
.pl-modal-body { flex: 1; min-height: 0; display: grid; grid-template-columns: 200px 1fr; }
.pl-side { border-right: 1px solid var(--border-faint); padding: 18px 0; overflow-y: auto; }
.pl-side .pl-side-h { padding: 0 20px 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .06em; }
.pl-side .pl-side-item { display: flex; align-items: center; gap: 8px; padding: 9px 20px; cursor: pointer; color: var(--black-alpha-72); font-size: 13px; border-left: 3px solid transparent; transition: background var(--t-base), color var(--t-base); }
.pl-side .pl-side-item:hover { background: var(--black-alpha-4); }
.pl-side .pl-side-item.active { background: var(--heat-12); color: var(--accent-black); border-left-color: var(--heat); font-weight: 600; }
.pl-side .pl-side-item .ct { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }
.pl-main { overflow-y: auto; padding: 0; display: flex; flex-direction: column; }
.pl-toolbar { padding: 14px 28px; border-bottom: 1px solid var(--border-faint); display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
.pl-toolbar .search { position: relative; flex: 1; max-width: 360px; }
.pl-toolbar .search input { width: 100%; height: 32px; padding: 0 10px 0 32px; background: var(--background-lighter); border: 1px solid var(--black-alpha-12); border-radius: var(--r-sm); font-size: 12.5px; font-family: inherit; color: var(--accent-black); outline: none; box-sizing: border-box; }
.pl-toolbar .search svg { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; color: var(--black-alpha-48); }
.pl-toolbar .btn-new { height: 32px; padding: 0 14px; display: inline-flex; align-items: center; gap: 6px; background: var(--surface); border: 1px solid var(--black-alpha-12); border-radius: var(--r-sm); color: var(--accent-black); font-family: inherit; font-size: 12.5px; cursor: pointer; margin-left: auto; }
.pl-toolbar .btn-new:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }
.pl-toolbar .btn-new svg { width: 13px; height: 13px; }
.pl-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 20px 28px 28px; }
.pl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
.pl-card { position: relative; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); padding: 10px; cursor: pointer; display: flex; flex-direction: column; gap: 6px; transition: background var(--t-base), border-color var(--t-base); }
.pl-card:hover { background: var(--surface); }
.pl-card.selected { border-color: var(--heat); background: var(--heat-12); }
.pl-card .pl-thumb { aspect-ratio: 1; border-radius: var(--r-sm); }
.pl-card .pl-name { font-size: 12.5px; font-weight: 500; color: var(--accent-black); }
.pl-card .pl-meta { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.pl-card .pl-check { position: absolute; top: 16px; right: 16px; width: 22px; height: 22px; background: rgba(255,255,255,.95); border: 1.5px solid var(--black-alpha-24); border-radius: 50%; display: grid; place-items: center; z-index: 2; color: var(--accent-white); }
.pl-card .pl-check svg { width: 11px; height: 11px; opacity: 0; }
.pl-card.selected .pl-check { background: var(--heat); border-color: var(--heat); }
.pl-card.selected .pl-check svg { opacity: 1; }
.pl-modal-f { padding: 14px 28px; border-top: 1px solid var(--border-faint); display: flex; justify-content: flex-end; align-items: center; gap: 10px; flex-shrink: 0; }
.pl-modal-f .summary { margin-right: auto; font-family: var(--font-mono); font-size: 12px; color: var(--black-alpha-56); letter-spacing: .02em; }
.pl-modal-f .summary b { color: var(--heat); font-weight: 700; }
/* ── 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 input,
.wiz-preview textarea,
.wiz-preview select { box-sizing: border-box; max-width: 100%; }
.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; }
/* 精简版计费 + 余额 */
.pv-bill-summary { margin-top: 14px; padding: 10px 12px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); display: flex; flex-direction: column; gap: 6px; }
.pv-bill-summary .row { display: flex; align-items: baseline; justify-content: space-between; font-size: 12px; }
.pv-bill-summary .row .k { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.pv-bill-summary .row .v { font-variant-numeric: tabular-nums; color: var(--accent-black); }
.pv-bill-summary .row .v b { color: var(--heat); font-size: 14px; font-weight: 700; }
.pv-bill-summary .row .v.low { color: var(--accent-crimson); }
.pv-agree-row { margin-top: 10px; }
.pv-agree-row label { display: flex; align-items: flex-start; gap: 6px; cursor: pointer; font-size: 11.5px; color: var(--black-alpha-56); line-height: 1.5; }
.pv-agree-row input[type="checkbox"] { margin-top: 2px; cursor: pointer; flex-shrink: 0; }
.pv-agree-row a { color: var(--heat); }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>新建项目</h1>
<div class="sub"><span class="mono">// 商品 → 配置 · 2 步开始生成</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>
<!-- ===== 商品库 全屏弹窗 (单选,新建项目用) ===== -->
<div class="pl-modal-bg" id="pl-modal-bg">
<div class="pl-modal">
<div class="pl-modal-h">
<h2>商品库</h2>
<span class="ct" id="pl-total-ct"></span>
<div class="actions">
<button class="x" type="button" id="pl-close-btn" aria-label="关闭">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg>
</button>
</div>
</div>
<div class="pl-modal-body">
<aside class="pl-side" id="pl-side">
<!-- JS 渲染分类 (动态从 PRODUCTS) -->
</aside>
<div class="pl-main">
<div class="pl-toolbar">
<div class="search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" id="pl-search-input" placeholder="搜索商品名">
</div>
<button class="btn-new" type="button" id="pl-new-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
新建商品
</button>
</div>
<div class="pl-scroll">
<div class="pl-grid" id="pl-grid"></div>
</div>
</div>
</div>
<div class="pl-modal-f">
<div class="summary">// 单选:点击商品即选用,自动关闭</div>
<button class="btn" type="button" id="pl-cancel-btn">取消</button>
</div>
</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 按此扩写。推荐 530 字。',
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: null,
scriptStyle: null,
persona: null,
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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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) || null; }
function getDuration() { return DURATIONS.find(d => d.id === state.duration) || null; }
function getStyle() { return STYLES.find(s => s.id === state.scriptStyle) || null; }
function getShots() { const d = getDuration(); return d ? (d.shots[0] + d.shots[1]) / 2 : 0; }
function getCost() {
// 全部返回 number,使用方各自 toFixed(2),避免 stringify 后再调 toFixed 报错
const p = getProduct();
const script = 0.20;
const assets = p ? p.imgs * 0.30 : 0;
const sb = 0.40; // 故事板 storyboard
const video = getShots() * 0.30; // 视频片段
const exp = 0.10; // 拼接导出
const subtotal = script + assets + sb + video + exp;
const fee = +(subtotal * 0.05).toFixed(2);
const total = +(subtotal + fee).toFixed(2);
return {
script, assets, sb, video, export: exp,
storyboard: sb, render: video, // 别名,兼容旧调用点
subtotal, fee, total,
};
}
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 canPass1() && canPass2() && canPass3() && 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() {
const p = getProduct(), d = getDuration(), st = getStyle();
if (!p || !d || !st || state.projectName.trim().length < 2) return;
// 持久化项目, 让 projects.html 下次加载时自动 prepend 到列表
try {
const seconds = (d.id.split('-')[1] || '15');
const baseName = state.projectName.trim();
const project = {
id: 'proj-' + Date.now(),
name: baseName + (/\sv\d+$/.test(baseName) ? '' : ' · v1'),
product: p.name,
source: '',
shots: Math.round(getShots()),
durationLabel: '0-' + seconds + 's',
status: 'wip',
stage: 1,
pillText: '脚本生成中',
createdAt: Date.now(),
};
const KEY = 'fs-extra-projects';
const list = JSON.parse(localStorage.getItem(KEY) || '[]');
list.push(project);
localStorage.setItem(KEY, JSON.stringify(list));
} catch (e) { /* localStorage 不可用时降级到只跳转 */ }
if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('项目已创建', '进入 pipeline · Stage 1 脚本');
setTimeout(() => { location.href = 'pipeline.html?stage=1&product=' + encodeURIComponent(p.name); }, 300);
}
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);
// 自动选中(同时种子项目名 / 卖点),触发 render() 刷新 step 1
selectProduct(product.id);
// 商品库若开着 → 强制 reset filter/query/搜索框,确保新商品一定可见
const libBg = document.getElementById('pl-modal-bg');
if (libBg && libBg.classList.contains('show')) {
_plCatFilter = '';
_plQuery = '';
const searchInput = document.getElementById('pl-search-input');
if (searchInput) searchInput.value = '';
document.getElementById('pl-total-ct').textContent = '// 共 ' + PRODUCTS.length + ' 个商品';
renderProdLibSide();
renderProdLib();
}
}
});
}
// ─── 商品库全屏弹窗 (单选) ───
let _plQuery = '';
let _plCatFilter = '';
function renderProdLib() {
const grid = document.getElementById('pl-grid');
let list = PRODUCTS;
if (_plCatFilter) list = list.filter(x => x.cat === _plCatFilter);
if (_plQuery) list = list.filter(x => x.name.includes(_plQuery));
grid.innerHTML = list.map(p => `
<div class="pl-card${state.productId === p.id ? ' selected' : ''}" data-id="${p.id}">
<div class="pl-check"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></div>
<div class="placeholder pl-thumb"><span class="ph-frame">${esc(p.name)}</span></div>
<div class="pl-name">${esc(p.name)}</div>
<div class="pl-meta">${esc(p.cat)}${p.price != null ? ' · ¥' + p.price : ''} · ${p.imgs} 张图</div>
</div>
`).join('');
grid.querySelectorAll('.pl-card').forEach(card => {
card.addEventListener('click', () => {
selectProduct(card.dataset.id);
closeProdLib();
});
});
}
function renderProdLibSide() {
// 动态分类(从 PRODUCTS 统计)
const counts = { '全部': PRODUCTS.length };
PRODUCTS.forEach(p => { counts[p.cat] = (counts[p.cat] || 0) + 1; });
const cats = ['全部', ...Object.keys(counts).filter(k => k !== '全部')];
const side = document.getElementById('pl-side');
side.innerHTML = '<div class="pl-side-h">分类</div>' +
cats.map(c => `<div class="pl-side-item${(_plCatFilter === '' && c === '全部') || _plCatFilter === c ? ' active' : ''}" data-cat="${c === '全部' ? '' : esc(c)}">${esc(c)} <span class="ct">${counts[c]}</span></div>`).join('');
side.querySelectorAll('.pl-side-item').forEach(item => {
item.addEventListener('click', () => {
side.querySelectorAll('.pl-side-item').forEach(x => x.classList.remove('active'));
item.classList.add('active');
_plCatFilter = item.dataset.cat;
renderProdLib();
});
});
}
function openProdLib() {
_plQuery = '';
_plCatFilter = '';
document.getElementById('pl-search-input').value = '';
document.getElementById('pl-total-ct').textContent = '// 共 ' + PRODUCTS.length + ' 个商品';
renderProdLibSide();
renderProdLib();
document.getElementById('pl-modal-bg').classList.add('show');
}
function closeProdLib() {
document.getElementById('pl-modal-bg').classList.remove('show');
}
// expose for inline onclick
window._wiz = {
selectProduct, selectSource, goNext, goPrev, jumpTo, applyPreset, startGenerate, openNewProduct,
openProdLib, closeProdLib,
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(); },
toggleAgree: v => { state.agreed = v; render(); },
setProjectName: v => {
state.projectName = v;
// 仅更新预览的标题, 不重新 render (避免 input 失焦)
const titleEl = $('#preview .pv-title');
if (titleEl) {
// 仅替换标题文字部分,保留 input
const p = getProduct();
const text = state.projectName || (p ? p.name + ' · 待命名' : '未命名项目');
const node = titleEl.childNodes[0];
if (node && node.nodeType === 3) node.nodeValue = text;
}
},
};
/* ============================================================
RENDER
============================================================ */
function railConfig() {
const p = getProduct(), du = getDuration(), st = getStyle();
const cfgReady = !!(du && st);
return [
{ n: 1, label: '选择商品', desc: p ? p.name : '未选择', done: canPass1() },
{ n: 2, label: '项目配置', desc: cfgReady ? (du.label + ' · ' + st.name) : '时长 · 风格 · 人物', done: cfgReady && canPass3() },
];
}
// 平滑滚动到 step section
function scrollToStep(n) {
const target = document.querySelector('#step-pane-' + n);
if (!target) return;
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// 暴露 (rail 内 onclick 引用)
window._wiz_scrollToStep = scrollToStep;
function renderRail() {
const cfg = railConfig();
const html = cfg.map(s => {
const stt = s.done ? 'done' : '';
const numContent = s.done ? ICONS.check : s.n;
return `<div class="step ${stt} clickable" onclick="_wiz_scrollToStep(${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 ids = new Set();
if (state.productId) ids.add(state.productId);
RECENT_IDS.forEach(id => ids.add(id));
const recent = [...ids].map(id => PRODUCTS.find(p => p.id === id)).filter(Boolean);
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-actions">
<button class="cap-pill" type="button" onclick="_wiz.openNewProduct()">
<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>
添加商品
</button>
<button class="cap-pill" type="button" onclick="_wiz.openProdLib()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M9 4v16"/></svg>
去商品库 <span style="font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-left: 2px;">// 共 ${PRODUCTS.length} 个</span>
</button>
</div>
<div class="pick-section-h">
<span>最近使用</span>
<span class="count">${recent.length}</span>
</div>
<div class="product-pick-row">
${recent.map(productPickHTML).join('')}
</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">推荐 530 字。这句话会作为 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();
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>
</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() {
// 实时预估面板已移除 (改为底部「开始」CTA)
const previewEl = $('#preview');
if (previewEl) previewEl.innerHTML = '';
return;
// legacy code below kept for reference but unreachable
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 ready = canPass1() && canPass2() && canPass3();
const balOk = balanceAfter() >= 5;
const canGo = ready && balOk && state.agreed;
const footState = !ready ? '填写中' : !balOk ? '余额不足' : !state.agreed ? '待确认协议' : '就绪';
const stepProgress = [canPass1(), canPass2(), canPass3()].filter(Boolean).length;
const configReady = !!(state.persona && state.duration && state.scriptStyle);
const summaryBlock = (p || configReady) ? `
<div class="pv-section">
<div class="lbl">// 已选</div>
<ul class="pv-list">
${p ? `<li>${esc(p.name)}</li>` : ''}
${configReady ? `<li>${esc(personaObj.name)} · ${esc(styleObj.name)} · ${esc(durObj.label)}</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>
</ul>
</div>`;
// 计费 + 余额(精简版,不展开 5 阶段明细)
const billingHTML = `
<div class="pv-bill-summary">
<div class="row"><span class="k">预估合计</span><span class="v"><b>¥${c.total.toFixed(2)}</b></span></div>
<div class="row"><span class="k">扣款后余额</span><span class="v ${balOk ? '' : 'low'}">¥${balanceAfter().toFixed(2)}</span></div>
</div>
<div class="pv-agree-row">
<label>
<input type="checkbox" id="pv-agree" ${state.agreed ? 'checked' : ''} onchange="_wiz.toggleAgree(this.checked)">
<span>同意 <a href="#" onclick="event.preventDefault();Shell.toast('用户协议')">协议</a> 与 <a href="#" onclick="event.preventDefault();Shell.toast('计费规则')">计费规则</a>(失败不扣费)</span>
</label>
</div>
`;
$('#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>
${summaryBlock}
${billingHTML}
<button class="btn btn-primary btn-lg" style="width:100%;margin-top:14px;justify-content:center;${canGo ? '' : 'opacity:.5;pointer-events:none;cursor:not-allowed;'}" onclick="_wiz.startGenerate()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3l14 9-14 9V3z"/></svg>
开始生成
</button>
<div class="pv-foot">
<span>${stepProgress}/3 步已完成</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');
// 单页式: 商品 (step1) + 项目配置 (原 step3, 现 step2),底部「开始」CTA
const p = getProduct(), du = getDuration(), st = getStyle();
const canStart = !!(p && du && st && 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>';
html += `<div class="wiz-start-bar">
<button class="btn-start${canStart ? '' : ' disabled'}" type="button" onclick="_wiz.startGenerate()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3l14 9-14 9V3z"/></svg>
<span>开始</span>
</button>
</div>`;
body.innerHTML = html;
}
// 商品库弹窗事件绑定 (DOM 静态元素)
document.getElementById('pl-close-btn').addEventListener('click', closeProdLib);
document.getElementById('pl-cancel-btn').addEventListener('click', closeProdLib);
document.getElementById('pl-search-input').addEventListener('input', e => {
_plQuery = e.target.value.trim();
renderProdLib();
});
document.getElementById('pl-new-btn').addEventListener('click', () => {
// 商品库保持 open(drawer z-index 1101 > pl-modal-bg 998 会覆盖之上)
openNewProduct();
});
// initial render
render();
})();
</script>
</body>
</html>