AirShelf/电商AI平台/projects-new.html
iye 5edfa05369
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 10s
Polish editor timeline interactions
2026-05-27 17:59:55 +08:00

1615 lines
98 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>新建项目 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<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: calc(64px + 24px);
align-self: start;
max-height: calc(100vh - 64px - 48px);
overflow-y: auto;
z-index: 2;
}
.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: flex-end; 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-row { display: flex; gap: 8px; flex-wrap: wrap; }
.theme-pill { display: inline-flex; gap: 6px; align-items: center; height: 36px; padding: 0 16px; border: 1px solid var(--border-faint); border-radius: 999px; background: var(--surface); font-size: 13px; font-weight: 500; font-family: inherit; cursor: pointer; color: var(--accent-black); transition: background var(--t-base), border-color var(--t-base), 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); font-weight: 600; }
.theme-pill svg { width: 13px; height: 13px; }
.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; }
/* ============================================================
第 1 步 · 商品选择器(沿用 products.html 商品库的卡片与 toolbar 视觉)
─ 命名空间 .pp- 前缀,避免与 products.html 冲突
============================================================ */
.pp-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.pp-toolbar .search-inline {
flex: 1; min-width: 220px; max-width: 340px;
display: inline-flex; align-items: center; gap: 8px;
height: 34px; padding: 0 12px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
transition: border-color var(--t-base);
}
.pp-toolbar .search-inline:focus-within { border-color: var(--heat-40); }
.pp-toolbar .search-inline svg { width: 14px; height: 14px; color: var(--black-alpha-48); flex-shrink: 0; }
.pp-toolbar .search-inline input { flex: 1; min-width: 0; height: 100%; border: 0; outline: 0; background: transparent; font-size: 13px; color: var(--accent-black); font-family: inherit; }
.pp-toolbar .pp-chip-wrap { position: relative; }
.pp-toolbar .pp-chip {
display: inline-flex; align-items: center; gap: 6px;
height: 34px; padding: 0 12px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
font-size: 13px; font-family: inherit;
color: var(--black-alpha-72);
cursor: pointer;
transition: border-color var(--t-base), color var(--t-base);
}
.pp-toolbar .pp-chip:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
.pp-toolbar .pp-chip.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); }
.pp-toolbar .pp-chip svg { width: 11px; height: 11px; opacity: .6; }
.pp-toolbar .pp-menu {
position: absolute; top: calc(100% + 4px); left: 0;
min-width: 160px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 6px 20px rgba(0,0,0,.06);
padding: 4px;
display: none;
z-index: 20;
}
.pp-toolbar .pp-chip-wrap.open .pp-menu { display: block; }
.pp-toolbar .pp-menu .mi {
display: flex; align-items: center; gap: 6px;
padding: 7px 10px;
border-radius: var(--r-sm);
font-size: 12.5px;
color: var(--accent-black);
cursor: pointer;
}
.pp-toolbar .pp-menu .mi:hover { background: var(--background-lighter); }
.pp-toolbar .pp-menu .mi.selected { color: var(--heat); font-weight: 600; }
.pp-toolbar .pp-menu .mi-check { width: 12px; height: 12px; opacity: 0; }
.pp-toolbar .pp-menu .mi.selected .mi-check { opacity: 1; }
.pp-toolbar .pp-clear {
display: inline-flex; align-items: center; gap: 4px;
height: 30px; padding: 0 10px;
background: transparent; border: 0; border-radius: var(--r-sm);
color: var(--black-alpha-56); font-size: 12.5px; font-family: inherit;
cursor: pointer;
}
.pp-toolbar .pp-clear:hover { color: var(--accent-crimson); background: var(--crimson-bg, #fdebea); }
.pp-toolbar .pp-clear svg { width: 11px; height: 11px; }
.pp-toolbar .spacer { flex: 1; }
.pp-view-tog { display: inline-flex; border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }
.pp-view-tog button {
width: 34px; height: 34px;
background: var(--surface);
border: 0; border-right: 1px solid var(--border-faint);
cursor: pointer;
color: var(--black-alpha-48);
display: grid; place-items: center;
transition: background var(--t-base), color var(--t-base);
}
.pp-view-tog button:last-child { border-right: 0; }
.pp-view-tog button:hover { background: var(--background-lighter); color: var(--accent-black); }
.pp-view-tog button.active { background: var(--heat-12); color: var(--heat); }
.pp-view-tog button svg { width: 14px; height: 14px; }
.pp-result-meta {
font-family: var(--font-mono); font-size: 11.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
margin: 4px 0 12px;
}
/* 网格 — 沿用 products.html 商品库卡片样式(复制必要部分)
固定 4 列 → 每页 8 tile(createCard + 7 商品)正好两行 */
.pp-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
@media (max-width: 1100px) { .pp-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 800px) { .pp-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.pp-grid .product-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
cursor: pointer; position: relative; overflow: hidden;
display: flex; flex-direction: column;
transition: background .15s, border-color .15s, transform .15s;
}
.pp-grid .product-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
.pp-grid .product-card.selected { border-color: var(--heat); background: var(--heat-12); }
.pp-grid .product-card.selected::after {
content: ''; position: absolute; top: 0; right: 0;
width: 0; height: 0;
border-top: 28px solid var(--heat);
border-left: 28px solid transparent;
z-index: 2;
}
.pp-grid .product-card.selected::before {
content: ''; position: absolute; top: 4px; right: 4px;
width: 10px; height: 10px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23ffffff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 8 7 12 13 4'/%3E%3C/svg%3E") no-repeat center / contain;
z-index: 3;
}
.pp-grid .product-thumb { aspect-ratio: 1.4 / 1; }
.pp-grid .product-body { padding: 14px 14px 12px; flex: 1; }
.pp-grid .product-name {
font-size: 14px; font-weight: 600; color: var(--accent-black);
line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pp-grid .product-cat {
display: inline-flex; align-items: center;
margin-top: 8px; padding: 2px 8px;
background: var(--background-lighter); color: var(--black-alpha-72);
border-radius: var(--r-sm); font-size: 11.5px;
}
.pp-grid .product-date {
font-family: var(--font-mono);
font-size: 11px; color: var(--black-alpha-48);
margin-top: 10px; letter-spacing: .02em;
}
.pp-grid .product-card.selected .product-cat { background: var(--surface); color: var(--heat); }
/* 创建新商品 空卡 */
.pp-grid .pp-create-card {
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
background: transparent;
cursor: pointer;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 10px; min-height: 220px;
color: var(--black-alpha-48);
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.pp-grid .pp-create-card:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
.pp-grid .pp-create-card .pc-plus {
width: 44px; height: 44px;
border-radius: 50%;
background: var(--heat); color: var(--accent-white);
display: grid; place-items: center;
transition: filter var(--t-base);
}
.pp-grid .pp-create-card:hover .pc-plus { filter: brightness(1.06); }
.pp-grid .pp-create-card .pc-plus svg { width: 18px; height: 18px; }
.pp-grid .pp-create-card .pc-t { font-size: 13px; font-weight: 600; }
.pp-grid .pp-create-card .pc-d { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .02em; }
/* 列表视图 */
.pp-grid.list-view { display: flex; flex-direction: column; gap: 6px; }
.pp-grid.list-view .product-card {
flex-direction: row; align-items: center;
}
.pp-grid.list-view .product-thumb { width: 96px; aspect-ratio: 1.4 / 1; flex-shrink: 0; }
.pp-grid.list-view .product-body { flex: 1; padding: 10px 14px; }
.pp-grid.list-view .pp-create-card { flex-direction: row; min-height: 56px; gap: 12px; }
.pp-grid.list-view .pp-create-card .pc-plus { width: 32px; height: 32px; }
/* 空筛选结果 */
.pp-empty {
grid-column: 1 / -1;
padding: 48px 24px; text-align: center;
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
background: var(--background-lighter);
color: var(--black-alpha-48);
font-size: 12.5px; font-family: var(--font-mono); letter-spacing: .02em;
}
.pp-empty .reset { display: inline-block; margin-top: 8px; color: var(--heat); cursor: pointer; text-decoration: underline; }
/* 分页 */
.pp-pager {
display: flex; align-items: center; gap: 16px;
margin-top: 18px; padding-top: 14px;
border-top: 1px solid var(--border-faint);
font-size: 12.5px; color: var(--black-alpha-56);
}
.pp-pager .total { font-family: var(--font-mono); letter-spacing: .02em; }
.pp-pager .pages { display: inline-flex; gap: 4px; margin-left: auto; }
.pp-pager .pages button {
min-width: 28px; height: 28px; padding: 0 8px;
border: 1px solid var(--border-faint); background: var(--surface);
border-radius: var(--r-sm);
cursor: pointer; font-size: 12.5px; color: var(--black-alpha-72); font-family: inherit;
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
}
.pp-pager .pages button:hover:not(.active):not(:disabled) { border-color: var(--black-alpha-32); color: var(--accent-black); }
.pp-pager .pages button.active { background: var(--heat); color: var(--accent-white); border-color: var(--heat); font-weight: 600; }
.pp-pager .pages button:disabled { opacity: .4; cursor: not-allowed; }
.pp-pager .pages .ellipsis {
min-width: 22px; height: 28px;
display: inline-flex; align-items: center; justify-content: center;
color: var(--black-alpha-48); font-family: var(--font-mono);
}
.pp-pager .page-size {
display: inline-flex; align-items: center; gap: 4px;
height: 28px; padding: 0 10px;
background: var(--surface); border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
cursor: pointer; font-family: inherit; font-size: 12.5px; color: var(--black-alpha-72);
}
.pp-pager .page-size:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
.pp-pager .page-size svg { width: 10px; height: 10px; opacity: .6; }
/* 底部提示 */
.pp-bottom-tip {
margin-top: 14px;
padding: 10px 14px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
font-size: 12.5px; color: var(--black-alpha-56);
display: flex; align-items: center; gap: 8px;
}
.pp-bottom-tip svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--black-alpha-48); }
.pp-bottom-tip a { color: var(--heat); cursor: pointer; text-decoration: none; }
.pp-bottom-tip a:hover { text-decoration: underline; }
/* ── 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; }
.config-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: end; margin-bottom: 16px; }
.config-row .field { margin-bottom: 0; }
.duration-select { cursor: pointer; }
@media (max-width: 900px) { .config-row { grid-template-columns: 1fr; } }
.field-label { display: block; font-size: 12.5px; color: var(--black-alpha-56); font-weight: 500; margin-bottom: 6px; }
.field-label .req { color: var(--heat); margin-left: 2px; }
.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.5"><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.5" 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/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></script>
<script src="assets/new-product-drawer.js?v=202605211643"></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: ['熬夜党', '敏感肌'], date: '2026-05-15' },
{ id: 'earphone', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', price: 199, imgs: 5, points: ['主动降噪', '32 小时续航', 'IP55 防水'], tags: ['通勤', '运动'], date: '2026-05-12' },
{ id: 'noodle', name: '滋啦速食牛肉面 · 6 桶装', cat: '食品饮料', price: 49.9, imgs: 4, points: ['3 分钟出餐', '真材实料牛肉', '0 防腐剂'], tags: ['加班', '独居'], date: '2026-05-10' },
{ id: 'sun', name: '透真清透物理防晒霜', cat: '美妆个护', price: 69, imgs: 4, points: ['SPF50 PA+++', '纯物理防晒', '不泛白不假面'], tags: ['SPF50', '通勤'], date: '2026-05-08' },
{ id: 'coffee', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', price: 89, imgs: 6, points: ['冷热水秒溶', '意式深烘', '24 颗轻便装'], tags: ['提神', '早八'], date: '2026-05-05' },
{ id: 'fryer', name: '小熊 4L 可视空气炸锅', cat: '家居家电', price: 159, imgs: 5, points: ['可视化窗口', '4L 大容量', '低脂少油'], tags: ['小户型', '健康'], date: '2026-05-03' },
{ id: 'yoga', name: '露露同款裸感瑜伽裤', cat: '运动户外', price: 119, imgs: 8, points: ['裸感面料', '高弹回弹', '随心动随心穿'], tags: ['健身房', '通勤'], date: '2026-04-30' },
];
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' } },
];
/* ---------- 合并其它页面创建的商品 ---------- */
// 商品库 / 工作台 / 新建项目 都共用同一 drawer(assets/new-product-drawer.js),
// 该 drawer 在 save() 时把新商品写入 sessionStorage['fs-extra-products']。
// 这里在 PRODUCTS hardcoded 数据后,把 storage 中尚不在 PRODUCTS 的商品 unshift
// 到列表头部 → 用户跨页面新建的商品在 step 1 也能立即看到。
(function mergeExtraProducts() {
try {
const raw = sessionStorage.getItem('fs-extra-products');
if (!raw) return;
const list = JSON.parse(raw);
if (!Array.isArray(list) || !list.length) return;
const existingIds = new Set(PRODUCTS.map(p => p.id));
// 按 createdAt 倒序(最新在前),逐个 unshift
list.slice().sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)).forEach(x => {
if (existingIds.has(x.id)) return;
existingIds.add(x.id);
PRODUCTS.unshift({
id: x.id,
name: x.name || '未命名商品',
cat: x.cat || '未分类',
price: null, // 表单未采集价格
imgs: Math.max(1, x.assets || 0), // 用素材数兜底,至少 1
points: Array.isArray(x.bullets) ? x.bullets : [],
tags: x.target ? String(x.target).split(/[ ,、、]+/).filter(Boolean).slice(0, 3) : [],
date: x.date || (x.createdAt ? new Date(x.createdAt).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10)),
createdAt: x.createdAt || Date.now(),
});
});
} catch (e) { /* storage 不可用就静默放弃 */ }
})();
const USER_EMAIL = 'li@shop.com';
const ACCOUNT_BALANCE = 327.40;
/* ---------- state ---------- */
const state = {
currentStep: 1,
productId: null,
pickSearch: '',
pickCat: '全部',
pickSort: 'recent', // recent | name | added
pickView: 'grid', // grid | list
pickPage: 1,
pickPageSize: 7, // 固定 4 列 × 2 行 = 8 tile,首页含 createCard 占 1 位 → 7 商品
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 icon = (name, opts) => window.IconKit ? window.IconKit.svg(name, opts) : '';
const ICONS = {
check: icon('check'),
search: icon('search'),
plus: icon('plus'),
x: icon('x'),
bulb: icon('info'),
arrow: icon('arrowRight'),
wallet: icon('wallet'),
};
/* ---------- 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 => { state.points[pt] = false; });
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 now = Date.now();
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) : [],
date: new Date(now).toISOString().slice(0, 10),
createdAt: now,
};
// 置顶插入,让用户立刻看到
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; state.pickPage = 1; renderStep1Only(); },
setCat: v => { state.pickCat = v; state.pickPage = 1; renderStep1Only(); },
setSort: v => { state.pickSort = v; state.pickPage = 1; renderStep1Only(); },
setView: v => { state.pickView = v; renderStep1Only(); },
setPage: v => { state.pickPage = Math.max(1, v); renderStep1Only(); },
setPageSize: v => { state.pickPageSize = v; state.pickPage = 1; renderStep1Only(); },
clearPickFilters: () => { state.pickSearch=''; state.pickCat='全部'; state.pickPage=1; 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 productCardHTML(p) {
const selected = state.productId === p.id;
return `<div class="product-card${selected ? ' selected' : ''}" onclick="_wiz.selectProduct('${p.id}')">
<div class="placeholder product-thumb"><span class="ph-frame">${esc(p.name)} · 1200×800</span></div>
<div class="product-body">
<div class="product-name">${esc(p.name)}</div>
<div class="product-cat">${esc(p.cat)}</div>
<div class="product-date">${esc(p.date || '')} 创建</div>
</div>
</div>`;
}
function pickerFilteredProducts() {
const q = (state.pickSearch || '').trim().toLowerCase();
let list = PRODUCTS.filter(p => {
if (state.pickCat !== '全部' && p.cat !== state.pickCat) return false;
if (q) {
const blob = (p.name + ' ' + p.cat + ' ' + (p.tags || []).join(' ')).toLowerCase();
if (!blob.includes(q)) return false;
}
return true;
});
if (state.pickSort === 'name') {
list = list.slice().sort((a, b) => a.name.localeCompare(b.name, 'zh'));
} else if (state.pickSort === 'added') {
list = list.slice();
} else {
// recent 排序优先级:
// 1) 用户新建的商品(有 createdAt) — 按 createdAt 倒序,最新在最前(紧挨 createCard 按钮)
// 2) RECENT_IDS 命中的商品 — 按命中顺序
// 3) 其余 — 保持原始顺序
const recent = new Map(RECENT_IDS.map((id, i) => [id, i]));
function weight(p) {
if (p.createdAt) return -p.createdAt; // 负值 → 最大的负数(最近创建)排最前
if (recent.has(p.id)) return recent.get(p.id);
return Number.POSITIVE_INFINITY;
}
list = list.slice().sort((a, b) => weight(a) - weight(b));
}
return list;
}
function pickerPagerHTML(total) {
const pageSize = state.pickPageSize;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const cur = Math.min(state.pickPage, totalPages);
function pageBtn(n) {
const act = n === cur ? ' active' : '';
return `<button type="button" class="${act.trim()}" onclick="_wiz.setPage(${n})">${n}</button>`;
}
const items = [];
items.push(`<button type="button"${cur === 1 ? ' disabled' : ''} onclick="_wiz.setPage(${cur - 1})"></button>`);
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) items.push(pageBtn(i));
} else {
items.push(pageBtn(1));
if (cur > 3) items.push('<span class="ellipsis">…</span>');
const lo = Math.max(2, cur - 1), hi = Math.min(totalPages - 1, cur + 1);
for (let i = lo; i <= hi; i++) items.push(pageBtn(i));
if (cur < totalPages - 2) items.push('<span class="ellipsis">…</span>');
items.push(pageBtn(totalPages));
}
items.push(`<button type="button"${cur === totalPages ? ' disabled' : ''} onclick="_wiz.setPage(${cur + 1})"></button>`);
return `<div class="pp-pager">
<span class="total">共 ${total} 条</span>
<div class="pages">${items.join('')}</div>
<span class="page-size">每页 ${pageSize} 条</span>
</div>`;
}
function renderStep1() {
const list = pickerFilteredProducts();
const total = list.length;
const pageSize = state.pickPageSize;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const cur = Math.min(state.pickPage, totalPages);
const pageList = list.slice((cur - 1) * pageSize, cur * pageSize);
const cats = ['全部', ...new Set(PRODUCTS.map(p => p.cat))];
const catChipActive = state.pickCat !== '全部';
const sortLabels = { recent: '最近使用', name: '商品名称', added: '添加顺序' };
const hasFilter = !!state.pickSearch || state.pickCat !== '全部';
const catMenu = cats.map(c => `<div class="mi${state.pickCat === c ? ' selected' : ''}" onclick="_wiz.setCat('${esc(c)}')">
<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6"><polyline points="3 8 7 12 13 4"/></svg>
<span>${esc(c)}</span>
</div>`).join('');
const sortMenu = Object.keys(sortLabels).map(k => `<div class="mi${state.pickSort === k ? ' selected' : ''}" onclick="_wiz.setSort('${k}')">
<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6"><polyline points="3 8 7 12 13 4"/></svg>
<span>${sortLabels[k]}</span>
</div>`).join('');
const cards = pageList.map(productCardHTML).join('');
const createCard = `<div class="pp-create-card" onclick="_wiz.openNewProduct()">
<div class="pc-plus"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg></div>
<div class="pc-t">创建新商品</div>
<div class="pc-d">// 在此添加一个新商品</div>
</div>`;
const gridContent = total === 0
? (createCard + `<div class="pp-empty">// NO MATCH<br>没有符合筛选条件的商品 <span class="reset" onclick="_wiz.clearPickFilters()">[ 清空筛选 ]</span></div>`)
: (createCard + cards);
return `<div class="wiz-pane active" data-step="1">
<div class="wiz-step-h">
<h2>第 1 步 · 选择商品</h2>
<p>从商品库选一个 SKU。它的主图与卖点会被 LLM 作为脚本/资产生成的素材。</p>
</div>
<div class="pp-toolbar">
<div class="search-inline">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" placeholder="搜索商品名称、标签" value="${esc(state.pickSearch)}" oninput="_wiz.setSearch(this.value)">
</div>
<div class="pp-chip-wrap" data-key="cat">
<button class="pp-chip${catChipActive ? ' active' : ''}" type="button" onclick="event.stopPropagation(); this.parentElement.classList.toggle('open'); document.querySelectorAll('.pp-chip-wrap.open').forEach(w=>{if(w!==this.parentElement)w.classList.remove('open')})">
<span>${state.pickCat === '全部' ? '全部分类' : esc(state.pickCat)}</span>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="pp-menu">${catMenu}</div>
</div>
${hasFilter ? `<button class="pp-clear" type="button" onclick="_wiz.clearPickFilters()">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>
清空筛选
</button>` : ''}
</div>
<div class="pp-result-meta">// 显示 ${pageList.length} / ${total} 个商品${hasFilter ? ' (已筛选)' : ''}</div>
<div class="pp-grid${state.pickView === 'list' ? ' list-view' : ''}">${gridContent}</div>
${total > pageSize ? pickerPagerHTML(total) : ''}
<div class="pp-bottom-tip">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/></svg>
<span>找不到想要的商品?可<a onclick="_wiz.openNewProduct()">创建新商品</a>,或前往 <a href="products.html">商品库 · 管理商品</a></span>
</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="config-row">
<div class="field">
<label class="field-label">项目名称<span class="req">*</span></label>
<input class="input" value="${esc(state.projectName)}" oninput="_wiz.setName(this.value)">
</div>
<div class="field">
<label class="field-label">视频时长<span class="req">*</span></label>
<select class="input duration-select" onchange="_wiz.setDur(this.value)">
<option value="" disabled ${state.duration ? '' : 'selected'}>选择时长</option>
${DURATIONS.map(d => `<option value="${esc(d.id)}" ${state.duration === d.id ? 'selected' : ''}>${esc(d.label)} · ${d.shots[0]}-${d.shots[1]} 镜</option>`).join('')}
</select>
</div>
</div>
<div class="field">
<label class="field-label">脚本风格</label>
<div class="opt-row cols-4">
${STYLES.map(s => `<div class="opt-card${state.scriptStyle === s.id ? ' selected' : ''}" onclick="_wiz.setStyle('${s.id}')">
<h4>${esc(s.name)}</h4>
<div class="note">${esc(s.note)}</div>
${s.tag ? `<span class="badge">[ ${esc(s.tag)} ]</span>` : ''}
</div>`).join('')}
</div>
</div>
<div class="field">
<label class="field-label">人物设定</label>
<div class="opt-row cols-6">
${PERSONAS.map(p => `<div class="opt-card${state.persona === p.id ? ' selected' : ''}" onclick="_wiz.setPersona('${p.id}')">
<h4>${esc(p.name)}</h4>
<div class="sub">${esc(p.sub)}</div>
<div class="metric"><span class="val">${esc(p.metric)}</span></div>
</div>`).join('')}
</div>
${showReco ? `<div class="reco-bubble">
<span class="ic">${ICONS.bulb}</span>
<div class="txt">
<span>抖音同人设 TOP 视频更常用 <strong>${esc(recoDur.label)}</strong> + <strong>${esc(recoStyle.name)}</strong></span>
<span class="meta">当前 ${esc(durObj.label)} · ${esc(styleObj.name)} → 推荐换为同人设最优组合</span>
</div>
<button class="btn-apply" onclick="_wiz.applyPreset()">一键套用</button>
<button class="dismiss" onclick="_wiz.dismissReco()" aria-label="忽略">${ICONS.x}</button>
</div>` : ''}
</div>
${Object.keys(state.points).length > 0 ? `<div class="field" style="margin-bottom: 0;">
<label class="field-label">关键卖点(可勾选要重点突出的)</label>
<div class="theme-pill-row">
${Object.entries(state.points).map(([k, v]) => `<button class="theme-pill${v ? ' active' : ''}" type="button" aria-pressed="${v ? 'true' : 'false'}" onclick="_wiz.togglePt('${esc(k).replace(/'/g, "\\'")}')">${v ? ICONS.check : ''}<span>${esc(k)}</span></button>`).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 (旧 .search-input → 新 .pp-toolbar .search-inline)
const inp = body.querySelector('.pp-toolbar .search-inline input')
|| body.querySelector('.search-input input');
if (inp && state.pickSearch && document.activeElement !== inp) {
inp.focus();
const v = inp.value;
try { inp.setSelectionRange(v.length, v.length); } catch (e) {}
}
}
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();
});
// 全局点击 → 关闭 picker chip 菜单
document.addEventListener('click', e => {
if (!e.target.closest('.pp-chip-wrap')) {
document.querySelectorAll('.pp-chip-wrap.open').forEach(w => w.classList.remove('open'));
}
});
// initial render
render();
})();
</script>
</body>
</html>