All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
1605 lines
99 KiB
HTML
1605 lines
99 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>新建项目 · 流·Studio</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
|
||
<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: 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 { 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; }
|
||
|
||
/* ============================================================
|
||
第 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; }
|
||
.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/shell.js?v=202605211643"></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 按此扩写。推荐 5–30 字。',
|
||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>' },
|
||
{ id: 'manual', name: '自带脚本', tag: '我已有稿', desc: '粘贴或上传完整脚本,系统按镜头自动切分并适配商品。',
|
||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M9 13h6M9 17h6"/></svg>' },
|
||
];
|
||
|
||
const DURATIONS = [
|
||
{ id: '0-10', label: '0-10 秒', shots: [3, 4], tag: '黄金完播', completion: 52, conversion: 1.6 },
|
||
{ id: '0-15', label: '0-15 秒', shots: [4, 5], tag: '完播率最佳', completion: 42, conversion: 1.8 },
|
||
{ id: '0-30', label: '0-30 秒', shots: [6, 8], tag: '卖点详解', completion: 32, conversion: 2.1 },
|
||
{ id: '0-60', label: '0-60 秒', shots: [10, 12], tag: '故事化', completion: 26, conversion: 2.4 },
|
||
];
|
||
|
||
const STYLES = [
|
||
{ id: 'pain', name: '痛点种草', note: '用户痛点切入,以「我懂你」的口吻引出产品。', tag: '最常用', flow: ['痛点', '共鸣', '产品', '效果', '引导'] },
|
||
{ id: 'review', name: '开箱测评', note: '朋友式分享,从开箱到使用感受娓娓道来。', flow: ['开箱', '首印象', '试用', '对比', '结论'] },
|
||
{ id: 'compare', name: '对比展示', note: '「用前 vs 用后 / 同类 vs 本品」直观呈现。', flow: ['对照', '差距', '本品', '数据', '购买'] },
|
||
];
|
||
|
||
const PERSONAS = [
|
||
{ id: 'urban', name: '都市白领女性', sub: '25-30 岁', metric: '大盘消费力', defaults: { duration: '0-15', style: 'pain' } },
|
||
{ id: 'bestie', name: '闺蜜种草', sub: '邻家女孩', metric: '复购最高', defaults: { duration: '0-15', style: 'pain' } },
|
||
{ id: 'ceo', name: '总裁亲选', sub: '创始人 IP', metric: '30 万销额案例', defaults: { duration: '0-30', style: 'pain' } },
|
||
{ id: 'reviewer', name: '专业测评师', sub: '垂类达人', metric: '互动 +30%', defaults: { duration: '0-30', style: 'review' } },
|
||
{ id: 'mom', name: '实用宝妈', sub: '家庭决策者', metric: '母婴/家清稳', defaults: { duration: '0-30', style: 'pain' } },
|
||
{ id: 'genz', name: '学生党', sub: 'Z 世代 18-24', metric: '平价快消', defaults: { duration: '0-10', style: 'compare' } },
|
||
];
|
||
|
||
/* ---------- 合并其它页面创建的商品 ---------- */
|
||
// 商品库 / 工作台 / 新建项目 都共用同一 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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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.5" 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.5" 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.5" 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 => { 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">推荐 5–30 字。这句话会作为 LLM 扩写的锚点,越具体越聚焦。</div>
|
||
</div>
|
||
</div>`;
|
||
} else if (s.id === 'manual') {
|
||
detail = `<div class="source-detail">
|
||
<div class="sd-h">// 已选 · <b>${esc(s.name)}</b></div>
|
||
<div class="field" style="margin-bottom: 0;">
|
||
<label class="field-label">粘贴脚本内容<span class="req">*</span></label>
|
||
<textarea class="textarea" placeholder="粘贴你的脚本内容(旁白 / 镜头描述均可,系统会自动切分镜头)" oninput="_wiz.setScript(this.value)">${esc(state.manualScript)}</textarea>
|
||
<div class="field-hint">最少 20 字。镜头数由你的脚本自然段落决定,时长 / 风格仍会影响后期渲染节奏。</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
}
|
||
return `<div class="wiz-pane active" data-step="2">
|
||
<div class="wiz-step-h">
|
||
<h2>第 2 步 · 脚本来源</h2>
|
||
<p>决定 LLM 如何获得初稿脚本。三种方式由「最省事」到「最保真原意」。</p>
|
||
</div>
|
||
<div class="source-row">
|
||
${SOURCES.map(s => `<div class="source-card${state.sourceId === s.id ? ' selected' : ''}" onclick="_wiz.selectSource('${s.id}')">
|
||
<span class="src-ic">${s.icon}</span>
|
||
<h4>${esc(s.name)}</h4>
|
||
<span class="src-tag">[ ${esc(s.tag)} ]</span>
|
||
<p class="src-desc">${esc(s.desc)}</p>
|
||
</div>`).join('')}
|
||
</div>
|
||
${detail}
|
||
</div>`;
|
||
}
|
||
|
||
function renderStep3() {
|
||
const personaObj = getPersona(), durObj = getDuration(), styleObj = getStyle();
|
||
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 (旧 .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>
|