AirShelf/电商AI平台/product-create.legacy.html
UI 设计 868ba69ea4
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
fix(cache): 给 shell.js/restraint.css 加 ?v=时间戳 强制绕过浏览器旧缓存
2026-05-21 16:44:10 +08:00

3884 lines
141 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>新建商品 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<style>
/* ============================================================
V2.1 product-create · 全屏新建商品工作台
- 顶部模式切换:[ 我有商品图 ] / [ AI 帮我生成图 ]
- 左侧 step 导航 + 已选素材
- 中央 canvas
- 右侧 controls
- Step 3 内嵌子 Tab(① 挑选模特 → ② 生成上身图)
============================================================ */
body { background: var(--background-base); }
.wb {
height: 100vh;
display: grid;
grid-template-rows: 56px 1fr;
background: var(--background-base);
}
/* upload-mode-content 已废弃,被独立的 product-create-upload.html 替代 */
#upload-mode-content, #upload-steps { display: none !important; }
/* ─── Top bar ─── */
.wb-top {
padding: 0 24px;
background: var(--surface);
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center; gap: 18px;
}
.wb-top .home {
display: flex; align-items: center; gap: 8px;
color: var(--black-alpha-56);
font-size: 13px;
cursor: pointer;
transition: color var(--t-base);
}
.wb-top .home:hover { color: var(--accent-black); }
.wb-top .home svg { width: 14px; height: 14px; }
.wb-top .ti {
font-size: 15px; font-weight: 600;
color: var(--accent-black);
display: flex; align-items: center; gap: 10px;
}
.wb-top .ti .name-edit {
background: transparent;
border: 1px solid transparent;
border-radius: var(--r-md);
padding: 4px 10px;
font-size: 15px; font-weight: 600;
color: var(--accent-black);
font-family: inherit;
width: 280px;
transition: border-color var(--t-base), background var(--t-base);
}
.wb-top .ti .name-edit:hover { border-color: var(--black-alpha-12); background: var(--black-alpha-3); }
.wb-top .ti .name-edit:focus { border-color: var(--heat-40); background: var(--surface); box-shadow: inset 0 0 0 1px var(--heat-40); }
.wb-top .ti .mode-pill {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--heat);
background: var(--heat-12);
border: 1px solid var(--heat-20);
border-radius: var(--r-sm);
padding: 2px 8px;
letter-spacing: .04em;
font-weight: 500;
}
.wb-top .right { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.wb-top .x {
width: 36px; height: 36px;
display: grid; place-items: center;
cursor: pointer;
color: var(--black-alpha-56);
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
}
.wb-top .x:hover { color: var(--accent-crimson); background: var(--crimson-bg); border-color: var(--crimson-bd); }
.wb-top .x svg { width: 14px; height: 14px; }
/* ─── Mode switch row ─── */
.mode-row {
padding: 14px 24px;
background: var(--background-lighter);
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center; gap: 14px;
}
.mode-row .lbl {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .04em;
flex-shrink: 0;
}
.mode-toggle {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-md);
padding: 3px;
gap: 2px;
}
.mode-toggle button {
padding: 6px 14px;
background: transparent;
border: 0;
border-radius: 6px;
font-size: 12.5px; font-weight: 500;
color: var(--black-alpha-56);
cursor: pointer;
font-family: inherit;
display: flex; align-items: center; gap: 6px;
transition: background var(--t-base), color var(--t-base);
}
.mode-toggle button:hover { color: var(--accent-black); }
.mode-toggle button.active { background: var(--heat); color: var(--accent-white); font-weight: 600; }
.mode-toggle button svg { width: 13px; height: 13px; }
.mode-row .mode-hint {
margin-left: auto;
font-size: 12.5px;
color: var(--black-alpha-56);
display: flex; align-items: center; gap: 8px;
}
.mode-row .mode-hint .mono {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .04em;
}
/* ─── Main split ─── */
.wb-main { display: grid; grid-template-columns: 280px 1fr 360px; min-height: 0; }
/* ─── Left sidebar ─── */
.wb-side {
border-right: 1px solid var(--border-faint);
background: var(--background-base);
overflow-y: auto;
padding: 18px 0;
}
.step-item {
padding: 14px 20px;
display: flex; align-items: flex-start; gap: 12px;
cursor: pointer;
border-left: 3px solid transparent;
position: relative;
transition: background var(--t-base);
}
.step-item:hover { background: var(--black-alpha-4); }
.step-item.active { background: var(--heat-12); border-left-color: var(--heat); }
.step-item.locked { opacity: .55; cursor: not-allowed; }
.step-item.locked:hover { background: transparent; }
.step-item .num {
width: 26px; height: 26px;
border: 1px solid var(--black-alpha-12);
background: var(--surface);
color: var(--black-alpha-48);
font-family: var(--font-mono); font-size: 11px; font-weight: 700;
border-radius: var(--r-md);
display: grid; place-items: center;
flex-shrink: 0;
}
.step-item.done .num { background: var(--accent-black); color: var(--accent-white); border-color: var(--accent-black); }
.step-item.active .num { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }
.step-item .info { flex: 1; min-width: 0; }
.step-item .ti2 { font-size: 13.5px; font-weight: 600; color: var(--black-alpha-56); }
.step-item.active .ti2 { color: var(--accent-black); }
.step-item.done .ti2 { color: var(--accent-black); }
.step-item .sub {
font-size: 11.5px; color: var(--black-alpha-48);
margin-top: 4px;
font-family: var(--font-mono);
letter-spacing: .02em;
line-height: 1.5;
}
.step-item.done .sub { color: var(--black-alpha-56); font-family: var(--font-sans); letter-spacing: 0; }
/* Sub items under active step */
.substep-list { padding: 4px 20px 8px 56px; }
.substep-item {
display: flex; align-items: center; gap: 10px;
padding: 8px 10px;
cursor: pointer;
font-size: 12.5px; color: var(--black-alpha-48);
border-left: 2px solid var(--border-faint);
margin-left: -4px;
transition: color var(--t-base), border-color var(--t-base);
}
.substep-item:hover { color: var(--black-alpha-56); border-left-color: var(--black-alpha-24); }
.substep-item.active { color: var(--heat); border-left-color: var(--heat); font-weight: 600; }
.substep-item.done { color: var(--accent-black); }
.substep-item .num-mini {
width: 18px; height: 18px;
border: 1px solid currentColor;
border-radius: var(--r-sm);
font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;
display: grid; place-items: center;
flex-shrink: 0;
}
.substep-item.active .num-mini, .substep-item.done .num-mini {
background: currentColor;
color: var(--accent-white);
}
.substep-item.done .num-mini { background: var(--accent-black); border-color: var(--accent-black); }
.side-divider { height: 1px; background: var(--border-faint); margin: 14px 20px; }
/* step-item 标签 (必须/附加) */
.step-tag-mini {
font-family: var(--font-mono);
font-size: 9.5px;
padding: 1px 6px;
border-radius: var(--r-sm);
margin-left: 4px;
letter-spacing: .04em;
font-weight: 500;
vertical-align: middle;
}
.step-tag-mini.req { background: var(--heat-12); color: var(--heat); }
.step-tag-mini.opt { background: var(--black-alpha-8); color: var(--black-alpha-56); }
.step-item.done .step-tag-mini.req { background: var(--forest-bg); color: var(--accent-forest); }
/* 附加资产 · 并列样式 (◇ 而非数字) */
.step-item.extra .num {
background: var(--background-lighter);
color: var(--black-alpha-48);
border: 1px dashed var(--black-alpha-24);
font-family: var(--font-mono);
font-size: 14px;
}
.step-item.extra.active {
background: var(--background-lighter);
border-left-color: var(--black-alpha-32);
}
.step-item.extra.active .num {
background: var(--accent-black);
color: var(--accent-white);
border-style: solid;
border-color: var(--accent-black);
}
.step-item.extra.done .num {
background: var(--forest-bg);
color: var(--accent-forest);
border: 1px solid var(--accent-forest);
}
.extra-hint {
padding: 0 20px 8px;
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .02em;
line-height: 1.6;
}
/* 资产库空态 */
.sel-asset-empty {
padding: 8px 20px;
font-size: 11px;
color: var(--black-alpha-32);
letter-spacing: .04em;
}
/* 原图缩略图横排 */
.side-orig-thumbs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
margin-bottom: 8px;
}
.side-orig-thumbs .ot {
aspect-ratio: 1;
border-radius: var(--r-sm);
background: var(--background-lighter);
border: 1px solid var(--border-faint);
overflow: hidden;
}
.side-orig-thumbs .ot img { width: 100%; height: 100%; object-fit: cover; }
/* 资产库子分组 */
.asset-group {
margin-bottom: 12px;
}
.asset-group .ag-h {
font-size: 10.5px;
font-family: var(--font-mono);
color: var(--black-alpha-48);
letter-spacing: .04em;
margin-bottom: 4px;
display: flex; align-items: center; justify-content: space-between;
}
.asset-group .ag-h .v-count { color: var(--heat); }
.asset-group .ag-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.asset-group .ag-item {
aspect-ratio: 1;
border-radius: var(--r-sm);
background: var(--background-lighter);
border: 1px solid var(--border-faint);
overflow: hidden;
position: relative;
cursor: zoom-in;
}
.asset-group .ag-item.current { border-color: var(--heat); border-width: 1.5px; }
.asset-group .ag-item img { width: 100%; height: 100%; object-fit: cover; }
.asset-group .ag-item .v-tag {
position: absolute;
bottom: 2px; left: 2px;
background: rgba(0,0,0,.65);
color: #fff;
font-family: var(--font-mono);
font-size: 8.5px;
padding: 1px 4px;
border-radius: 2px;
letter-spacing: .04em;
}
.side-section-h {
padding: 4px 20px 8px;
font-family: var(--font-mono);
font-size: 10.5px; color: var(--black-alpha-48);
letter-spacing: .08em; text-transform: uppercase;
font-weight: 600;
}
.selected-assets-side { padding: 0 20px; }
.sel-asset-label {
font-size: 11.5px;
color: var(--black-alpha-48);
margin-bottom: 6px;
font-family: var(--font-mono);
letter-spacing: .02em;
}
.sel-asset-thumb { aspect-ratio: 4/5; margin-bottom: 14px; }
.sel-asset-thumb.square { aspect-ratio: 1; }
/* ─── Center canvas ─── */
.wb-canvas {
background: var(--background-base);
overflow-y: auto;
padding: 0;
display: flex; flex-direction: column;
min-width: 0;
}
/* AI mode 容器也要 flex column 才能让 canvas-inner 撑满高度 */
#ai-mode-content {
flex: 1; min-height: 0;
display: flex; flex-direction: column;
}
/* Sub-tab strip on top (only in Step 3) */
.subtab-strip {
padding: 0 32px;
border-bottom: 1px solid var(--border-faint);
background: var(--surface);
display: flex; align-items: center; gap: 0;
position: sticky; top: 0; z-index: 5;
}
.subtab {
padding: 14px 18px;
font-size: 13px; font-weight: 500;
color: var(--black-alpha-56);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
cursor: pointer;
display: flex; align-items: center; gap: 8px;
transition: color var(--t-base), background var(--t-base);
border-radius: var(--r-md) var(--r-md) 0 0;
}
.subtab:hover { color: var(--accent-black); background: var(--black-alpha-4); }
.subtab.active { color: var(--accent-black); border-bottom-color: var(--heat); }
.subtab.disabled { color: var(--black-alpha-32); cursor: not-allowed; }
.subtab.disabled:hover { background: transparent; color: var(--black-alpha-32); }
.subtab .num-circ {
width: 20px; height: 20px;
border: 1px solid currentColor;
border-radius: 50%;
font-family: var(--font-mono); font-size: 10px; font-weight: 700;
display: grid; place-items: center;
}
.subtab.done .num-circ { background: var(--accent-black); color: var(--accent-white); border-color: var(--accent-black); }
.subtab.active .num-circ { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }
.subtab .arrow { color: var(--black-alpha-32); padding: 0 6px; }
.subtab-meta {
margin-left: auto;
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .04em;
}
.subtab-meta .accent { color: var(--heat); font-weight: 700; }
/* Canvas inner */
.canvas-inner {
padding: 32px 32px 0;
flex: 1; min-height: 0;
max-width: 1280px; width: 100%;
display: flex; flex-direction: column;
}
.canvas-h {
margin-bottom: 24px;
display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
}
.canvas-h h2 { font-size: 24px; font-weight: 600; letter-spacing: -.018em; line-height: 1.25; color: var(--accent-black); }
.canvas-h p { font-size: 13.5px; color: var(--black-alpha-56); margin-top: 8px; line-height: 1.6; }
.canvas-h .step-tag {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
padding: 4px 10px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
letter-spacing: .04em;
flex-shrink: 0;
}
/* Step pane switching */
.step-pane { display: none; }
.step-pane.active {
display: flex; flex-direction: column;
flex: 1; min-height: 0;
animation: fade .2s ease;
}
@keyframes fade {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.sub-pane { display: none; }
.sub-pane.active {
display: flex; flex-direction: column;
flex: 1; min-height: 0;
animation: fade .2s ease;
}
/* ─── Step 1: 商品信息 + 上传原图 ─── */
.form-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 28px;
max-width: 920px;
}
.s1-grid { display: grid; grid-template-columns: 280px 1fr; gap: 28px; }
.s1-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
.s1-upload-card { display: flex; flex-direction: column; }
@media (max-width: 1100px) { .s1-2col { grid-template-columns: 1fr; } }
/* Step 1 / 4 共用 upload-zone */
.s1-upload-card .upload-zone, .step-pane .upload-zone {
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
padding: 22px 20px;
text-align: center;
background: var(--background-lighter);
color: var(--black-alpha-56);
font-size: 13px;
cursor: pointer;
display: flex; flex-direction: column; align-items: center; gap: 4px;
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
}
.step-pane .upload-zone:hover { border-color: var(--heat); background: var(--heat-8); color: var(--heat); }
.step-pane .upload-zone:hover .uz-ic { background: var(--heat); color: #fff; border-color: var(--heat); }
.step-pane .upload-zone strong { color: var(--heat); font-weight: 600; }
.step-pane .upload-zone .uz-ic {
width: 40px; height: 40px;
border-radius: var(--r-md);
background: var(--surface);
color: var(--heat);
border: 1px solid var(--heat-20);
display: grid; place-items: center;
margin-bottom: 6px;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
}
.step-pane .upload-zone .uz-ic svg { width: 18px; height: 18px; }
.step-pane .upload-zone .uz-hint { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
.step-pane .upload-zone.full { cursor: not-allowed; opacity: .55; pointer-events: none; }
/* Step 1 上传 grid · 5 列复用 */
.step-pane .upload-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 5px;
}
.step-pane .up-thumb {
aspect-ratio: 1;
border-radius: var(--r-sm);
overflow: hidden;
position: relative;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
cursor: zoom-in;
transition: transform var(--t-fast), border-color var(--t-base);
}
.step-pane .up-thumb:hover { transform: scale(1.02); border-color: var(--heat); }
.step-pane .up-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.step-pane .up-thumb .slot-x {
position: absolute; top: 3px; right: 3px;
width: 18px; height: 18px;
background: rgba(0,0,0,.7); color: #fff;
border-radius: 50%; display: none; place-items: center;
cursor: pointer; z-index: 3; border: 0; padding: 0;
transition: background var(--t-base), transform var(--t-fast);
}
.step-pane .up-thumb:hover .slot-x { display: grid; }
.step-pane .up-thumb .slot-x:hover { background: var(--accent-crimson); transform: scale(1.1); }
.step-pane .up-thumb .slot-x svg { width: 9px; height: 9px; }
/* Step 2 · 三视图大图区 */
.triview-stage {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 18px;
max-width: 960px;
}
.triview-display {
aspect-ratio: 3 / 1;
border-radius: var(--r-md);
overflow: hidden;
border: 1px solid var(--border-faint);
position: relative;
background: var(--background-lighter);
}
.triview-display .placeholder { width: 100%; height: 100%; border: 0; border-radius: 0; }
.triview-display img { width: 100%; height: 100%; object-fit: cover; display: block; }
.triview-vtag {
position: absolute;
top: 12px; right: 12px;
background: rgba(0,0,0,.7);
color: #fff;
font-family: var(--font-mono);
font-size: 11px;
padding: 4px 10px;
border-radius: var(--r-sm);
letter-spacing: .04em;
}
/* 版本历史 */
.triview-versions {
margin-top: 24px;
max-width: 960px;
}
.triview-versions .tv-h {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .04em;
margin-bottom: 10px;
}
.triview-versions .tv-list {
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 4px;
}
.triview-versions .tv-item {
flex-shrink: 0;
width: 180px;
aspect-ratio: 3 / 1;
border-radius: var(--r-md);
overflow: hidden;
border: 1px solid var(--border-faint);
position: relative;
cursor: pointer;
background: var(--background-lighter);
transition: border-color var(--t-base);
}
.triview-versions .tv-item:hover { border-color: var(--black-alpha-32); }
.triview-versions .tv-item.current { border-color: var(--heat); border-width: 1.5px; box-shadow: 0 0 0 3px var(--heat-12); }
.triview-versions .tv-item .tv-vtag {
position: absolute; bottom: 4px; left: 4px;
background: rgba(0,0,0,.65); color: #fff;
font-family: var(--font-mono); font-size: 10px;
padding: 2px 6px; border-radius: 2px;
letter-spacing: .04em;
}
.triview-versions .tv-empty {
padding: 14px;
text-align: center;
color: var(--black-alpha-48);
font-size: 12px;
background: var(--surface);
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
font-family: var(--font-mono);
letter-spacing: .02em;
}
/* Step 4 · 平台选择 */
.platform-picker { max-width: 920px; }
.platform-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin: 10px 0 12px;
}
.plat-card {
display: flex; align-items: center; gap: 10px;
padding: 12px 14px;
background: var(--surface);
border: 1.5px solid var(--border-faint);
border-radius: var(--r-md);
cursor: pointer;
user-select: none;
transition: border-color var(--t-base), background var(--t-base);
}
.plat-card:hover { border-color: var(--black-alpha-32); }
.plat-card input[type="checkbox"] { accent-color: var(--heat); width: 14px; height: 14px; cursor: pointer; }
.plat-card input[type="checkbox"]:checked + .plat-info .plat-name { color: var(--heat); }
.plat-card:has(input:checked) { border-color: var(--heat); background: var(--heat-8); }
.plat-info { display: flex; flex-direction: column; gap: 3px; }
.plat-name { font-size: 13px; font-weight: 600; color: var(--accent-black); }
.plat-spec { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.platform-cost { font-size: 11px; color: var(--black-alpha-48); letter-spacing: .04em; }
/* Step 4 · 平台生成结果 */
.platform-results { margin-top: 18px; max-width: 920px; }
.platform-empty {
padding: 36px 20px;
text-align: center;
color: var(--black-alpha-48);
font-size: 12.5px;
background: var(--surface);
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
letter-spacing: .04em;
}
.platform-block {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 16px 18px;
margin-bottom: 12px;
}
.platform-block .pb-h {
display: flex; align-items: center; gap: 10px;
margin-bottom: 12px;
}
.platform-block .pb-h .pb-name { font-size: 14px; font-weight: 600; color: var(--accent-black); }
.platform-block .pb-h .pb-spec { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); }
.platform-block .pb-h .pb-regen {
margin-left: auto;
height: 28px; padding: 0 10px;
font-size: 12px;
background: transparent;
border: 1px solid var(--border-faint);
color: var(--black-alpha-72);
border-radius: var(--r-md);
cursor: pointer;
display: inline-flex; align-items: center; gap: 5px;
font-family: inherit;
}
.platform-block .pb-h .pb-regen:hover { border-color: var(--heat); color: var(--heat); }
.platform-block .pb-h .pb-regen svg { width: 11px; height: 11px; }
.platform-block .pb-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.platform-block .pb-card {
aspect-ratio: 1;
border-radius: var(--r-sm);
border: 1px solid var(--border-faint);
background: var(--background-lighter);
position: relative;
overflow: hidden;
cursor: zoom-in;
transition: border-color var(--t-base);
}
.platform-block .pb-card:hover { border-color: var(--heat); }
.platform-block .pb-card .placeholder { width: 100%; height: 100%; border: 0; border-radius: 0; }
.platform-block .pb-card img { width: 100%; height: 100%; object-fit: cover; }
.platform-block .pb-card .pb-x {
position: absolute; top: 4px; right: 4px;
width: 22px; height: 22px;
background: rgba(0,0,0,.65); color: #fff;
border: 0; border-radius: 50%;
display: none; place-items: center;
cursor: pointer;
transition: background var(--t-base);
}
.platform-block .pb-card:hover .pb-x { display: grid; }
.platform-block .pb-card .pb-x:hover { background: var(--accent-crimson); }
.platform-block .pb-card .pb-x svg { width: 11px; height: 11px; }
.platform-block .pb-card .pb-vtag {
position: absolute; bottom: 4px; left: 4px;
background: rgba(0,0,0,.65); color: #fff;
font-family: var(--font-mono); font-size: 9.5px;
padding: 2px 5px; border-radius: 2px;
letter-spacing: .04em;
}
/* Step 5 · 资产分组清单 */
.rv-assets { margin: 18px 0 8px; }
.rv-asset-group { margin-bottom: 16px; }
.rv-asset-group .rvg-h {
display: flex; align-items: center; gap: 8px;
font-size: 13px; font-weight: 500;
color: var(--accent-black);
margin-bottom: 8px;
}
.rv-asset-group .rvg-h .count {
font-family: var(--font-mono); font-size: 11px;
color: var(--heat); font-weight: 600;
}
.rv-asset-group .rvg-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 6px;
}
.rv-asset-group .rvg-item {
aspect-ratio: 1;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
overflow: hidden;
cursor: zoom-in;
position: relative;
transition: border-color var(--t-base);
}
.rv-asset-group .rvg-item:hover { border-color: var(--heat); }
.rv-asset-group .rvg-item img { width: 100%; height: 100%; object-fit: cover; }
.rv-asset-group .rvg-item .placeholder { width: 100%; height: 100%; border: 0; border-radius: 0; }
.rv-asset-group .rvg-item .rvg-tag {
position: absolute; bottom: 3px; left: 3px;
background: rgba(0,0,0,.65); color: #fff;
font-family: var(--font-mono); font-size: 9px;
padding: 1px 4px; border-radius: 2px;
letter-spacing: .04em;
}
.rv-asset-empty {
padding: 18px;
text-align: center;
color: var(--black-alpha-48);
font-family: var(--font-mono); font-size: 11.5px;
background: var(--background-lighter);
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
letter-spacing: .04em;
}
.upload-c {
aspect-ratio: 4/5;
background: var(--surface);
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
display: grid; place-items: center;
text-align: center;
cursor: pointer;
color: var(--black-alpha-56);
margin-bottom: 12px;
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
padding: 24px;
}
.upload-c:hover {
border-color: var(--heat);
background: var(--heat-8);
color: var(--heat);
}
.upload-c.has-file {
border-style: solid; padding: 0;
border-color: var(--heat-40);
background: var(--background-lighter);
}
.upload-c.has-file .placeholder { width: 100%; height: 100%; }
.upload-c .ic-cam {
width: 44px; height: 44px;
border: 1px solid currentColor;
border-radius: var(--r-md);
display: grid; place-items: center;
margin: 0 auto 14px;
}
.upload-c .ic-cam svg { width: 22px; height: 22px; }
.up-hint {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
text-align: center;
letter-spacing: .02em;
}
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.bullet-list { list-style: none; padding: 0; }
.bullet-list li {
display: flex; gap: 10px; align-items: center;
padding: 10px 12px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
margin-bottom: 6px;
font-size: 13px;
color: var(--accent-black);
transition: border-color var(--t-base), background var(--t-base);
}
.bullet-list li.bl-item { cursor: text; }
.bullet-list li.bl-item:hover { border-color: var(--black-alpha-24); }
.bullet-list li.bl-add { background: var(--surface); border-style: dashed; }
.bullet-list li.bl-add:focus-within { border-color: var(--heat-40); background: var(--surface); }
.bullet-list .num {
width: 20px; height: 20px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-size: 11px; color: var(--black-alpha-56);
display: grid; place-items: center;
flex-shrink: 0;
font-family: var(--font-mono);
font-weight: 600;
}
.bullet-list li.bl-add .num { background: transparent; color: var(--heat); border-color: var(--heat-40); }
.bullet-list .bl-text { flex: 1; min-width: 0; }
.bullet-list .bl-input {
flex: 1; min-width: 0;
height: 24px; border: 0; padding: 0 4px;
background: transparent; font-size: 13px;
color: var(--accent-black); font-family: inherit;
outline: none;
}
.bullet-list .bl-input::placeholder { color: var(--black-alpha-48); }
.bullet-list .bl-x {
width: 24px; height: 24px;
display: grid; place-items: center;
color: var(--black-alpha-32);
border-radius: var(--r-sm);
cursor: pointer;
flex-shrink: 0;
opacity: 0;
transition: opacity var(--t-base), background var(--t-base), color var(--t-base);
}
.bullet-list li.bl-item:hover .bl-x { opacity: 1; }
.bullet-list .bl-x:hover { background: var(--crimson-bg); color: var(--accent-crimson); }
.bullet-list .bl-x svg { width: 11px; height: 11px; }
/* ─── Step 2: 头图 4 选 1 ─── */
.big-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
max-width: 760px;
}
.big-card {
aspect-ratio: 1;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
cursor: pointer;
position: relative;
overflow: hidden;
transition: border-color var(--t-base), transform var(--t-fast);
}
.big-card:hover { border-color: var(--black-alpha-32); }
.big-card .placeholder { width: 100%; height: 100%; border: 0; border-radius: 0; }
.big-card.selected {
border: 2px solid var(--heat);
box-shadow: 0 0 0 4px var(--heat-12);
}
.big-card.selected::after {
content: '';
position: absolute; top: 12px; right: 12px;
width: 28px; height: 28px;
background: var(--heat);
color: var(--accent-white);
border-radius: 50%;
display: grid; place-items: center;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M3.5 8.5l3 3 6-6' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-size: 16px;
background-repeat: no-repeat;
background-position: center;
}
.big-card .corner-info {
position: absolute; bottom: 12px; left: 12px;
background: rgba(255,255,255,.95);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
padding: 4px 10px;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: .04em;
color: var(--black-alpha-56);
}
.regen-line {
margin-top: 20px;
display: flex; align-items: center; gap: 12px;
}
.regen-line .info {
margin-left: auto;
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
/* ─── Step 3 SUB 1: 模特库网格 ─── */
.filter-bar {
display: flex; align-items: center; gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.search-input { position: relative; width: 280px; }
.search-input svg {
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
color: var(--black-alpha-56); width: 14px; height: 14px;
z-index: 2; pointer-events: none;
}
.search-input input {
padding-left: 36px;
height: 32px;
font-size: 12.5px;
background: var(--surface);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-md);
color: var(--accent-black);
outline: none;
width: 100%;
transition: border-color var(--t-base);
}
.search-input input:focus { border-color: var(--heat-40); box-shadow: inset 0 0 0 1px var(--heat-40); }
.filter-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);
display: inline-flex; align-items: center; gap: 5px;
cursor: pointer;
font-family: inherit;
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.filter-chip:hover { background: var(--black-alpha-4); border-color: var(--black-alpha-24); color: var(--accent-black); }
.filter-chip.active { border-color: var(--heat-40); color: var(--heat); background: var(--heat-12); font-weight: 600; }
.filter-chip svg { width: 11px; height: 11px; }
.results-meta {
margin-left: auto;
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .04em;
}
.results-meta .accent { color: var(--heat); font-weight: 700; }
.model-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
}
@media (max-width: 1500px) { .model-grid { grid-template-columns: repeat(5, 1fr); } }
@media (max-width: 1280px) { .model-grid { grid-template-columns: repeat(4, 1fr); } }
.model-card {
aspect-ratio: 3/4;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
cursor: pointer;
position: relative;
overflow: hidden;
transition: border-color var(--t-base);
}
.model-card:hover { border-color: var(--black-alpha-32); }
.model-card .placeholder { width: 100%; height: 100%; border: 0; border-radius: 0; }
.model-card.selected {
border: 2px solid var(--heat);
box-shadow: 0 0 0 3px var(--heat-12);
}
.model-card.selected::after {
content: '';
position: absolute; top: 8px; right: 8px;
width: 22px; height: 22px;
background: var(--heat);
color: var(--accent-white);
border-radius: 50%;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M3.5 8.5l3 3 6-6' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-size: 14px;
background-repeat: no-repeat;
background-position: center;
z-index: 2;
}
.model-card .lbl-bottom {
position: absolute; bottom: 0; left: 0; right: 0;
background: rgba(255,255,255,.96);
border-top: 1px solid var(--border-faint);
padding: 8px 10px;
display: flex; flex-direction: column; gap: 3px;
}
.model-card .lbl-bottom .nm {
font-size: 12.5px; font-weight: 600;
color: var(--accent-black);
}
.model-card .lbl-bottom .tags {
font-family: var(--font-mono);
font-size: 9.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
.model-card .pose-tag {
position: absolute; top: 8px; left: 8px;
background: rgba(0,0,0,.6);
color: var(--accent-white);
font-family: var(--font-mono);
font-size: 9px;
border-radius: var(--r-sm);
padding: 2px 6px;
letter-spacing: .04em;
z-index: 1;
}
.scroll-loader {
margin-top: 20px;
padding: 14px 18px;
background: var(--surface);
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
text-align: center;
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
/* ─── Step 3 SUB 2: 上身图 + 模特切换 ─── */
.model-switcher {
display: flex; align-items: center; gap: 8px;
margin-bottom: 20px;
padding: 12px 16px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
flex-wrap: wrap;
}
.model-switcher .lbl {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
.model-switcher .hint-right {
margin-left: auto;
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
/* ─── Step 3 SUB 1: 模特来源切换 (平台库 / 我的) ─── */
.kind-toggle {
display: flex;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 4px;
gap: 2px;
margin-bottom: 14px;
width: fit-content;
}
.kind-toggle .kt-btn {
padding: 7px 16px;
font-size: 12.5px;
color: var(--black-alpha-56);
border-radius: var(--r-sm);
cursor: pointer;
display: inline-flex; align-items: center; gap: 8px;
transition: background var(--t-base), color var(--t-base);
background: transparent;
border: 0;
font-family: inherit;
font-weight: 500;
}
.kind-toggle .kt-btn:hover { color: var(--accent-black); }
.kind-toggle .kt-btn.active {
background: var(--accent-black);
color: var(--accent-white);
}
.kind-toggle .kt-btn .kt-num {
font-family: var(--font-mono);
font-size: 10.5px;
opacity: .7;
letter-spacing: .04em;
}
/* ─── Step 3 · 模特卡片来源徽章 (替换 pose-tag) ─── */
.model-card .source-tag {
position: absolute; top: 8px; left: 8px;
background: rgba(0,0,0,.66);
color: var(--accent-white);
font-family: var(--font-mono);
font-size: 9px;
border-radius: var(--r-sm);
padding: 2px 7px;
letter-spacing: .04em;
z-index: 1;
}
.model-card.kind-mine .source-tag {
background: var(--heat);
color: var(--accent-white);
}
/* ─── Step 3 · "+ 上传新模特" 卡片 ─── */
.model-card-add {
aspect-ratio: 3/4;
background: var(--surface);
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
cursor: pointer;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 10px;
color: var(--black-alpha-56);
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
padding: 16px;
text-align: center;
}
.model-card-add:hover {
border-color: var(--heat);
background: var(--heat-8);
color: var(--heat);
}
.model-card-add .add-ic {
width: 38px; height: 38px;
background: var(--heat-12);
color: var(--heat);
border-radius: 50%;
display: grid; place-items: center;
}
.model-card-add .add-ic svg { width: 18px; height: 18px; }
.model-card-add .add-t {
font-size: 13px; font-weight: 600;
color: var(--accent-black);
}
.model-card-add .add-d {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
line-height: 1.5;
}
.model-card-add:hover .add-t { color: var(--heat); }
/* ─── 空状态: 我的模特库 ─── */
.mine-empty {
grid-column: 1 / -1;
padding: 36px 24px;
background: var(--surface);
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
text-align: center;
color: var(--black-alpha-56);
}
.mine-empty .me-t { font-size: 13.5px; font-weight: 600; color: var(--accent-black); margin-bottom: 6px; }
.mine-empty .me-d { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
/* ─── Step 3 SUB 2: 多模特上身图 (重构) ─── */
.outfits-wrap { display: flex; flex-direction: column; gap: 20px; }
.outfit-block {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 18px 20px;
}
.outfit-block .ob-h {
display: flex; align-items: center; gap: 12px;
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-faint);
}
.outfit-block .ob-mname {
font-size: 14px; font-weight: 600;
color: var(--accent-black);
display: inline-flex; align-items: center; gap: 8px;
}
.outfit-block .ob-mkind {
font-family: var(--font-mono);
font-size: 9.5px;
color: var(--black-alpha-48);
background: var(--background-lighter);
border: 1px solid var(--border-faint);
padding: 2px 7px;
border-radius: var(--r-sm);
letter-spacing: .04em;
}
.outfit-block.kind-mine .ob-mkind {
color: var(--heat);
background: var(--heat-12);
border-color: var(--heat-40);
}
.outfit-block .ob-meta {
margin-left: auto;
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
.outfit-block .ob-regen-all {
height: 30px; padding: 0 12px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
font-size: 12px;
font-family: inherit;
cursor: pointer;
color: var(--black-alpha-72);
display: inline-flex; align-items: center; gap: 6px;
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.outfit-block .ob-regen-all:hover { background: var(--heat-8); border-color: var(--heat-40); color: var(--heat); }
.outfit-block .ob-regen-all svg { width: 12px; height: 12px; }
.outfit-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
@media (max-width: 1280px) { .outfit-grid { grid-template-columns: repeat(3, 1fr); } }
.outfit-card {
aspect-ratio: 3/4;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
cursor: pointer;
position: relative;
overflow: hidden;
transition: border-color var(--t-base);
}
.outfit-card:hover { border-color: var(--black-alpha-32); }
.outfit-card .placeholder { width: 100%; height: 100%; border: 0; border-radius: 0; }
.outfit-card.selected {
border: 2px solid var(--heat);
box-shadow: 0 0 0 3px var(--heat-12);
}
.outfit-card .oc-tag {
position: absolute; bottom: 8px; left: 8px;
background: rgba(255,255,255,.95);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
padding: 2px 8px;
font-family: var(--font-mono);
font-size: 10px;
color: var(--black-alpha-56);
letter-spacing: .04em;
pointer-events: none;
}
.outfit-card .oc-actions {
position: absolute; top: 8px; right: 8px;
display: flex; gap: 4px;
opacity: 0;
transition: opacity var(--t-base);
}
.outfit-card:hover .oc-actions { opacity: 1; }
.outfit-card .oc-btn {
width: 26px; height: 26px;
background: rgba(0,0,0,.7);
color: var(--accent-white);
border: 0;
border-radius: var(--r-sm);
cursor: pointer;
display: grid; place-items: center;
transition: background var(--t-base);
}
.outfit-card .oc-btn:hover { background: var(--accent-black); }
.outfit-card .oc-btn.del:hover { background: var(--accent-crimson); }
.outfit-card .oc-btn svg { width: 12px; height: 12px; }
.outfit-empty-step {
padding: 36px 24px;
background: var(--surface);
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
text-align: center;
color: var(--black-alpha-56);
}
.outfit-empty-step .ee-t { font-size: 13.5px; font-weight: 600; color: var(--accent-black); margin-bottom: 6px; }
.outfit-empty-step .ee-d { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.outfit-empty-step .ee-btn { margin-top: 14px; }
/* ─── 新模特上传弹窗 ─── */
.nm-modal-bg {
position: fixed; inset: 0;
background: rgba(0,0,0,.42);
z-index: 200;
display: none;
align-items: center; justify-content: center;
padding: 20px;
animation: nmFade .15s ease;
}
.nm-modal-bg.open { display: flex; }
@keyframes nmFade { from { opacity: 0; } to { opacity: 1; } }
.nm-modal {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-lg);
box-shadow: 0 20px 60px rgba(0,0,0,.18);
width: 100%;
max-width: 820px;
max-height: calc(100vh - 40px);
display: flex; flex-direction: column;
overflow: hidden;
position: relative;
}
.nm-modal-h {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 24px;
border-bottom: 1px solid var(--border-faint);
}
.nm-modal-h h3 {
font-size: 17px; font-weight: 600;
color: var(--accent-black);
display: inline-flex; align-items: center; gap: 10px;
}
.nm-modal-h .nm-tag {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--heat);
background: var(--heat-12);
border: 1px solid var(--heat-40);
border-radius: var(--r-sm);
padding: 3px 9px;
letter-spacing: .04em;
}
.nm-modal-h .nm-x {
width: 28px; height: 28px;
background: transparent;
border: 0;
color: var(--black-alpha-56);
border-radius: var(--r-sm);
cursor: pointer;
display: grid; place-items: center;
transition: color var(--t-base), background var(--t-base);
}
.nm-modal-h .nm-x:hover { color: var(--accent-black); background: var(--black-alpha-4); }
.nm-modal-h .nm-x svg { width: 16px; height: 16px; }
.nm-modal-b {
padding: 22px 24px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
overflow-y: auto;
}
.nm-left .nm-up-zone {
aspect-ratio: 3/4;
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
background: var(--background-lighter);
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 10px;
color: var(--black-alpha-56);
cursor: pointer;
text-align: center;
padding: 16px;
transition: border-color var(--t-base), background var(--t-base);
position: relative;
overflow: hidden;
}
.nm-left .nm-up-zone:hover { border-color: var(--heat); background: var(--heat-8); color: var(--heat); }
.nm-left .nm-up-zone.has-img { padding: 0; border-style: solid; }
.nm-left .nm-up-zone img { width: 100%; height: 100%; object-fit: cover; }
.nm-left .nm-up-zone .nm-uz-ic svg { width: 26px; height: 26px; }
.nm-left .nm-up-zone .nm-uz-t { font-size: 13px; font-weight: 600; color: var(--accent-black); }
.nm-left .nm-up-zone:hover .nm-uz-t { color: var(--heat); }
.nm-left .nm-up-zone .nm-uz-d { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; line-height: 1.6; }
.nm-left .nm-up-zone .nm-uz-replace {
position: absolute; top: 8px; right: 8px;
background: rgba(0,0,0,.7); color: white;
font-size: 11px; padding: 4px 10px;
border-radius: var(--r-sm); border: 0;
cursor: pointer;
font-family: inherit;
opacity: 0;
transition: opacity var(--t-base);
}
.nm-left .nm-up-zone.has-img:hover .nm-uz-replace { opacity: 1; }
.nm-left .nm-uz-hint {
margin-top: 10px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .02em;
line-height: 1.6;
}
.nm-right { display: flex; flex-direction: column; gap: 14px; }
.nm-right .field-label {
font-size: 12.5px; font-weight: 600;
color: var(--accent-black);
display: block;
margin-bottom: 6px;
}
.nm-right .field-label .req { color: var(--heat); }
.nm-right .field-hint {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
margin-bottom: 6px;
}
.nm-right .nm-input, .nm-right .nm-select {
width: 100%;
height: 36px;
background: var(--background-lighter);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-md);
padding: 0 12px;
font-size: 13px;
color: var(--accent-black);
outline: none;
font-family: inherit;
transition: border-color var(--t-base);
}
.nm-right .nm-input:focus, .nm-right .nm-select:focus { border-color: var(--heat-40); }
.nm-right .nm-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.nm-right .nm-tags {
display: flex; flex-wrap: wrap; gap: 6px;
margin-top: 4px;
}
.nm-right .nm-tag-chip {
height: 26px; padding: 0 10px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-size: 11.5px;
color: var(--black-alpha-72);
cursor: pointer;
font-family: inherit;
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.nm-right .nm-tag-chip:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
.nm-right .nm-tag-chip.active {
background: var(--heat-12);
border-color: var(--heat-40);
color: var(--heat);
font-weight: 600;
}
.nm-modal-f {
padding: 14px 24px;
border-top: 1px solid var(--border-faint);
background: var(--background-lighter);
display: flex; align-items: center; gap: 12px;
}
.nm-modal-f .nm-cost {
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
margin-right: auto;
}
.nm-modal-f .nm-cost .price { color: var(--heat); font-weight: 700; }
/* ─── Step 4: 完成确认 ─── */
.review-box {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 32px;
max-width: 800px;
}
.review-box h3 { font-size: 22px; font-weight: 600; letter-spacing: -.012em; color: var(--accent-black); }
.review-box .review-meta {
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-48);
margin-top: 6px;
letter-spacing: .02em;
}
.review-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-top: 20px;
}
.review-grid .placeholder { aspect-ratio: 1; }
.review-summary {
margin-top: 24px;
padding: 16px 20px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
font-size: 13px;
color: var(--black-alpha-72);
line-height: 1.7;
}
.review-summary dl > div {
display: flex;
padding: 6px 0;
border-bottom: 1px solid var(--border-faint);
}
.review-summary dl > div:last-child { border-bottom: 0; }
.review-summary dl dt {
width: 80px;
color: var(--black-alpha-48);
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: .04em;
}
.review-summary dl dd {
flex: 1;
color: var(--accent-black);
font-weight: 500;
}
/* ─── Right panel ─── */
.wb-controls {
border-left: 1px solid var(--border-faint);
background: var(--background-base);
display: flex; flex-direction: column;
overflow: hidden;
min-height: 0;
}
.ctrl-section { margin-bottom: 24px; }
.ctrl-section h3 {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .08em;
text-transform: uppercase;
font-weight: 600;
margin-bottom: 10px;
display: flex; align-items: center; gap: 6px;
}
.ctrl-section h3 .accent {
color: var(--heat);
font-weight: 700;
}
.hover-detail {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 16px;
}
.hover-detail .hd-name {
font-size: 16px; font-weight: 600;
color: var(--accent-black);
}
.hover-detail .hd-tags {
display: flex; gap: 5px; flex-wrap: wrap;
margin-top: 8px;
}
.hover-detail .hd-tags span {
font-family: var(--font-mono);
font-size: 10px;
color: var(--black-alpha-56);
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
padding: 2px 6px;
letter-spacing: .02em;
}
.hover-detail .hd-views {
display: grid; grid-template-columns: repeat(3, 1fr);
gap: 5px; margin-top: 14px;
}
.hover-detail .hd-views .placeholder { aspect-ratio: 3/4; }
.hover-detail .hd-meta {
display: flex; flex-direction: column; gap: 7px;
margin-top: 14px; padding-top: 14px;
border-top: 1px solid var(--border-faint);
font-size: 12px;
color: var(--black-alpha-72);
}
.hover-detail .hd-meta div { display: flex; gap: 10px; }
.hover-detail .hd-meta .k {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
width: 60px;
letter-spacing: .04em;
flex-shrink: 0;
}
.selected-list { display: flex; flex-direction: column; gap: 6px; }
.sel-row {
display: flex; align-items: center; gap: 10px;
padding: 8px 10px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
transition: border-color var(--t-base);
}
.sel-row:hover { border-color: var(--black-alpha-24); }
.sel-row .placeholder { width: 30px; height: 38px; flex-shrink: 0; border-radius: var(--r-sm); }
.sel-row .nm { flex: 1; font-size: 12.5px; font-weight: 600; color: var(--accent-black); }
.sel-row .x-btn {
width: 22px; height: 22px;
border-radius: var(--r-sm);
display: grid; place-items: center;
cursor: pointer;
color: var(--black-alpha-48);
transition: color var(--t-base), background var(--t-base);
}
.sel-row .x-btn:hover { color: var(--accent-crimson); background: var(--crimson-bg); }
.sel-empty {
padding: 18px 14px;
text-align: center;
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
letter-spacing: .02em;
}
.tip-mini {
padding: 12px 14px;
background: var(--heat-8);
border: 1px solid var(--heat-20);
border-radius: var(--r-md);
font-size: 12px;
color: var(--black-alpha-72);
line-height: 1.6;
}
.tip-mini strong {
color: var(--heat);
display: block;
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: .04em;
margin-bottom: 6px;
font-weight: 600;
}
/* ─── 中央 step pane · 底部确认按钮区 (sticky 固定在视口底) ─── */
.pane-cta {
margin: auto -32px 0; /* 推到底 + 撑满 canvas-inner 左右 padding */
padding: 14px 32px;
border-top: 1px solid var(--border-faint);
display: flex;
align-items: center;
gap: 12px;
position: sticky;
bottom: 0;
background: var(--background-base);
z-index: 5;
}
/* 顶部加渐变模糊带,让 sticky 区分滚动内容 */
.pane-cta::before {
content: '';
position: absolute;
top: -16px; left: 0; right: 0;
height: 16px;
background: linear-gradient(to top, var(--background-base), transparent);
pointer-events: none;
}
.pane-cta .pane-cost {
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
margin-right: auto;
}
.pane-cta .pane-cost .price { color: var(--heat); font-weight: 700; }
.pane-cta .btn-lg {
padding: 10px 22px;
font-size: 13.5px;
font-weight: 600;
}
/* ─── 右栏 · 资产预览 (始终显示) ─── */
#right-asset-preview {
display: flex; flex-direction: column;
flex: 1;
min-height: 0;
}
.rap-head {
padding: 18px 22px 14px;
border-bottom: 1px solid var(--border-faint);
}
.rap-head h3 {
font-size: 16px; font-weight: 600;
color: var(--accent-black);
letter-spacing: -.012em;
line-height: 1.3;
word-break: break-all;
}
.rap-head .rap-meta {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
margin-top: 6px;
letter-spacing: .02em;
}
.rap-scroll {
flex: 1;
overflow-y: auto;
padding: 14px 22px 18px;
}
.rap-group { margin-bottom: 18px; }
.rap-group:last-child { margin-bottom: 0; }
.rap-h {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 8px;
}
.rap-h-t {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-56);
letter-spacing: .04em;
font-weight: 600;
}
.rap-h-c {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--heat);
background: var(--heat-12);
padding: 1px 7px;
border-radius: var(--r-sm);
letter-spacing: .04em;
font-weight: 700;
}
.rap-group.empty .rap-h-c { color: var(--black-alpha-32); background: var(--black-alpha-4); }
.rap-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
}
.rap-item {
aspect-ratio: 1;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
position: relative;
overflow: hidden;
cursor: pointer;
transition: border-color var(--t-base);
}
.rap-item:hover { border-color: var(--black-alpha-32); }
.rap-item img, .rap-item .placeholder {
width: 100%; height: 100%; object-fit: cover;
border: 0; border-radius: 0;
}
.rap-item .rap-mtag {
position: absolute; bottom: 2px; left: 2px; right: 2px;
background: rgba(0,0,0,.6); color: var(--accent-white);
font-family: var(--font-mono);
font-size: 8.5px;
padding: 1px 4px;
border-radius: 2px;
letter-spacing: .02em;
text-align: center;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
pointer-events: none;
}
.rap-empty {
grid-column: 1 / -1;
padding: 14px 12px;
background: var(--background-lighter);
border: 1px dashed var(--border-faint);
border-radius: var(--r-sm);
text-align: center;
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-32);
letter-spacing: .02em;
}
.rap-cta-zone {
padding: 14px 22px 16px;
border-top: 1px solid var(--border-faint);
background: var(--surface);
}
.rap-cta-zone .btn-lg {
width: 100%;
justify-content: center;
font-size: 13.5px;
font-weight: 600;
padding: 12px 14px;
}
.rap-cta-zone .btn-primary[disabled] {
background: var(--black-alpha-8);
color: var(--black-alpha-32);
cursor: not-allowed;
pointer-events: none;
}
.rap-cta-hint {
margin-top: 8px;
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
text-align: center;
}
.rap-cta-hint.ready { color: var(--accent-forest); }
/* ─── 右栏 · 折叠按钮 + 状态 ─── */
#right-asset-preview .rap-head {
display: flex; align-items: flex-start; gap: 10px;
}
#right-asset-preview .rap-head > div { flex: 1; min-width: 0; }
.rap-collapse-btn {
width: 28px; height: 28px;
background: transparent; border: 0;
color: var(--black-alpha-48);
border-radius: var(--r-sm);
cursor: pointer;
display: grid; place-items: center;
flex-shrink: 0;
transition: color var(--t-base), background var(--t-base);
}
.rap-collapse-btn:hover { color: var(--accent-black); background: var(--black-alpha-4); }
.rap-collapse-btn svg { width: 16px; height: 16px; }
.wb-controls.collapsed { width: 56px !important; min-width: 56px !important; }
.wb-controls.collapsed #right-asset-preview > *:not(.rap-collapse-floating) { display: none; }
.rap-collapse-floating {
display: none;
width: 56px; height: 56px;
align-items: center; justify-content: center;
cursor: pointer;
color: var(--black-alpha-56);
border-bottom: 1px solid var(--border-faint);
}
.rap-collapse-floating svg { width: 18px; height: 18px; }
.wb-controls.collapsed .rap-collapse-floating { display: flex; }
.wb-controls.collapsed .rap-collapse-floating:hover { color: var(--accent-black); background: var(--black-alpha-4); }
/* ─── 资产 "加入商品" 勾选 chip ─── */
.pick-chip {
position: absolute; bottom: 8px; right: 8px;
height: 26px; padding: 0 10px;
background: rgba(255,255,255,.95);
border: 1px solid var(--black-alpha-24);
border-radius: 999px;
font-size: 11px; font-weight: 600;
color: var(--black-alpha-56);
display: inline-flex; align-items: center; gap: 5px;
cursor: pointer;
z-index: 4;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
font-family: inherit;
}
.pick-chip:hover { border-color: var(--heat); color: var(--heat); }
.pick-chip svg { width: 12px; height: 12px; }
.pick-chip.picked {
background: var(--heat);
border-color: var(--heat);
color: var(--accent-white);
}
.pick-chip.picked:hover { background: var(--heat); }
/* ─── 空状态 (Step3/4: 还未触发生成) ─── */
.gen-empty {
padding: 60px 40px;
background: var(--surface);
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
text-align: center;
color: var(--black-alpha-56);
}
.gen-empty .ge-ic {
width: 56px; height: 56px;
background: var(--heat-12);
color: var(--heat);
border-radius: 50%;
display: grid; place-items: center;
margin: 0 auto 16px;
}
.gen-empty .ge-ic svg { width: 24px; height: 24px; }
.gen-empty .ge-t { font-size: 14px; font-weight: 600; color: var(--accent-black); margin-bottom: 6px; }
.gen-empty .ge-d { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; line-height: 1.6; }
/* ─── 模特卡片 · 勾选 checkbox + 详情入口 ─── */
.model-card .mc-check {
position: absolute; top: 8px; right: 8px;
width: 24px; height: 24px;
background: rgba(255,255,255,.95);
border: 1.5px solid var(--black-alpha-32);
border-radius: 50%;
cursor: pointer;
z-index: 3;
display: grid; place-items: center;
transition: background var(--t-base), border-color var(--t-base);
}
.model-card .mc-check:hover { border-color: var(--heat); }
.model-card .mc-check svg {
width: 14px; height: 14px;
color: transparent;
transition: color var(--t-base);
}
.model-card.selected .mc-check {
background: var(--heat);
border-color: var(--heat);
}
.model-card.selected .mc-check svg { color: var(--accent-white); }
/* 取消旧的勾标 (覆盖之前的 ::after) */
.model-card.selected::after { display: none; }
/* ─── 模特详情弹窗 ─── */
.md-modal-bg {
position: fixed; inset: 0;
background: rgba(0,0,0,.42);
z-index: 210;
display: none;
align-items: center; justify-content: center;
padding: 20px;
}
.md-modal-bg.open { display: flex; }
.md-modal {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-lg);
box-shadow: 0 20px 60px rgba(0,0,0,.18);
width: 100%;
max-width: 760px;
max-height: calc(100vh - 40px);
display: flex; flex-direction: column;
overflow: hidden;
}
.md-h {
padding: 16px 22px;
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center; justify-content: space-between;
}
.md-h h3 {
font-size: 17px; font-weight: 600;
color: var(--accent-black);
display: inline-flex; align-items: center; gap: 10px;
}
.md-h .md-kind {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-56);
background: var(--background-lighter);
border: 1px solid var(--border-faint);
padding: 3px 9px;
border-radius: var(--r-sm);
letter-spacing: .04em;
}
.md-h .md-kind.mine {
color: var(--heat); background: var(--heat-12); border-color: var(--heat-40);
}
.md-h .md-x {
width: 28px; height: 28px;
background: transparent; border: 0;
color: var(--black-alpha-56);
border-radius: var(--r-sm);
cursor: pointer;
display: grid; place-items: center;
transition: color var(--t-base), background var(--t-base);
}
.md-h .md-x:hover { color: var(--accent-black); background: var(--black-alpha-4); }
.md-h .md-x svg { width: 16px; height: 16px; }
.md-body {
padding: 22px;
overflow-y: auto;
flex: 1;
}
.md-views {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.md-views .placeholder, .md-views img {
aspect-ratio: 3/4;
border-radius: var(--r-md);
width: 100%;
}
.md-views img { object-fit: cover; border: 1px solid var(--border-faint); }
.md-tags {
display: flex; flex-wrap: wrap; gap: 6px;
margin-top: 18px;
}
.md-tags .tag {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-72);
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
padding: 4px 9px;
letter-spacing: .02em;
}
.md-meta {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border-faint);
display: flex; flex-direction: column;
gap: 10px;
}
.md-meta .row {
display: flex; gap: 14px;
font-size: 13px;
color: var(--black-alpha-72);
}
.md-meta .k {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
width: 80px;
letter-spacing: .04em;
flex-shrink: 0;
}
.md-foot {
padding: 14px 22px;
border-top: 1px solid var(--border-faint);
background: var(--background-lighter);
display: flex; align-items: center; gap: 12px;
}
.md-foot .md-foot-info {
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-48);
margin-right: auto;
letter-spacing: .02em;
}
.md-foot .md-foot-info.is-selected { color: var(--accent-forest); }
/* ─── Bottom bar (sticky in right column) ─── */
.wb-bottom {
flex-shrink: 0;
padding: 12px 22px 14px;
background: var(--surface);
border-top: 1px solid var(--border-faint);
display: flex; flex-direction: column; gap: 8px;
}
.wb-bottom .row1 {
display: flex; align-items: center; justify-content: space-between;
}
.cost-info {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .04em;
}
.cost-info .price { color: var(--heat); font-weight: 700; }
/* ─── "我有图" 模式专用样式 ─── */
.single-mode {
display: none;
max-width: 920px;
margin: 0 auto;
}
.single-mode.active { display: block; }
.upload-grid {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 8px; margin-top: 12px;
}
.upload-grid .placeholder { aspect-ratio: 1; }
.upload-zone {
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
padding: 28px;
text-align: center;
background: var(--background-lighter);
color: var(--black-alpha-56);
font-size: 13.5px;
cursor: pointer;
transition: border-color var(--t-base), background var(--t-base);
}
.upload-zone:hover { border-color: var(--heat); background: var(--heat-8); }
.upload-zone strong { color: var(--heat); font-weight: 600; }
/* ============================================================
新建商品 · 表单 view (参考图样式) · 默认显示
· 切到 AI workflow mode 时 .app 隐藏, .wb 显示
============================================================ */
.wb { display: none; }
body.workflow-mode .app { display: none !important; }
body.workflow-mode .wb { display: grid; }
body.workflow-mode .topbar { display: none; }
/* form mode 沿用 Shell 标准布局,
注意: Shell.render() 会把 #page 内容平铺到 #page-content 并删掉 #page,
所以限宽必须加在 .page-head 和 .pc-layout 上 (它们才是实际存在的元素) */
body:not(.workflow-mode) .page-head {
max-width: 1280px;
margin: 0 auto 24px;
}
body:not(.workflow-mode) .btn-guide {
display: inline-flex; align-items: center; gap: 6px;
font-size: 13px;
color: var(--black-alpha-56);
background: transparent;
border: 0;
cursor: pointer;
padding: 8px 10px;
border-radius: var(--r-md);
font-family: inherit;
transition: background var(--t-base), color var(--t-base);
}
body:not(.workflow-mode) .btn-guide:hover { color: var(--accent-black); background: var(--black-alpha-4); }
body:not(.workflow-mode) .btn-guide svg { width: 14px; height: 14px; }
/* ─── 主区 · 两栏 grid (左主右辅) ─── */
.pc-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 440px;
gap: 20px;
padding-bottom: 32px;
max-width: 1280px;
margin: 0 auto;
}
/* ─── 左栏 · 表单区 ─── */
.pc-main { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.form-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 24px 28px;
}
.form-card .form-h {
font-size: 15px; font-weight: 600;
color: var(--accent-black);
margin-bottom: 18px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-faint);
}
/* ─── 表单底部 action 行 (form-card 内) ─── */
.form-card .form-footer {
display: flex; align-items: center; gap: 10px;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--border-faint);
}
.form-card .form-footer .btn-guide { margin-right: auto; }
.form-card .field { margin-bottom: 16px; }
.form-card .field:last-child { margin-bottom: 0; }
.form-card .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; }
.form-card .field-label {
display: block;
font-size: 13px; font-weight: 500;
color: var(--accent-black);
margin-bottom: 6px;
}
.form-card .field-label .req { color: var(--heat); margin-left: 2px; }
.form-card .field-label .opt {
color: var(--black-alpha-48);
font-weight: 400;
font-size: 12px;
margin-left: 6px;
}
.form-card .input,
.form-card .select,
.form-card .textarea {
width: 100%;
height: 38px;
background: var(--background-lighter);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-md);
padding: 0 14px;
font-size: 13.5px;
color: var(--accent-black);
outline: none;
font-family: inherit;
transition: border-color var(--t-base);
}
.form-card .textarea {
height: auto;
min-height: 80px;
padding: 10px 14px;
line-height: 1.6;
resize: vertical;
}
.form-card .input:focus,
.form-card .select:focus,
.form-card .textarea:focus {
border-color: var(--heat-40);
box-shadow: inset 0 0 0 1px var(--heat-40);
}
.form-card .char-count {
display: block;
margin-top: 4px;
text-align: right;
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-32);
letter-spacing: .02em;
}
/* ─── 商品主图区 · 左右两栏 (上传 + 示例) ─── */
.form-card .pf-upload-row {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
gap: 16px;
align-items: stretch;
}
.form-card .pf-upload-zone {
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
padding: 28px 20px;
background: var(--background-lighter);
cursor: pointer;
text-align: center;
transition: border-color var(--t-base), background var(--t-base);
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-height: 180px;
}
.form-card .pf-upload-zone:hover { border-color: var(--heat); background: var(--heat-8); }
.form-card .pf-upload-zone .uz-ic {
width: 44px; height: 44px;
margin: 0 auto 10px;
background: var(--surface);
border: 1px solid var(--heat-20);
border-radius: var(--r-md);
color: var(--heat);
display: grid; place-items: center;
}
.form-card .pf-upload-zone .uz-ic svg { width: 20px; height: 20px; }
.form-card .pf-upload-zone .uz-t { font-size: 14px; color: var(--accent-black); font-weight: 500; }
.form-card .pf-upload-zone .uz-t strong { color: var(--heat); font-weight: 600; }
.form-card .pf-upload-zone .uz-d {
margin-top: 8px;
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
.form-card .pf-example {
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 16px;
display: flex; flex-direction: column; gap: 10px;
}
.form-card .pf-example .ex-h {
font-size: 13px; font-weight: 600;
color: var(--accent-black);
}
.form-card .pf-example .ex-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.form-card .pf-example .ex-grid .ex-thumb {
aspect-ratio: 1;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
overflow: hidden;
position: relative;
display: grid; place-items: center;
color: var(--black-alpha-32);
}
.form-card .pf-example .ex-grid .ex-thumb svg { width: 22px; height: 22px; }
.form-card .pf-example .ex-grid .ex-thumb::after {
content: '';
position: absolute; inset: 0;
background:
repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
pointer-events: none;
}
.form-card .pf-example .ex-d {
font-size: 12px;
color: var(--black-alpha-56);
line-height: 1.5;
}
.form-card .pf-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
margin-top: 12px;
}
.form-card .pf-grid:empty { display: none; }
.form-card .pf-thumb {
aspect-ratio: 1;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
position: relative;
overflow: hidden;
cursor: pointer;
}
.form-card .pf-thumb img { width: 100%; height: 100%; object-fit: cover; }
.form-card .pf-thumb .pf-x {
position: absolute; top: 4px; right: 4px;
width: 22px; height: 22px;
background: rgba(0,0,0,.7);
color: var(--accent-white);
border: 0; border-radius: 50%;
cursor: pointer;
display: grid; place-items: center;
opacity: 0;
transition: opacity var(--t-base);
}
.form-card .pf-thumb:hover .pf-x { opacity: 1; }
.form-card .pf-thumb .pf-x svg { width: 11px; height: 11px; }
/* ─── bullet-list (核心卖点) · form view 复用原样式 ─── */
.form-card .bullet-list { list-style: none; padding: 0; margin: 0; }
.form-card .bullet-list .bl-item,
.form-card .bullet-list .bl-add {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
margin-bottom: 6px;
font-size: 13.5px;
}
.form-card .bullet-list .bl-add { background: transparent; border-style: dashed; }
.form-card .bullet-list .num {
width: 22px; height: 22px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-family: var(--font-mono);
font-size: 11px;
color: var(--heat);
font-weight: 700;
display: grid; place-items: center;
flex-shrink: 0;
}
.form-card .bullet-list .bl-text { flex: 1; color: var(--accent-black); }
.form-card .bullet-list .bl-input {
flex: 1;
background: transparent; border: 0; outline: none;
font-size: 13.5px;
color: var(--accent-black);
font-family: inherit;
}
.form-card .bullet-list .bl-x {
width: 22px; height: 22px;
color: var(--black-alpha-48);
cursor: pointer;
display: grid; place-items: center;
border-radius: var(--r-sm);
transition: color var(--t-base), background var(--t-base);
}
.form-card .bullet-list .bl-x:hover { color: var(--accent-crimson); background: var(--crimson-bg); }
.form-card .bullet-list .bl-x svg { width: 11px; height: 11px; }
/* ─── 右栏 · 辅助 cards ─── */
.pc-aside { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
/* AI 入口 card */
.pc-ai-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 20px;
}
.pc-ai-card .pa-h h3 {
font-size: 14.5px; font-weight: 600;
color: var(--accent-black);
}
.pc-ai-card .pa-h p {
margin-top: 6px;
font-size: 12.5px;
color: var(--black-alpha-56);
line-height: 1.6;
}
.pc-ai-card .pa-entries { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.pc-ai-card .pa-entry {
display: flex; align-items: center; gap: 12px;
padding: 10px 12px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
transition: border-color var(--t-base), background var(--t-base);
}
.pc-ai-card .pa-entry:hover { border-color: var(--heat-40); background: var(--heat-8); }
.pc-ai-card .pa-entry .pe-thumb {
width: 56px; height: 56px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
flex-shrink: 0;
display: grid; place-items: center;
color: var(--heat);
}
.pc-ai-card .pa-entry .pe-thumb svg { width: 22px; height: 22px; }
.pc-ai-card .pa-entry .pe-info { flex: 1; min-width: 0; }
.pc-ai-card .pa-entry .pe-info .nm { font-size: 13px; font-weight: 600; color: var(--accent-black); }
.pc-ai-card .pa-entry .pe-info .dd { font-size: 11.5px; color: var(--black-alpha-56); margin-top: 3px; line-height: 1.5; }
.pc-ai-card .pa-entry .pe-cta {
font-size: 12.5px;
color: var(--heat);
background: var(--surface);
border: 1px solid var(--heat-40);
border-radius: var(--r-md);
padding: 5px 12px;
cursor: pointer;
font-family: inherit;
flex-shrink: 0;
transition: background var(--t-base);
}
.pc-ai-card .pa-entry .pe-cta:hover { background: var(--heat-12); }
/* 创建小贴士 card */
.pc-tips-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 20px;
}
.pc-tips-card .pt-h {
font-size: 14px; font-weight: 600;
color: var(--accent-black);
margin-bottom: 12px;
}
.pc-tips-card .pt-item {
display: flex; gap: 10px;
padding: 8px 0;
border-bottom: 1px dashed var(--border-faint);
}
.pc-tips-card .pt-item:last-of-type { border-bottom: 0; }
.pc-tips-card .pt-item .pt-ic {
width: 20px; height: 20px;
background: var(--heat-12);
color: var(--heat);
border-radius: var(--r-sm);
display: grid; place-items: center;
flex-shrink: 0;
margin-top: 1px;
}
.pc-tips-card .pt-item .pt-ic svg { width: 11px; height: 11px; }
.pc-tips-card .pt-item .pt-text { flex: 1; }
.pc-tips-card .pt-item .pt-text .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); }
.pc-tips-card .pt-item .pt-text .d { font-size: 11.5px; color: var(--black-alpha-56); margin-top: 3px; line-height: 1.5; }
.pc-tips-card .pt-guide {
margin-top: 10px;
display: block;
text-align: center;
padding: 8px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
font-size: 12.5px;
color: var(--black-alpha-72);
cursor: pointer;
transition: background var(--t-base), color var(--t-base);
}
.pc-tips-card .pt-guide:hover { background: var(--heat-12); color: var(--heat); border-color: var(--heat-40); }
/* 商品创建后 流程图 */
.pc-after-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 20px;
}
.pc-after-card .pa-title {
font-size: 14px; font-weight: 600;
color: var(--accent-black);
margin-bottom: 14px;
}
.pc-after-card .pa-flow {
display: flex; align-items: center; gap: 8px;
}
.pc-after-card .pa-node {
flex: 1;
text-align: center;
display: flex; flex-direction: column; align-items: center; gap: 6px;
}
.pc-after-card .pa-node .ic {
width: 36px; height: 36px;
background: var(--heat-12);
color: var(--heat);
border-radius: 50%;
display: grid; place-items: center;
}
.pc-after-card .pa-node .ic svg { width: 16px; height: 16px; }
.pc-after-card .pa-node .lbl {
font-size: 11px;
color: var(--black-alpha-72);
line-height: 1.4;
}
.pc-after-card .pa-arrow {
color: var(--black-alpha-32);
flex-shrink: 0;
}
.pc-after-card .pa-arrow svg { width: 14px; height: 14px; }
@media (max-width: 1100px) {
.pc-layout { grid-template-columns: 1fr; }
.form-card .pf-upload-row { grid-template-columns: 1fr; }
}
/* ─── 新建商品 · drawer 形态 ─── */
.pc-drawer { width: 820px; max-width: 100vw; }
.pc-drawer .drawer-h h3 { font-size: 16px; font-weight: 600; }
.pc-drawer .drawer-b { padding: 24px 28px; }
/* drawer 内的 form-card 去掉外观, 由 drawer 自身提供容器 */
.pc-drawer .drawer-b .form-card {
background: transparent;
border: 0;
padding: 0;
border-radius: 0;
}
.pc-drawer .drawer-f {
padding: 14px 24px;
background: var(--surface);
align-items: center;
}
.pc-drawer .drawer-f .btn-guide {
margin-right: auto;
display: inline-flex; align-items: center; gap: 6px;
font-size: 13px;
color: var(--black-alpha-56);
background: transparent;
border: 0;
cursor: pointer;
padding: 8px 10px;
border-radius: var(--r-md);
font-family: inherit;
transition: background var(--t-base), color var(--t-base);
}
.pc-drawer .drawer-f .btn-guide:hover { color: var(--accent-black); background: var(--black-alpha-4); }
.pc-drawer .drawer-f .btn-guide svg { width: 14px; height: 14px; }
@media (max-width: 900px) {
.pc-drawer .drawer-b .pf-upload-row { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- ============================================================
新建商品 · Form view (默认显示) · 参考图样式
· 切到 AI workflow 时 body 加 .workflow-mode, 这块被 .app 隐藏覆盖
============================================================ -->
<div id="page"></div>
<!-- 右弹窗 · 新建商品 -->
<div class="drawer-bg show" id="pc-drawer-bg"></div>
<aside class="drawer show pc-drawer" id="pc-drawer" role="dialog" aria-label="新建商品">
<div class="drawer-h">
<h3>新建商品</h3>
<button class="x" type="button" id="pc-drawer-close" aria-label="关闭">
<svg width="14" height="14" 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 class="drawer-b">
<div class="form-card">
<div class="form-h">基础信息</div>
<div class="field">
<label class="field-label">商品名称<span class="req">*</span></label>
<input class="input" id="pf-name" placeholder="请输入商品名称(必填)" maxlength="100">
</div>
<div class="field-row">
<div>
<label class="field-label">品类<span class="req">*</span></label>
<select class="select" id="pf-cat" data-cat-select>
<option>美妆个护</option>
<option>服饰内衣</option>
<option>食品饮料</option>
<option>家居家电</option>
<option>数码 3C</option>
<option>个护清洁</option>
<option>运动户外</option>
<option>母婴亲子</option>
</select>
</div>
<div>
<label class="field-label">目标人群<span class="opt">(选填)</span></label>
<input class="input" id="pf-target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
</div>
</div>
<div class="field">
<label class="field-label">商品主图<span class="req">*</span></label>
<input type="file" id="pf-file" accept="image/*" multiple hidden>
<div class="pf-upload-row">
<div class="pf-upload-zone" id="pf-zone">
<div class="uz-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
</div>
<div class="uz-t">点击上传或<strong>拖拽图片</strong>到此处</div>
<div class="uz-d">// 支持 JPG、PNG 格式,建议尺寸 800×800 以上,大小不超过 10MB</div>
</div>
<div class="pf-example">
<div class="ex-h">示例图</div>
<div class="ex-grid">
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h10l1 4v12H6V8l1-4z"/><path d="M9 4v3M15 4v3M9 11h6M9 14h6"/></svg></div>
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="5" width="12" height="15" rx="2"/><path d="M9 9h6M9 12h6M9 15h4"/></svg></div>
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3h8l1 5v12H7V8l1-5z"/><circle cx="12" cy="13" r="2.5"/></svg></div>
</div>
<div class="ex-d">优质的商品图有助于生成更好的素材效果</div>
</div>
</div>
<div class="pf-grid" id="pf-grid"></div>
</div>
<div class="field" style="margin-bottom: 0;">
<label class="field-label">核心卖点<span class="req">*</span></label>
<ul class="bullet-list" id="pf-bullets" data-bl>
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder="添加新卖点 · 回车确认"></li>
</ul>
</div>
</div>
</div>
<div class="drawer-f">
<button class="btn-guide" type="button" onclick="Shell.toast('使用指南', '点击查看完整填写指南')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 015 0c0 1.5-2.5 2-2.5 4M12 17h.01"/></svg>
使用指南
</button>
<button class="btn" type="button" id="pc-cancel-btn">取消</button>
<button class="btn btn-primary" type="button" id="pc-save-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
创建商品
</button>
</div>
</aside>
<script src="assets/shell.js?v=202605211643"></script>
<script>
// ============================================================
// 调用 Shell.render · 注入网站 sidebar (form mode 显示)
// ============================================================
Shell.render({
active: 'products',
crumbs: [{ label: '工作台', href: 'index.html' }, { label: '商品库', href: 'products.html' }, { label: '新建商品' }]
});
// ============================================================
// Form view · 多图上传 (与 workflow 独立)
// ============================================================
const PF_MAX = 5;
const pfFiles = []; // {id, dataUrl, name}
const pfFile = document.getElementById('pf-file');
const pfZone = document.getElementById('pf-zone');
const pfGrid = document.getElementById('pf-grid');
function pfUid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 5); }
function pfEsc(s) { return (s || '').replace(/[<>&"]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;' })[c]); }
function pfRender() {
pfGrid.innerHTML = pfFiles.map(u => `
<div class="pf-thumb" data-id="${u.id}">
<img src="${u.dataUrl}" alt="${pfEsc(u.name)}">
<button class="pf-x" type="button" title="删除">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
</button>
</div>
`).join('');
pfGrid.querySelectorAll('.pf-thumb .pf-x').forEach(b => {
b.onclick = e => {
e.stopPropagation();
const id = b.closest('.pf-thumb').dataset.id;
const i = pfFiles.findIndex(f => f.id === id);
if (i >= 0) pfFiles.splice(i, 1);
pfRender();
};
});
pfGrid.querySelectorAll('.pf-thumb img').forEach(img => {
img.onclick = () => Shell._openLightbox(img.src, img.alt);
});
}
function pfAdd(fileList) {
const remaining = PF_MAX - pfFiles.length;
if (remaining <= 0) { Shell.toast('已达上限', `${PF_MAX} / ${PF_MAX}`); return; }
const incoming = [...fileList].filter(f => f.type.startsWith('image/'));
const accepted = incoming.slice(0, remaining);
let done = 0;
accepted.forEach(f => {
const r = new FileReader();
r.onload = e => {
pfFiles.push({ id: pfUid(), dataUrl: e.target.result, name: f.name });
if (++done === accepted.length) {
pfRender();
Shell.toast('图片已添加', `${pfFiles.length}`);
}
};
r.readAsDataURL(f);
});
}
pfZone.onclick = () => { if (pfFiles.length < PF_MAX) pfFile.click(); };
pfZone.addEventListener('dragover', e => { e.preventDefault(); pfZone.style.borderColor = 'var(--heat)'; });
pfZone.addEventListener('dragleave', () => { pfZone.style.borderColor = ''; });
pfZone.addEventListener('drop', e => {
e.preventDefault();
pfZone.style.borderColor = '';
if (e.dataTransfer?.files?.length) pfAdd(e.dataTransfer.files);
});
pfFile.onchange = e => { pfAdd(e.target.files); e.target.value = ''; };
// ============================================================
// Form view · bullet-list (核心卖点) 交互
// ============================================================
(function initBullets() {
const list = document.getElementById('pf-bullets');
if (!list) return;
const addInput = list.querySelector('.bl-add .bl-input');
const xSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
const renumber = () => {
[...list.querySelectorAll('.bl-item')].forEach((li, i) => {
li.querySelector('.num').textContent = i + 1;
});
};
const bindX = x => {
x.onclick = () => {
const li = x.closest('li');
li.style.transition = 'opacity .15s, transform .15s';
li.style.opacity = 0;
li.style.transform = 'translateX(-8px)';
setTimeout(() => { li.remove(); renumber(); }, 150);
};
};
const addBullet = text => {
const t = (text || '').trim();
if (!t) return;
const li = document.createElement('li');
li.className = 'bl-item';
li.innerHTML = `<span class="num">0</span><span class="bl-text">${pfEsc(t)}</span><span class="bl-x" title="删除">${xSvg}</span>`;
list.querySelector('.bl-add').before(li);
bindX(li.querySelector('.bl-x'));
renumber();
};
addInput?.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
addBullet(addInput.value);
addInput.value = '';
}
});
})();
// ============================================================
// 保存 + Mode 切换 · form ↔ workflow
// ============================================================
function pfValidate() {
const name = document.getElementById('pf-name').value.trim();
if (!name) { Shell.toast('请填写商品名称'); return false; }
if (pfFiles.length < 1) { Shell.toast('请至少上传 1 张商品图'); return false; }
return true;
}
function pfSave(redirect = true) {
if (!pfValidate()) return false;
Shell.toast('商品已创建', document.getElementById('pf-name').value);
if (redirect) setTimeout(() => location.href = 'products.html', 600);
return true;
}
// drawer 按钮: 保存 / 取消 / 关闭 / 点击遮罩
document.getElementById('pc-save-btn').onclick = () => pfSave(true);
const _back = () => location.href = 'products.html';
document.getElementById('pc-cancel-btn').onclick = _back;
document.getElementById('pc-drawer-close').onclick = _back;
document.getElementById('pc-drawer-bg').onclick = _back;
document.addEventListener('keydown', e => { if (e.key === 'Escape') _back(); });
// (AI 入口跳转 / pfSyncToWorkflow 已随 drawer 形态移除)
// 把 form view 的数据同步到 workflow (.wb) 的 state 和 DOM
function pfSyncToWorkflow() {
if (typeof state === 'undefined') return; // workflow JS 未初始化
// 商品名
const name = document.getElementById('pf-name').value.trim();
const aiName = document.getElementById('ai-name');
if (aiName) aiName.value = name;
const topbarName = document.getElementById('topbar-name');
if (topbarName) topbarName.value = name;
// 品类
const cat = document.getElementById('pf-cat').value;
const aiCat = document.getElementById('ai-cat');
if (aiCat) aiCat.value = cat;
// 目标人群
const target = document.getElementById('pf-target').value.trim();
const aiTarget = document.getElementById('ai-target');
if (aiTarget) aiTarget.value = target;
// 卖点
const aiBullets = document.getElementById('ai-bullets');
if (aiBullets) {
[...aiBullets.querySelectorAll('.bl-item')].forEach(li => li.remove());
const xSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
[...document.querySelectorAll('#pf-bullets .bl-item .bl-text')].forEach((el, i) => {
const t = el.textContent;
const li = document.createElement('li');
li.className = 'bl-item';
li.innerHTML = `<span class="num">${i + 1}</span><span class="bl-text">${pfEsc(t)}</span><span class="bl-x" title="删除">${xSvg}</span>`;
aiBullets.querySelector('.bl-add').before(li);
});
}
// 原图 → uploads
state.uploads.length = 0;
pfFiles.forEach(f => state.uploads.push({ id: f.id, dataUrl: f.dataUrl, name: f.name, type: 'image/' }));
if (typeof renderS1Grid === 'function') renderS1Grid();
if (typeof refreshSideAssets === 'function') refreshSideAssets();
if (typeof updateBottom === 'function') updateBottom();
}
// ============================================================
// STATE
// ============================================================
const state = {
mode: 'ai',
step: 1, // 1..4
sub: 1, // Step 3 内部 sub-tab (① 挑模特 → ② 生上身)
uploads: [], // Step 1 上传的原图
triViews: [], // Step 2 三视图版本历史 [{id, dataUrl?, createdAt, label}]
triCurrent: null, // 当前选中的三视图 id (= 加入商品的版本)
// Step 3
ownModels: [], // 商家上传的模特
selectedModels: new Set(), // 已选模特 id (用于触发生成)
outfits: {}, // { modelId: [{id, label}] } · 资产库
outfitRegenCount: {},
pickedOutfits: new Set(), // 已加入商品的 outfit id
// Step 4
platforms: {}, // 各平台头图 { platKey: { cards: [{id, label}], regenCount } } · 资产库
pickedPlatformCards: new Set(), // 已加入商品的 平台头图 card id
// 平台 checkbox (Step4 还未触发生成时的勾选)
selectedPlatforms: new Set(),
rightCollapsed: false,
};
const PLATFORM_META = {
amazon: { name: '亚马逊', spec: '1:1 白底', ratio: '1/1' },
xhs: { name: '小红书', spec: '3:4 生活感', ratio: '3/4' },
dy: { name: '抖音', spec: '9:16 视觉冲击', ratio: '9/16' },
tb: { name: '淘宝', spec: '4:5 营销', ratio: '4/5' },
jd: { name: '京东', spec: '1:1 商务', ratio: '1/1' },
pdd: { name: '拼多多', spec: '1:1 促销', ratio: '1/1' },
sph: { name: '视频号', spec: '9:16 引流', ratio: '9/16' },
ks: { name: '快手', spec: '9:16 老铁', ratio: '9/16' },
};
const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
const esc = (s) => (s || '').replace(/[<>&"]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;' })[c]);
// ============================================================
// STEP NAVIGATION (5-step)
// ============================================================
function showStep(n) {
state.step = n;
document.querySelectorAll('.step-pane').forEach(p => p.classList.remove('active'));
document.querySelector(`[data-pane="${n}"]`)?.classList.add('active');
refreshSideSteps();
const strip = document.getElementById('subtab-strip');
if (strip) strip.style.display = n === 3 ? 'flex' : 'none';
updateBottom();
const canvas = document.querySelector('.wb-canvas');
if (canvas) canvas.scrollTop = 0;
// Step 4 进入时如果已勾选平台,渲染结果区
if (n === 4) renderPlatformResults();
}
// 已完成的 step 可点击回退
function canEnterStep(n) {
if (n === 1) return true;
if (n === 2) return state.uploads.length >= 1 && getName();
if (n === 3) return state.triViews.length >= 1; // 附加:三视图后可入
if (n === 4) return state.triViews.length >= 1; // 附加:三视图后可入
return false;
}
function getName() { return (document.getElementById('ai-name')?.value || '').trim(); }
// 真实"完成"状态(用于 sidebar done 视觉)· 附加 step 仅在用户实际生成了内容时才标 done
function isStepDone(sn) {
if (sn === 1) return state.uploads.length >= 1 && !!getName();
if (sn === 2) return state.triViews.length >= 1;
if (sn === 3) return countOutfitCards() > 0 || state.ownModels.length > 0;
if (sn === 4) return countPlatformCards() > 0;
return false;
}
function refreshSideSteps() {
document.querySelectorAll('#ai-steps .step-item').forEach(s => {
s.classList.remove('active', 'done', 'locked');
const sn = +s.dataset.step;
if (sn === state.step) {
s.classList.add('active');
} else if (isStepDone(sn)) {
s.classList.add('done');
} else if (!canEnterStep(sn)) {
s.classList.add('locked');
}
});
}
function showSub(n) {
state.sub = n;
document.querySelectorAll('.sub-pane').forEach(p => p.classList.remove('active'));
document.querySelector(`[data-sub-pane="${n}"]`)?.classList.add('active');
document.querySelectorAll('.subtab').forEach(t => {
t.classList.remove('active', 'done', 'disabled');
const tn = +t.dataset.sub;
if (tn < n) t.classList.add('done');
else if (tn === n) t.classList.add('active');
});
document.querySelectorAll('.substep-item').forEach(t => {
t.classList.remove('active', 'done');
const tn = +t.dataset.sub;
if (tn < n) t.classList.add('done');
else if (tn === n) t.classList.add('active');
});
const subtabMeta = document.getElementById('subtab-meta');
if (subtabMeta) {
subtabMeta.innerHTML = n === 1
? `// SUB-STEP 1 / 2 · 已选模特 <span class="accent">${selectedCount()}</span>`
: `// SUB-STEP 2 / 2 · ${state.selectedModels.size} 个模特 × 4 张`;
}
// 进入 SUB 2 时仅渲染(已生成的 outfits)· 不再自动触发生成
if (n === 2) renderOutfits();
updateBottom();
document.querySelector('.wb-canvas').scrollTop = 0;
}
window.showSub = showSub;
const SVG_LEFT = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>';
const SVG_RIGHT = '<svg width="14" height="14" 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>';
const SVG_CHECK = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>';
function updateBottom() {
// 更新各 step pane 底部成本提示 + topbar finish 状态
refreshPaneCosts();
refreshFinishCta();
}
function refreshPaneCosts() {
// Step 2 已生成: 切换文案为 "重新生成"
const p2lbl = document.getElementById('pane2-gen-label');
const p2cost = document.getElementById('pane2-cost');
if (p2lbl && p2cost) {
if (state.triViews.length > 0) {
p2lbl.textContent = '重新生成 (新增版本)';
p2cost.innerHTML = `// 已有 ${state.triViews.length} 个版本 · 重新生成 ~<span class="price">¥0.30</span>`;
} else {
p2lbl.textContent = '生成商品三视图';
p2cost.innerHTML = '// 生成三视图 · ~<span class="price">¥0.30</span> / 次';
}
}
// Step 3 SUB1 成本提示
const p3cost = document.getElementById('pane3-cost');
if (p3cost) {
const n = state.selectedModels.size;
p3cost.innerHTML = n > 0
? `// 已选 ${n} 个模特 · 预计 ${n * 4} 张 · ~<span class="price">¥${(n * 0.4).toFixed(2)}</span>`
: '// 未选模特 · 先勾选 1+ 个模特';
}
// Step 4 成本提示
const p4cost = document.getElementById('pane4-cost');
if (p4cost) {
const n = state.selectedPlatforms.size;
p4cost.innerHTML = n > 0
? `// 已选 ${n} 个平台 · 预计 ${n * 4} 张 · ~<span class="price">¥${(n * 0.5).toFixed(2)}</span>`
: '// 未选平台 · 先勾选 1+ 个平台';
}
}
function refreshFinishCta() {
// topbar 完成创建按钮
const tb = document.getElementById('topbar-finish');
if (!tb) return;
const ready = state.triViews.length >= 1;
tb.disabled = !ready;
tb.title = ready ? `加入商品库 (${countPickedAssets()} 张已选资产)` : '完成 Step 2 三视图后可创建';
}
function bindStepButtons() {
// Step 1 底部 · 下一步
document.getElementById('pane1-next')?.addEventListener('click', () => {
if (!getName()) { Shell.toast('请填写商品名称'); return; }
if (state.uploads.length < 1) { Shell.toast('请至少上传 1 张商品图'); return; }
showStep(2);
});
// Step 2 底部 · 生成 / 重新生成三视图
document.getElementById('pane2-gen')?.addEventListener('click', () => {
if (state.triViews.length === 0) autoGenTriView();
else triViewRegenerate();
});
// Step 3 SUB1 底部 · 生成上身图
document.getElementById('pane3-gen')?.addEventListener('click', () => {
const n = state.selectedModels.size;
if (n < 1) { Shell.toast('请先勾选至少 1 个模特'); return; }
generateOutfitsForSelected();
});
// Step 3 SUB2 底部 · 返回挑选
document.getElementById('pane3sub2-back')?.addEventListener('click', () => showSub(1));
// Step 4 底部 · 生成平台头图
document.getElementById('pane4-gen')?.addEventListener('click', () => {
const n = state.selectedPlatforms.size;
if (n < 1) { Shell.toast('请先勾选至少 1 个平台'); return; }
generatePlatformsForSelected();
});
// Topbar 完成创建
document.getElementById('topbar-finish')?.addEventListener('click', () => {
if (state.triViews.length < 1) { Shell.toast('需先完成商品三视图'); return; }
Shell.toast('商品已加入商品库', getName());
setTimeout(() => location.href = 'products.html', 800);
});
}
// 折叠右栏
window.toggleRightPanel = function() {
state.rightCollapsed = !state.rightCollapsed;
document.querySelector('.wb-controls')?.classList.toggle('collapsed', state.rightCollapsed);
};
// sub-tab click
document.querySelectorAll('.subtab').forEach(t => {
t.onclick = () => {
if (state.sub === 2 || +t.dataset.sub === 1 || selectedCount() > 0) {
showSub(+t.dataset.sub);
}
};
});
// sidebar step click
document.querySelectorAll('#ai-steps .step-item').forEach(s => {
s.onclick = () => {
if (s.classList.contains('locked')) return;
const n = +s.dataset.step;
if (n) showStep(n);
};
});
// sub-nav click
document.querySelectorAll('#sub-nav .substep-item').forEach(t => {
t.onclick = () => showSub(+t.dataset.sub);
});
// ============================================================
// STEP 3 · 模特卡片 · 勾选 + 点卡查看详情弹窗
// ============================================================
// 初始化:把 HTML 中预选的卡片同步到 state.selectedModels
document.querySelectorAll('#model-grid .model-card.selected').forEach(c => {
state.selectedModels.add(c.dataset.id);
});
function selectedCount() { return state.selectedModels.size; }
function refreshCounts() {
const n = selectedCount();
const setText = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
setText('sel-count', n);
setText('meta-count', n);
setText('sel-count-x4', n * 4);
refreshKindNums();
if (state.step === 3) updateBottom();
}
function refreshKindNums() {
const all = document.querySelectorAll('#model-grid .model-card').length;
const lib = document.querySelectorAll('#model-grid .model-card[data-kind="lib"]').length;
const mine = document.querySelectorAll('#model-grid .model-card[data-kind="mine"]').length;
const setText = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
setText('kt-num-all', all);
setText('kt-num-lib', lib);
setText('kt-num-mine', mine);
}
const CHECK_SVG = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.5l3 3 6-6"/></svg>';
function toggleModelSelection(card) {
const id = card.dataset.id;
const wasSelected = card.classList.contains('selected');
card.classList.toggle('selected');
if (wasSelected) {
state.selectedModels.delete(id);
// 已生成的 outfits 保留在资产库 · 仅取消勾选标记
(state.outfits[id] || []).forEach(c => state.pickedOutfits.delete(c.id));
} else {
state.selectedModels.add(id);
}
refreshCounts();
refreshSideAssets();
}
function bindModelCard(card) {
// 注入勾选 checkbox 图标(若未注入)
if (!card.querySelector('.mc-check')) {
const chk = document.createElement('span');
chk.className = 'mc-check';
chk.title = '点击勾选/取消';
chk.innerHTML = CHECK_SVG;
chk.addEventListener('click', e => {
e.stopPropagation();
toggleModelSelection(card);
});
card.appendChild(chk);
}
// 整卡点击 → 打开详情弹窗
card.onclick = () => openModelDetail(card);
}
document.querySelectorAll('#model-grid .model-card').forEach(bindModelCard);
// ============================================================
// 模特详情弹窗 · 大图 + 标签 + 选中按钮
// ============================================================
let mdCurrentCard = null;
const mdModal = document.getElementById('md-modal');
window.openModelDetail = function(card) {
if (!card) return;
mdCurrentCard = card;
const id = card.dataset.id;
const kind = card.dataset.kind || 'lib';
const name = card.dataset.name;
const base = card.dataset.base || '';
const role = card.dataset.role || '';
const tags = (card.dataset.tags || '').split(',').filter(Boolean);
document.getElementById('md-name').textContent = name;
const kindEl = document.getElementById('md-kind');
kindEl.textContent = kind === 'mine' ? '我的 · 入资产库' : '平台模特库';
kindEl.classList.toggle('mine', kind === 'mine');
// 三视图(我的模特: 使用上传图;库模特:用 placeholder)
const views = document.getElementById('md-views');
if (kind === 'mine') {
const own = state.ownModels.find(m => m.id === id);
if (own?.sourceImg) {
views.innerHTML = `
<div><img src="${own.sourceImg}" alt="正面"></div>
<div class="placeholder"><span class="ph-frame">侧面</span></div>
<div class="placeholder"><span class="ph-frame">背面</span></div>
`;
} else {
views.innerHTML = `<div class="placeholder"><span class="ph-frame">正面</span></div><div class="placeholder"><span class="ph-frame">侧面</span></div><div class="placeholder"><span class="ph-frame">背面</span></div>`;
}
} else {
views.innerHTML = `
<div class="placeholder"><span class="ph-frame">${esc(name)}<br>正面</span></div>
<div class="placeholder"><span class="ph-frame">${esc(name)}<br>侧面</span></div>
<div class="placeholder"><span class="ph-frame">${esc(name)}<br>背面</span></div>
`;
}
// tags
const baseTags = base ? base.split(' · ').filter(Boolean) : [];
const allTags = baseTags.concat([role]).concat(tags).filter(Boolean);
document.getElementById('md-tags').innerHTML = allTags.map(t => `<span class="tag">${esc(t)}</span>`).join('');
// meta rows
const ageStr = baseTags.length > 1 ? baseTags[1] : '';
document.getElementById('md-age').textContent = ageStr ? `${ageStr} 岁 · 可微调 ±3` : '—';
document.getElementById('md-outfit').textContent = kind === 'mine' ? '商家上传立绘(原图)' : '白 T + 白短裤(默认立绘)';
document.getElementById('md-body-info').textContent = base.includes('双人') ? '双人组合' : (base.startsWith('男') ? '普通男 · 175cm / 65kg' : '普通女 · 165cm / 50kg');
document.getElementById('md-used').textContent = kind === 'mine' ? '商家上传 · 仅本商品' : '4 个项目 · 平均评分 4.5';
updateMdSelectButton(card.classList.contains('selected'));
document.getElementById('md-select-btn').onclick = () => {
toggleModelSelection(card);
updateMdSelectButton(card.classList.contains('selected'));
};
mdModal.classList.add('open');
};
window.closeModelDetail = function() {
mdModal.classList.remove('open');
mdCurrentCard = null;
};
mdModal?.addEventListener('click', e => { if (e.target === mdModal) closeModelDetail(); });
function updateMdSelectButton(isSelected) {
const lbl = document.getElementById('md-select-label');
const info = document.getElementById('md-foot-info');
const btn = document.getElementById('md-select-btn');
if (isSelected) {
lbl.textContent = '已选中 · 点击取消';
info.textContent = '// 已选中,下一步将生成 4 张上身图';
info.classList.add('is-selected');
btn.classList.remove('btn-primary');
} else {
lbl.textContent = '选中此模特';
info.textContent = '// 未选中';
info.classList.remove('is-selected');
btn.classList.add('btn-primary');
}
}
window.removeModel = function(id) {
const card = document.querySelector(`#model-grid .model-card[data-id="${id}"]`);
if (card) card.classList.remove('selected');
state.selectedModels.delete(id);
(state.outfits[id] || []).forEach(c => state.pickedOutfits.delete(c.id));
refreshCounts();
refreshSideAssets();
};
// ============================================================
// 来源切换 · 全部 / 模特库 / 我的
// ============================================================
document.querySelectorAll('#kind-toggle .kt-btn').forEach(btn => {
btn.onclick = () => {
document.querySelectorAll('#kind-toggle .kt-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const kind = btn.dataset.kind;
document.querySelectorAll('#model-grid .model-card').forEach(c => {
const k = c.dataset.kind;
c.style.display = (kind === 'all' || kind === k) ? '' : 'none';
});
// "+ 上传新模特" 卡片在所有视图下显示
const addCard = document.getElementById('add-model-card');
if (addCard) addCard.style.display = '';
};
});
// ============================================================
// 核心卖点 · bullet-list 交互
// ============================================================
document.querySelectorAll('[data-bl]').forEach(list => {
const addInput = list.querySelector('.bl-add .bl-input');
const renumber = () => {
[...list.querySelectorAll('.bl-item')].forEach((li, i) => {
li.querySelector('.num').textContent = i + 1;
});
};
const xSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
const addBullet = (text) => {
const t = (text || '').trim();
if (!t) return;
const li = document.createElement('li');
li.className = 'bl-item';
li.innerHTML = `<span class="num">0</span><span class="bl-text">${t.replace(/[<>&]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;' })[c])}</span><span class="bl-x" title="删除">${xSvg}</span>`;
list.querySelector('.bl-add').before(li);
bindX(li.querySelector('.bl-x'));
renumber();
};
const bindX = (x) => {
x.addEventListener('click', () => {
const li = x.closest('li');
li.style.transition = 'opacity .15s, transform .15s';
li.style.opacity = 0;
li.style.transform = 'translateX(-8px)';
setTimeout(() => { li.remove(); renumber(); }, 150);
});
};
list.querySelectorAll('.bl-x').forEach(bindX);
addInput?.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
addBullet(addInput.value);
addInput.value = '';
}
});
});
// ============================================================
// Step 1 · 多图上传 (data URLs · 不入资产库,仅商品详情)
// ============================================================
const MAX_UPLOADS = 5;
const s1File = document.getElementById('s1-file');
const s1Zone = document.getElementById('s1-zone');
const s1ZoneText = document.getElementById('s1-zone-text');
const s1Grid = document.getElementById('s1-grid');
function renderS1Grid() {
if (!s1Grid) return;
const xSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
s1Grid.innerHTML = state.uploads.map(u => `
<div class="up-thumb" data-id="${u.id}">
<img src="${u.dataUrl}" alt="${esc(u.name)}">
<button class="slot-x" type="button" title="删除">${xSvg}</button>
</div>
`).join('');
s1Grid.querySelectorAll('.up-thumb').forEach(t => {
const id = t.dataset.id;
t.querySelector('.slot-x').addEventListener('click', e => {
e.stopPropagation();
const i = state.uploads.findIndex(u => u.id === id);
if (i >= 0) {
const rm = state.uploads.splice(i, 1)[0];
Shell.toast('已删除', rm.name);
renderS1Grid(); syncS1Zone(); renderAssetPreview(); updateBottom();
}
});
t.addEventListener('click', () => {
const u = state.uploads.find(x => x.id === id);
if (u) Shell._openLightbox(u.dataUrl, u.name);
});
});
}
function syncS1Zone() {
if (!s1Zone || !s1ZoneText) return;
const full = state.uploads.length >= MAX_UPLOADS;
s1Zone.classList.toggle('full', full);
s1ZoneText.textContent = full ? `已达上限 (${MAX_UPLOADS} / ${MAX_UPLOADS})` : '点击或拖拽上传图片';
}
function addS1Files(fileList) {
const remaining = MAX_UPLOADS - state.uploads.length;
if (remaining <= 0) { Shell.toast('已达上限', `${MAX_UPLOADS} / ${MAX_UPLOADS}`); return; }
const incoming = [...fileList].filter(f => f.type.startsWith('image/'));
const accepted = incoming.slice(0, remaining);
const overflow = incoming.length - accepted.length;
let done = 0;
accepted.forEach(f => {
const reader = new FileReader();
reader.onload = e => {
state.uploads.push({ id: uid(), dataUrl: e.target.result, name: f.name, type: f.type });
done++;
if (done === accepted.length) {
renderS1Grid(); syncS1Zone(); renderAssetPreview(); updateBottom();
const msg = overflow > 0 ? `+ ${done} 张 · 超出 ${overflow} 张忽略` : `+ ${done} 张 · 共 ${state.uploads.length}`;
Shell.toast('图片已添加 (商品详情)', msg);
}
};
reader.readAsDataURL(f);
});
}
if (s1File) s1File.addEventListener('change', e => { addS1Files(e.target.files); e.target.value = ''; });
if (s1Zone) {
s1Zone.addEventListener('click', () => { if (state.uploads.length < MAX_UPLOADS) s1File.click(); });
s1Zone.addEventListener('dragover', e => { e.preventDefault(); if (state.uploads.length < MAX_UPLOADS) s1Zone.style.borderColor = 'var(--heat)'; });
s1Zone.addEventListener('dragleave', () => { s1Zone.style.borderColor = ''; });
s1Zone.addEventListener('drop', e => { e.preventDefault(); s1Zone.style.borderColor = ''; if (e.dataTransfer?.files?.length) addS1Files(e.dataTransfer.files); });
}
// 商品名 → topbar 同步
document.getElementById('ai-name')?.addEventListener('input', e => {
const tb = document.getElementById('topbar-name');
if (tb) tb.value = e.target.value;
});
// ============================================================
// 右栏 · 商品预览 (只显示已加入商品的资产)
// ============================================================
function countOutfitCards() {
return Object.values(state.outfits).reduce((s, arr) => s + arr.length, 0);
}
function countPlatformCards() {
return Object.values(state.platforms).reduce((s, p) => s + p.cards.length, 0);
}
function countPickedAssets() {
let n = 0;
if (state.triCurrent) n++;
n += state.pickedOutfits.size;
n += state.pickedPlatformCards.size;
return n;
}
// 兼容旧调用
function countTotalAssets() { return countPickedAssets(); }
function renderAssetPreview() {
const name = getName() || '未命名商品';
const cat = document.getElementById('ai-cat')?.value || '';
const total = countPickedAssets();
document.getElementById('rap-name').textContent = name;
document.getElementById('rap-meta').textContent = `// ${cat || '未填写品类'} · 加入商品 ${total} 张 · 原图 ${state.uploads.length}`;
// 原图 (始终显示,商品详情数据)
const gOrig = document.getElementById('rap-grid-orig');
document.getElementById('rap-c-orig').textContent = state.uploads.length;
document.querySelector('[data-group="orig"]')?.classList.toggle('empty', state.uploads.length === 0);
if (state.uploads.length) {
gOrig.innerHTML = state.uploads.map(u => `<div class="rap-item" data-src="${u.dataUrl}" data-name="${esc(u.name)}"><img src="${u.dataUrl}" alt=""></div>`).join('');
} else {
gOrig.innerHTML = '<div class="rap-empty mono">// 还未上传</div>';
}
// 三视图 · 只显示当前加入商品的版本
const gTri = document.getElementById('rap-grid-tri');
const curTri = state.triViews.find(v => v.id === state.triCurrent);
document.getElementById('rap-c-tri').textContent = curTri ? 1 : 0;
document.querySelector('[data-group="tri"]')?.classList.toggle('empty', !curTri);
if (curTri) {
gTri.innerHTML = `<div class="rap-item"><div class="placeholder"><span class="ph-frame">${esc(curTri.label)}</span></div><span class="rap-mtag">${esc(curTri.label)}</span></div>`;
} else {
gTri.innerHTML = '<div class="rap-empty mono">// 还未生成三视图</div>';
}
// 上身图 · 只显示已加入商品的
const gOut = document.getElementById('rap-grid-outfit');
const pickedOutfitsArr = [];
Object.entries(state.outfits).forEach(([modelId, cards]) => {
const m = getModelById(modelId);
cards.forEach(c => {
if (state.pickedOutfits.has(c.id)) {
pickedOutfitsArr.push({ modelName: m?.name || modelId, label: c.label });
}
});
});
document.getElementById('rap-c-outfit').textContent = pickedOutfitsArr.length;
document.querySelector('[data-group="outfit"]')?.classList.toggle('empty', pickedOutfitsArr.length === 0);
if (pickedOutfitsArr.length) {
gOut.innerHTML = pickedOutfitsArr.map(p => `<div class="rap-item"><div class="placeholder"><span class="ph-frame">${esc(p.modelName)}</span></div><span class="rap-mtag">${esc(p.label)}</span></div>`).join('');
} else {
gOut.innerHTML = '<div class="rap-empty mono">// 还未加入上身图</div>';
}
// 我的模卡 · 默认入资产库,但显不显示由用户决定 - 此处用 ownModels 数显示在商品详情下
const gOwn = document.getElementById('rap-grid-own');
document.getElementById('rap-c-own').textContent = state.ownModels.length;
document.querySelector('[data-group="own"]')?.classList.toggle('empty', state.ownModels.length === 0);
if (state.ownModels.length) {
gOwn.innerHTML = state.ownModels.map(m => `<div class="rap-item" data-src="${m.sourceImg}" data-name="${esc(m.name)}"><img src="${m.sourceImg}" alt=""><span class="rap-mtag">${esc(m.name)}</span></div>`).join('');
} else {
gOwn.innerHTML = '<div class="rap-empty mono">// 未上传我的模特</div>';
}
// 平台头图 · 只显示已加入商品的
const gPlat = document.getElementById('rap-grid-plat');
const pickedPlatArr = [];
Object.entries(state.platforms).forEach(([pk, p]) => {
p.cards.forEach((c, i) => {
if (state.pickedPlatformCards.has(c.id)) {
pickedPlatArr.push({ platName: PLATFORM_META[pk].name, idx: i + 1 });
}
});
});
document.getElementById('rap-c-plat').textContent = pickedPlatArr.length;
document.querySelector('[data-group="plat"]')?.classList.toggle('empty', pickedPlatArr.length === 0);
if (pickedPlatArr.length) {
gPlat.innerHTML = pickedPlatArr.map(p => `<div class="rap-item"><div class="placeholder"><span class="ph-frame">${esc(p.platName)}<br>${p.idx}</span></div><span class="rap-mtag">${esc(p.platName)}</span></div>`).join('');
} else {
gPlat.innerHTML = '<div class="rap-empty mono">// 还未加入平台头图</div>';
}
// 原图 / 我的模卡 点击 lightbox
document.querySelectorAll('#right-asset-preview .rap-item[data-src]').forEach(it => {
it.onclick = () => Shell._openLightbox(it.dataset.src, it.dataset.name);
});
refreshFinishCta();
refreshSideSteps();
}
// 兼容旧名
function refreshSideAssets() { renderAssetPreview(); }
// ============================================================
// Step 2 · 商品三视图 · 按钮触发生成 + 版本历史
// ============================================================
function autoGenTriView() {
if (state.triViews.length > 0) return triViewRegenerate();
const btn = document.getElementById('pane2-gen');
if (btn) btn.disabled = true;
Shell.toast('正在生成三视图', '~12s · ¥0.30');
setTimeout(() => {
const v = { id: uid(), label: `v${state.triViews.length + 1}`, createdAt: new Date().toLocaleTimeString().slice(0, 5) };
state.triViews.push(v);
state.triCurrent = v.id; // 默认选中最新 = 加入商品
// 切换显示
document.getElementById('triview-stage-empty').style.display = 'none';
document.getElementById('triview-stage').style.display = '';
document.getElementById('triview-versions').style.display = '';
if (btn) btn.disabled = false;
renderTriView();
refreshSideAssets();
updateBottom();
Shell.toast('三视图已生成 · 当前版本已加入商品', v.label);
}, 1200);
}
function triViewRegenerate() {
const btn = document.getElementById('pane2-gen');
if (btn) btn.disabled = true;
Shell.toast('重新生成中', '~¥0.30 · 新增版本');
setTimeout(() => {
const v = { id: uid(), label: `v${state.triViews.length + 1}`, createdAt: new Date().toLocaleTimeString().slice(0, 5) };
state.triViews.push(v);
state.triCurrent = v.id;
if (btn) btn.disabled = false;
renderTriView();
refreshSideAssets();
updateBottom();
Shell.toast('新版本已生成 · 已切换为加入商品的版本', v.label);
}, 1000);
}
function renderTriView() {
const display = document.getElementById('triview-display');
const vtag = document.getElementById('triview-vtag');
const list = document.getElementById('triview-list');
if (!display || !list) return;
const cur = state.triViews.find(v => v.id === state.triCurrent) || state.triViews[state.triViews.length - 1];
if (cur) {
const ph = display.querySelector('.ph-frame');
if (ph) ph.innerHTML = `[ 正面 ] [ 侧面 ] [ 背面 ]<br>三视角合一 · ${cur.label} · ${cur.createdAt}`;
if (vtag) vtag.textContent = `当前 · ${cur.label} · ${cur.createdAt} (已加入商品)`;
}
if (state.triViews.length === 0) {
list.innerHTML = '<div class="tv-empty">// 暂无版本</div>';
return;
}
list.innerHTML = state.triViews.map(v => `
<div class="tv-item ${v.id === state.triCurrent ? 'current' : ''}" data-id="${v.id}" title="${v.id === state.triCurrent ? '当前加入商品的版本' : '点击切换为加入商品的版本'}">
<div class="placeholder"><span class="ph-frame">${v.label}</span></div>
<span class="tv-vtag">${v.label} · ${v.createdAt}${v.id === state.triCurrent ? ' · ✓' : ''}</span>
</div>
`).join('');
list.querySelectorAll('.tv-item').forEach(it => {
it.onclick = () => {
state.triCurrent = it.dataset.id;
renderTriView();
refreshSideAssets();
Shell.toast('已切换加入商品的三视图版本');
};
});
}
// ============================================================
// Step 3 · 上身图生成 (SUB 2)
// 模特库模卡: 不入资产 · 上身图入资产
// 我的模卡: 入资产 · 上身图入资产
// ============================================================
const OUTFIT_LABELS = ['持物半身', '使用中', '镜前自拍', '全身展示'];
function getModelById(id) {
// own
const own = state.ownModels.find(m => m.id === id);
if (own) return { id: own.id, name: own.name, role: own.role, kind: 'mine' };
// lib (from DOM)
const card = document.querySelector(`#model-grid .model-card[data-id="${id}"]`);
if (card) return { id, name: card.dataset.name, role: card.dataset.role || '', kind: card.dataset.kind || 'lib' };
return null;
}
// 按钮触发 · 仅为还没生成过 outfits 的已选模特生成
function generateOutfitsForSelected() {
const targets = Array.from(state.selectedModels).filter(id => !state.outfits[id]);
if (targets.length === 0) {
Shell.toast('已选模特都已生成 · 切换到上身图查看');
showSub(2);
return;
}
Shell.toast(`正在生成 ${targets.length * 4} 张上身图`, `${(targets.length * 0.4).toFixed(2)} · ${targets.length} 模特`);
setTimeout(() => {
targets.forEach(id => {
state.outfits[id] = OUTFIT_LABELS.map(label => ({ id: uid(), label }));
});
showSub(2);
renderOutfits();
refreshSideAssets();
updateBottom();
Shell.toast('上身图已生成 · 勾选喜欢的加入商品库');
}, 1500);
}
const PICK_SVG = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 8.5l3 3 6-6"/></svg>';
function renderOutfits() {
const wrap = document.getElementById('outfits-wrap');
if (!wrap) return;
const modelsWithOutfits = Array.from(state.selectedModels).filter(id => state.outfits[id]);
if (modelsWithOutfits.length === 0) {
wrap.innerHTML = `
<div class="outfit-empty-step">
<div class="ee-t">还未生成上身图</div>
<div class="ee-d">// 回到 SUB1 勾选模特 · 点底部「生成上身图」按钮</div>
<div class="ee-btn"><button class="btn" onclick="showSub(1)">← 返回挑选模特</button></div>
</div>
`;
return;
}
wrap.innerHTML = modelsWithOutfits.map(modelId => {
const m = getModelById(modelId);
if (!m) return '';
const cards = state.outfits[modelId] || [];
const regenN = state.outfitRegenCount[modelId] || 0;
const pickedInBlock = cards.filter(c => state.pickedOutfits.has(c.id)).length;
return `
<div class="outfit-block ${m.kind === 'mine' ? 'kind-mine' : ''}" data-model="${modelId}">
<div class="ob-h">
<span class="ob-mname">
${esc(m.name)}
<span class="ob-mkind">${m.kind === 'mine' ? '我的' : '模特库'}</span>
</span>
<span class="ob-meta">// 4 张 · 已加入商品 ${pickedInBlock}${regenN > 0 ? ` · 已重生 ${regenN}` : ''}</span>
<button class="ob-regen-all" data-act="regen-all-outfit" data-model="${modelId}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3.5-7.1L21 8M21 4v4h-4"/></svg>
整组重生
</button>
</div>
<div class="outfit-grid">
${cards.map(c => `
<div class="outfit-card ${state.pickedOutfits.has(c.id) ? 'selected' : ''}" data-cid="${c.id}" data-model="${modelId}">
<div class="placeholder"><span class="ph-frame">${esc(m.name)}<br>${esc(c.label)}</span></div>
<span class="oc-tag">${esc(c.label)}</span>
<div class="oc-actions">
<button class="oc-btn" data-act="regen-one" data-model="${modelId}" data-cid="${c.id}" title="单张重生">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3.5-7.1L21 8M21 4v4h-4"/></svg>
</button>
<button class="oc-btn del" data-act="del-outfit" data-model="${modelId}" data-cid="${c.id}" title="删除">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
</button>
</div>
<button class="pick-chip ${state.pickedOutfits.has(c.id) ? 'picked' : ''}" data-act="pick-outfit" data-cid="${c.id}">
${PICK_SVG}${state.pickedOutfits.has(c.id) ? '已加入商品' : '加入商品'}
</button>
</div>
`).join('')}
</div>
</div>
`;
}).join('');
wrap.querySelectorAll('[data-act="regen-all-outfit"]').forEach(b => {
b.onclick = e => { e.stopPropagation(); regenAllOutfitsFor(b.dataset.model); };
});
wrap.querySelectorAll('[data-act="regen-one"]').forEach(b => {
b.onclick = e => { e.stopPropagation(); regenOneOutfit(b.dataset.model, b.dataset.cid); };
});
wrap.querySelectorAll('[data-act="del-outfit"]').forEach(b => {
b.onclick = e => { e.stopPropagation(); delOutfitCard(b.dataset.model, b.dataset.cid); };
});
wrap.querySelectorAll('[data-act="pick-outfit"]').forEach(b => {
b.onclick = e => { e.stopPropagation(); togglePickOutfit(b.dataset.cid); };
});
}
function togglePickOutfit(cardId) {
if (state.pickedOutfits.has(cardId)) state.pickedOutfits.delete(cardId);
else state.pickedOutfits.add(cardId);
renderOutfits();
refreshSideAssets();
}
function regenAllOutfitsFor(modelId) {
// 老 cards 的 picked 状态保留(资产库仍存),新 cards 替换中央展示
state.outfitRegenCount[modelId] = (state.outfitRegenCount[modelId] || 0) + 1;
const v = state.outfitRegenCount[modelId];
// 移除旧 picked 标记(因为中央只展示新版本,旧版仅存档)
(state.outfits[modelId] || []).forEach(c => state.pickedOutfits.delete(c.id));
state.outfits[modelId] = OUTFIT_LABELS.map(label => ({ id: uid(), label: `${label} v${v + 1}` }));
renderOutfits(); refreshSideAssets();
const m = getModelById(modelId);
Shell.toast(`${m?.name || '模特'} 整组重生`, '~¥0.40 · 4 张');
}
function regenOneOutfit(modelId, cardId) {
const arr = state.outfits[modelId];
if (!arr) return;
const i = arr.findIndex(c => c.id === cardId);
if (i < 0) return;
state.pickedOutfits.delete(cardId);
const old = arr[i];
arr[i] = { id: uid(), label: `${old.label.split(' v')[0]}` };
renderOutfits(); refreshSideAssets();
Shell.toast('单张已重生', '~¥0.10');
}
function delOutfitCard(modelId, cardId) {
const arr = state.outfits[modelId];
if (!arr) return;
state.pickedOutfits.delete(cardId);
state.outfits[modelId] = arr.filter(c => c.id !== cardId);
renderOutfits(); refreshSideAssets();
Shell.toast('已删除上身图');
}
// ============================================================
// Step 3 · 上传新模特 · 弹窗
// ============================================================
let nmSourceImg = null;
const nmModal = document.getElementById('nm-modal');
const nmFile = document.getElementById('nm-file');
const nmZone = document.getElementById('nm-up-zone');
const nmSubmit = document.getElementById('nm-submit');
window.openNewModelModal = function() {
nmModal.classList.add('open');
};
window.closeNewModelModal = function() {
nmModal.classList.remove('open');
};
function resetNewModelForm() {
nmSourceImg = null;
document.getElementById('nm-name').value = '';
document.getElementById('nm-role').value = '';
document.getElementById('nm-gender').value = '女';
document.getElementById('nm-age').value = '22-28';
document.querySelectorAll('#nm-tags .nm-tag-chip').forEach(c => c.classList.remove('active'));
nmZone.classList.remove('has-img');
nmZone.innerHTML = `
<span class="nm-uz-ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg></span>
<span class="nm-uz-t">点击上传参考图</span>
<span class="nm-uz-d">建议正面半身照 · JPG / PNG / WEBP</span>
<button class="nm-uz-replace" type="button">替换</button>
`;
}
document.getElementById('add-model-card')?.addEventListener('click', () => {
resetNewModelForm();
openNewModelModal();
});
nmModal?.addEventListener('click', e => { if (e.target === nmModal) closeNewModelModal(); });
nmZone?.addEventListener('click', () => nmFile.click());
nmFile?.addEventListener('change', e => {
const f = e.target.files?.[0];
if (!f || !f.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = ev => {
nmSourceImg = ev.target.result;
nmZone.classList.add('has-img');
nmZone.innerHTML = `<img src="${nmSourceImg}" alt=""><button class="nm-uz-replace" type="button">替换</button>`;
};
reader.readAsDataURL(f);
e.target.value = '';
});
document.querySelectorAll('#nm-tags .nm-tag-chip').forEach(chip => {
chip.onclick = e => { e.preventDefault(); chip.classList.toggle('active'); };
});
nmSubmit?.addEventListener('click', () => {
const name = document.getElementById('nm-name').value.trim();
if (!nmSourceImg) { Shell.toast('请上传参考图'); return; }
if (!name) { Shell.toast('请填写模特名称'); return; }
const gender = document.getElementById('nm-gender').value;
const age = document.getElementById('nm-age').value;
const role = document.getElementById('nm-role').value.trim() || '我的模特';
const tags = [...document.querySelectorAll('#nm-tags .nm-tag-chip.active')].map(c => c.dataset.tag);
closeNewModelModal();
Shell.toast('AI 生成模卡中', '~6s · ¥0.30');
setTimeout(() => {
const id = 'mine-' + uid();
const model = {
id, kind: 'mine', name, base: `${gender} · ${age}`, role,
tags: tags.join(','), sourceImg: nmSourceImg,
createdAt: new Date().toLocaleTimeString().slice(0, 5),
};
state.ownModels.push(model);
// 注入卡片 + 自动选中
injectOwnModelCard(model);
state.selectedModels.add(id);
refreshCounts();
refreshSideAssets();
Shell.toast('模卡已生成并入资产库', `${name} · 已自动选中`);
}, 900);
});
function injectOwnModelCard(m) {
const grid = document.getElementById('model-grid');
if (!grid) return;
const div = document.createElement('div');
div.className = 'model-card kind-mine selected';
div.dataset.kind = 'mine';
div.dataset.id = m.id;
div.dataset.name = m.name;
div.dataset.base = m.base;
div.dataset.role = m.role;
div.dataset.tags = m.tags;
div.innerHTML = `
<span class="source-tag">[ 我的 ]</span>
<div class="placeholder"><img src="${m.sourceImg}" style="width:100%;height:100%;object-fit:cover;"></div>
<div class="lbl-bottom"><div class="nm">${esc(m.name)}</div><div class="tags">${esc(m.base)} · ${esc(m.role)}</div></div>
`;
// 插入到 "+ 上传新模特" 之后
const addCard = document.getElementById('add-model-card');
if (addCard && addCard.nextSibling) grid.insertBefore(div, addCard.nextSibling);
else grid.appendChild(div);
bindModelCard(div);
// 若当前为"全部"或"我的"视图则显示,否则隐藏
const activeKind = document.querySelector('#kind-toggle .kt-btn.active')?.dataset.kind || 'all';
if (activeKind === 'lib') div.style.display = 'none';
}
// ============================================================
// Step 4 · 平台头图
// ============================================================
function renderPlatformResults() {
const wrap = document.getElementById('platform-results');
if (!wrap) return;
const keys = Object.keys(state.platforms);
if (keys.length === 0) {
wrap.innerHTML = '<div class="platform-empty mono">// 勾选上方平台,点击下方「生成平台头图」按钮开始</div>';
return;
}
wrap.innerHTML = keys.map(pk => {
const p = state.platforms[pk];
const meta = PLATFORM_META[pk];
return `
<div class="platform-block" data-plat="${pk}">
<div class="pb-h">
<span class="pb-name">${meta.name}</span>
<span class="pb-spec">${meta.spec}</span>
<button class="pb-regen" data-act="regen-all" data-plat="${pk}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3.5-7.1L21 8M21 4v4h-4"/></svg>
整组重生
</button>
</div>
<div class="pb-grid">
${p.cards.map((c, i) => `
<div class="pb-card ${state.pickedPlatformCards.has(c.id) ? 'selected' : ''}" data-cid="${c.id}" data-plat="${pk}">
<div class="placeholder"><span class="ph-frame">${meta.name}<br>候选 ${i+1}</span></div>
<button class="pb-x" data-act="del" data-plat="${pk}" data-cid="${c.id}">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
</button>
<span class="pb-vtag">${c.label || `候选${i+1}`}</span>
<button class="pick-chip ${state.pickedPlatformCards.has(c.id) ? 'picked' : ''}" data-act="pick-plat" data-cid="${c.id}">
${PICK_SVG}${state.pickedPlatformCards.has(c.id) ? '已加入' : '加入商品'}
</button>
</div>
`).join('')}
</div>
</div>
`;
}).join('');
wrap.querySelectorAll('[data-act="regen-all"]').forEach(b => {
b.onclick = e => { e.stopPropagation(); regenPlatform(b.dataset.plat); };
});
wrap.querySelectorAll('[data-act="del"]').forEach(b => {
b.onclick = e => {
e.stopPropagation();
const pk = b.dataset.plat, cid = b.dataset.cid;
state.pickedPlatformCards.delete(cid);
state.platforms[pk].cards = state.platforms[pk].cards.filter(c => c.id !== cid);
if (state.platforms[pk].cards.length === 0) delete state.platforms[pk];
renderPlatformResults(); refreshSideAssets();
Shell.toast('已删除候选');
};
});
wrap.querySelectorAll('[data-act="pick-plat"]').forEach(b => {
b.onclick = e => { e.stopPropagation(); togglePickPlatform(b.dataset.cid); };
});
}
function togglePickPlatform(cardId) {
if (state.pickedPlatformCards.has(cardId)) state.pickedPlatformCards.delete(cardId);
else state.pickedPlatformCards.add(cardId);
renderPlatformResults();
refreshSideAssets();
}
function genPlatform(pk) {
if (!state.platforms[pk]) state.platforms[pk] = { cards: [], regenCount: 0 };
state.platforms[pk].cards = Array.from({ length: 4 }, (_, i) => ({ id: uid(), label: `候选${i+1}` }));
}
function regenPlatform(pk) {
(state.platforms[pk]?.cards || []).forEach(c => state.pickedPlatformCards.delete(c.id));
state.platforms[pk].cards = Array.from({ length: 4 }, (_, i) => ({ id: uid(), label: `候选${i+1} v${++state.platforms[pk].regenCount + 1}` }));
renderPlatformResults(); refreshSideAssets();
Shell.toast(`${PLATFORM_META[pk].name} 整组重生`, '~¥0.50');
}
// 按钮触发 · 仅为还没生成过 platforms 的已勾选平台生成
function generatePlatformsForSelected() {
const targets = Array.from(state.selectedPlatforms).filter(pk => !state.platforms[pk]);
if (targets.length === 0) {
Shell.toast('已勾选平台都已生成 · 可勾选「加入商品」');
return;
}
Shell.toast(`正在生成 ${targets.length * 4} 张平台头图`, `${(targets.length * 0.5).toFixed(2)} · ${targets.length} 平台`);
setTimeout(() => {
targets.forEach(pk => genPlatform(pk));
renderPlatformResults();
refreshSideAssets();
updateBottom();
Shell.toast('平台头图已生成 · 勾选喜欢的加入商品库');
}, 1500);
}
// 平台 checkbox 变化 → 仅记录勾选状态,不再自动生成
document.querySelectorAll('.plat-card input[type="checkbox"]').forEach(cb => {
cb.onchange = () => {
const pk = cb.dataset.plat;
if (cb.checked) {
state.selectedPlatforms.add(pk);
} else {
state.selectedPlatforms.delete(pk);
// 取消勾选时,如该平台已生成,保留资产但提示
if (state.platforms[pk]) {
state.platforms[pk].cards.forEach(c => state.pickedPlatformCards.delete(c.id));
delete state.platforms[pk];
renderPlatformResults();
refreshSideAssets();
}
}
updateBottom();
};
});
// 商品名 / 品类 输入时同步右栏资产预览
document.getElementById('ai-name')?.addEventListener('input', renderAssetPreview);
document.getElementById('ai-cat')?.addEventListener('change', renderAssetPreview);
// ============================================================
// INIT
// ============================================================
bindStepButtons();
refreshKindNums();
showStep(1);
refreshSideAssets();
</script>
</body>
</html>