AirShelf/电商AI平台/model-photo.html
iye 553014cc79
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
更新电商AI平台原型交互
2026-05-25 19:12:56 +08:00

4752 lines
207 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>
/* viewport-fit · 工作台铺满 (跟 demo-a 一致 · padding:0) */
.app { height: 100vh; overflow: hidden; }
main { display: flex; flex-direction: column; min-height: 0; }
#page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }
.mp-layout {
flex: 1; min-height: 0;
display: grid;
grid-template-columns: 260px 1fr;
}
@media (max-width: 1280px) {
.mp-layout { grid-template-columns: 240px 1fr; }
}
/* ─── 主区: flat 双区 (头部 + body) · 无 card · 用 border 分隔 ─── */
.mp-main {
display: flex; flex-direction: column;
min-height: 0;
overflow: hidden;
}
/* 主区头部 · toolbar 风格 (跟 image-optimize 一致) */
.mp-main-h {
flex-shrink: 0;
position: relative; z-index: 20;
display: flex; align-items: center; gap: 10px;
padding: 12px 28px;
border-bottom: 1px solid var(--border-faint);
background: var(--surface);
}
.mp-main-h .cur-title {
display: flex; align-items: baseline; gap: 8px;
min-width: 0; max-width: 50%;
}
.mp-main-h .cur-title .crumb {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .04em;
flex-shrink: 0;
}
.mp-main-h .cur-title .nm {
font-size: 15px; font-weight: 600;
color: var(--accent-black);
letter-spacing: -.005em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mp-main-h .cur-title .nm.placeholder {
font-weight: 400; font-size: 13px;
color: var(--black-alpha-48);
}
.mp-main-h .spacer { flex: 1; }
.mp-main-h .search-btn {
width: 32px; height: 32px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
cursor: pointer;
display: grid; place-items: center;
transition: border-color var(--t-base), color var(--t-base);
}
.mp-main-h .search-btn:hover { border-color: var(--heat-20); color: var(--heat); }
.mp-main-h .search-btn svg { width: 14px; height: 14px; }
.mp-main-h .tb-chip {
display: inline-flex; align-items: center; gap: 6px;
height: 32px; padding: 0 10px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-size: 12.5px; color: var(--black-alpha-72);
font-family: inherit; cursor: pointer;
transition: border-color var(--t-base), color var(--t-base);
}
.mp-main-h .tb-chip:hover { border-color: var(--heat-20); color: var(--heat); }
.mp-main-h .tb-chip svg { width: 10px; height: 10px; opacity: .6; }
.mp-main-h .tb-chip.active {
background: var(--heat-12);
border-color: var(--heat-40);
color: var(--heat);
}
.mp-main-h .tb-chip.active svg { opacity: .8; }
.mp-main-h .tb-chip.active .lbl::after {
content: ':';
margin: 0 2px 0 0;
}
/* search · 折叠图标态 + 展开输入框 */
.mp-main-h .tb-search-wrap { display: flex; align-items: center; }
.mp-main-h .tb-search-input {
width: 0; height: 32px;
padding: 0; margin: 0;
border: 1px solid transparent;
background: transparent;
border-radius: var(--r-sm);
font-size: 12.5px; color: var(--accent-black);
font-family: inherit; outline: none;
transition: width var(--t-base), padding var(--t-base), border-color var(--t-base), margin var(--t-base), background var(--t-base);
}
.mp-main-h .tb-search-wrap.expanded .tb-search-input {
width: 220px;
padding: 0 10px;
margin-left: 6px;
background: var(--surface);
border-color: var(--border-faint);
}
.mp-main-h .tb-search-wrap.expanded .tb-search-input:focus { border-color: var(--heat-40); }
.mp-main-h .tb-search-wrap.expanded .search-btn { border-color: var(--heat-40); color: var(--heat); }
/* dropdown · chip 下拉菜单 */
.mp-main-h .tb-menu-wrap { position: relative; }
.mp-main-h .tb-menu {
position: absolute;
top: calc(100% + 6px); right: 0;
min-width: 160px; max-width: 260px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
box-shadow: 0 8px 24px rgba(0,0,0,.08);
padding: 4px;
z-index: 50;
display: none;
max-height: 320px; overflow-y: auto;
}
.mp-main-h .tb-menu-wrap.open .tb-menu { display: block; }
.mp-main-h .tb-menu-item {
display: flex; align-items: center; gap: 8px;
width: 100%; padding: 7px 10px;
background: transparent; border: 0;
border-radius: 4px;
font-size: 12.5px; color: var(--black-alpha-72);
font-family: inherit; cursor: pointer; text-align: left;
transition: background var(--t-base), color var(--t-base);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mp-main-h .tb-menu-item:hover { background: var(--background-lighter); color: var(--accent-black); }
.mp-main-h .tb-menu-item.active {
background: var(--heat-12); color: var(--heat); font-weight: 500;
}
.mp-main-h .tb-menu-empty {
padding: 10px;
font-size: 11.5px; color: var(--black-alpha-48);
font-family: var(--font-mono); letter-spacing: .02em;
text-align: center;
}
/* 批次被筛选隐藏 */
.mp-result-batch[data-hidden="1"] { display: none !important; }
.mp-main-body {
flex: 1; min-height: 0;
display: grid;
grid-template-columns: 320px 1fr;
}
@media (max-width: 1280px) {
.mp-main-body { grid-template-columns: 300px 1fr; }
}
/* 内部 form / preview 不再带 border + radius, 由 layout 用 border-right 分隔 */
.mp-main-body > .mp-form {
border: 0; border-radius: 0;
border-right: 1px solid var(--border-faint);
background: var(--surface);
}
.mp-main-body > .mp-preview {
border: 0; border-radius: 0;
background: var(--background-base);
}
/* ─── 商品空间 (最左 · 单选 · 当前商品决定结果区批次) ─── */
.mp-prod-space {
background: var(--surface);
border-right: 1px solid var(--border-faint);
display: flex; flex-direction: column;
min-height: 0;
overflow: hidden;
}
/* 顶部工具栏: 返回 + 折叠 (跟 image-optimize 视觉一致) */
.mp-side-top {
flex-shrink: 0;
display: flex; align-items: center; gap: 8px;
padding: 14px 14px 10px;
border-bottom: 1px solid var(--border-faint);
}
.mp-side-top .back-pill {
display: inline-flex; align-items: center; gap: 6px;
height: 28px; padding: 0 12px 0 8px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-pill);
color: var(--accent-black);
font-size: 12.5px; font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.mp-side-top .back-pill:hover {
background: var(--heat-12);
border-color: var(--heat-20);
color: var(--heat);
}
.mp-side-top .back-pill svg { width: 14px; height: 14px; }
.mp-side-top .fold {
margin-left: auto;
width: 26px; height: 26px;
background: transparent; border: 0; border-radius: var(--r-sm);
display: grid; place-items: center;
color: var(--black-alpha-48); cursor: pointer;
transition: background var(--t-base), color var(--t-base);
}
.mp-side-top .fold:hover { background: var(--black-alpha-4); color: var(--accent-black); }
.mp-side-top .fold svg { width: 14px; height: 14px; }
.mp-ps-h {
flex-shrink: 0;
padding: 12px 14px 10px;
}
/* 商品列表 头部 (// 商品空间 + 新建商品 主 CTA · 右上显眼橙色) */
.mp-list-h {
flex-shrink: 0;
display: flex; align-items: center; gap: 8px;
padding: 4px 14px 10px;
}
.mp-list-h .mono {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .06em;
text-transform: uppercase;
}
.mp-list-h .new-prod {
margin-left: auto;
height: 28px; padding: 0 12px 0 10px;
display: inline-flex; align-items: center; gap: 6px;
background: var(--heat); color: #fff;
border: 1px solid var(--heat);
border-radius: var(--r-sm);
font-size: 12px; font-weight: 600;
font-family: inherit;
cursor: pointer;
box-shadow:
inset 0 -2px 4px rgba(250, 93, 25, 0.20),
0 1px 1px rgba(250, 93, 25, 0.12),
0 2px 4px rgba(250, 93, 25, 0.10);
transition: filter var(--t-base), transform var(--t-fast), box-shadow var(--t-base);
}
.mp-list-h .new-prod:hover {
filter: brightness(.96);
box-shadow:
inset 0 -2px 4px rgba(250, 93, 25, 0.20),
0 1px 1px rgba(250, 93, 25, 0.16),
0 4px 8px rgba(250, 93, 25, 0.20);
}
.mp-list-h .new-prod:active { transform: scale(.98); }
.mp-list-h .new-prod svg { width: 12px; height: 12px; }
/* 搜索框 (spec §10.3 · icon z-index:2) */
.mp-ps-search {
position: relative;
height: 32px;
}
.mp-ps-search svg {
position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
width: 13px; height: 13px;
color: var(--black-alpha-48);
z-index: 2;
pointer-events: none;
}
.mp-ps-search input {
width: 100%; height: 100%;
padding: 0 10px 0 30px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-size: 12.5px; color: var(--accent-black);
font-family: inherit;
outline: none;
transition: border-color var(--t-base), background var(--t-base);
}
.mp-ps-search input:focus { border-color: var(--heat-40); background: var(--surface); }
.mp-ps-search input::placeholder { color: var(--black-alpha-48); }
/* 商品列表 (flex:1 占据中部所有剩余空间) */
.mp-ps-list {
flex: 1; min-height: 0;
overflow-y: auto;
padding: 4px 10px 10px;
display: flex; flex-direction: column; gap: 4px;
}
.mp-ps-empty {
padding: 24px 14px;
text-align: center;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .04em;
line-height: 1.7;
}
.mp-prod-item {
display: flex; align-items: center; gap: 10px;
padding: 8px;
border: 1px solid transparent;
border-radius: var(--r-sm);
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base);
}
.mp-prod-item:hover { background: var(--black-alpha-4); }
.mp-prod-item.active { background: var(--heat-12); border-color: var(--heat-20); }
.mp-prod-item .thumb {
flex-shrink: 0;
width: 36px; height: 36px;
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
overflow: hidden;
}
.mp-prod-item.active .thumb { border-color: var(--heat); }
.mp-prod-item .body { flex: 1; min-width: 0; }
.mp-prod-item .nm {
font-size: 12.5px;
color: var(--accent-black); font-weight: 500;
line-height: 1.3;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.mp-prod-item.active .nm { color: var(--heat); font-weight: 600; }
.mp-prod-item .sub {
margin-top: 2px;
font-family: var(--font-mono); font-size: 10px;
color: var(--black-alpha-48); letter-spacing: .02em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 全部商品入口 (贴底) */
.mp-ps-all {
flex-shrink: 0;
margin: 0 10px 12px;
padding: 9px 12px;
background: var(--background-lighter);
border: 1px dashed var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
font-size: 12px;
font-family: inherit;
cursor: pointer;
display: flex; align-items: center; gap: 8px;
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.mp-ps-all:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
.mp-ps-all .ct {
margin-left: auto;
color: var(--black-alpha-48);
font-family: var(--font-mono); font-size: 10.5px;
}
.mp-ps-all:hover .ct { color: var(--heat); }
.mp-ps-all svg { width: 12px; height: 12px; }
/* ─── 左栏 · 表单 ─── */
.mp-form {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
overflow-y: auto;
padding: 18px 20px;
display: flex; flex-direction: column;
}
.mp-step { margin-bottom: 22px; }
.mp-step:last-child { margin-bottom: 0; }
.mp-step-h {
display: flex; align-items: center; gap: 8px;
margin-bottom: 12px;
}
.mp-step-h .num {
width: 22px; height: 22px;
border-radius: 50%;
background: var(--heat-12); color: var(--heat);
font-family: var(--font-mono); font-size: 11px;
font-weight: 700;
display: grid; place-items: center;
flex-shrink: 0;
}
.mp-step-h .title { font-size: 14px; font-weight: 600; color: var(--accent-black); }
.mp-step-h .right { margin-left: auto; font-size: 12px; color: var(--heat); cursor: pointer; }
.mp-step-h .right:hover { text-decoration: underline; }
/* 商品选择器 · 已选 chip 列表 */
.prod-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 6px; }
.prod-row {
display: flex; align-items: center; gap: 10px;
padding: 8px 10px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
transition: background var(--t-base), border-color var(--t-base);
}
.prod-row .thumb {
width: 28px; height: 28px;
flex-shrink: 0;
border-radius: var(--r-sm);
}
.prod-row .info { flex: 1; min-width: 0; }
.prod-row .nm {
font-size: 12.5px; color: var(--accent-black); font-weight: 500;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.prod-row .meta {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
margin-top: 2px;
}
.prod-row .x {
width: 22px; height: 22px;
display: grid; place-items: center;
background: transparent; border: 0;
border-radius: var(--r-sm);
cursor: pointer;
color: var(--black-alpha-48);
flex-shrink: 0;
transition: background var(--t-base), color var(--t-base);
}
.prod-row .x:hover { background: var(--black-alpha-8); color: var(--accent-crimson); }
.prod-row .x svg { width: 12px; height: 12px; }
.prod-row .swap {
width: 22px; height: 22px;
display: grid; place-items: center;
background: transparent; border: 0;
border-radius: var(--r-sm);
cursor: pointer;
color: var(--black-alpha-48);
flex-shrink: 0;
transition: background var(--t-base), color var(--t-base);
}
.prod-row .swap:hover { background: var(--heat-12); color: var(--heat); }
.prod-row .swap svg { width: 13px; height: 13px; }
.prod-empty {
padding: 14px 10px;
text-align: center;
background: var(--background-lighter);
border: 1px dashed var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-48);
font-size: 12px;
margin-bottom: 6px;
}
.prod-empty .mono {
font-family: var(--font-mono);
font-size: 10.5px;
letter-spacing: .02em;
margin-top: 4px;
}
.prod-add {
display: flex; align-items: center; justify-content: center; gap: 6px;
padding: 8px 10px;
background: transparent;
border: 1px dashed var(--heat-40);
border-radius: var(--r-sm);
cursor: pointer;
font-size: 12.5px;
color: var(--heat);
font-family: inherit;
transition: background var(--t-base);
width: 100%;
}
.prod-add:hover { background: var(--heat-12); }
.prod-add svg { width: 12px; height: 12px; }
.prod-add[hidden] { display: none; }
/* 商品库全屏弹窗 (复用 ml-modal-bg 结构) */
.pl-modal-bg {
position: fixed; inset: 0;
background: var(--surface);
z-index: 998;
display: none;
}
.pl-modal-bg.show { display: flex; }
.pl-modal {
margin: 0;
flex: 1;
background: var(--surface);
overflow: hidden;
display: flex; flex-direction: column;
}
.pl-modal-h {
display: flex; align-items: center; gap: 14px;
padding: 14px 28px;
border-bottom: 1px solid var(--border-faint);
flex-shrink: 0;
}
.pl-modal-h h2 { font-size: 16px; font-weight: 600; }
.pl-modal-h .ct {
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
.pl-modal-h .actions { margin-left: auto; display: flex; gap: 10px; }
.pl-modal-body {
flex: 1; min-height: 0;
display: grid;
grid-template-columns: 200px 1fr;
}
.pl-side {
border-right: 1px solid var(--border-faint);
padding: 18px 0;
overflow-y: auto;
}
.pl-side .pl-side-h {
padding: 0 20px 8px;
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .06em;
}
.pl-side .pl-side-item {
display: flex; align-items: center; gap: 8px;
padding: 9px 20px;
cursor: pointer;
color: var(--black-alpha-72);
font-size: 13px;
border-left: 3px solid transparent;
transition: background var(--t-base), color var(--t-base);
}
.pl-side .pl-side-item:hover { background: var(--black-alpha-4); }
.pl-side .pl-side-item.active {
background: var(--heat-12);
color: var(--accent-black);
border-left-color: var(--heat);
font-weight: 600;
}
.pl-side .pl-side-item .ct {
margin-left: auto;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48);
}
.pl-main { overflow-y: auto; padding: 0; display: flex; flex-direction: column; }
.pl-toolbar {
padding: 14px 28px;
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center; gap: 12px;
flex-shrink: 0;
}
.pl-toolbar .search {
position: relative; flex: 1; max-width: 360px;
}
.pl-toolbar .search input {
width: 100%; height: 32px;
padding: 0 10px 0 32px;
background: var(--background-lighter);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-sm);
font-size: 12.5px;
font-family: inherit;
color: var(--accent-black);
outline: none;
}
.pl-toolbar .search svg {
position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
width: 14px; height: 14px; color: var(--black-alpha-48);
}
.pl-toolbar .btn-new {
height: 32px;
padding: 0 14px;
display: inline-flex; align-items: center; gap: 6px;
background: var(--surface);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-sm);
color: var(--accent-black);
font-family: inherit;
font-size: 12.5px;
cursor: pointer;
margin-left: auto;
transition: background var(--t-base), border-color var(--t-base);
}
.pl-toolbar .btn-new:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }
.pl-toolbar .btn-new svg { width: 13px; height: 13px; }
.pl-scroll {
flex: 1; min-height: 0;
overflow-y: auto;
padding: 20px 28px 28px;
}
.pl-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
}
.pl-card {
position: relative;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 10px;
cursor: pointer;
display: flex; flex-direction: column; gap: 6px;
transition: background var(--t-base), border-color var(--t-base);
}
.pl-card:hover { background: var(--surface); }
.pl-card.selected { border-color: var(--heat); background: var(--heat-12); }
.pl-card .pl-thumb {
aspect-ratio: 1;
border-radius: var(--r-sm);
}
.pl-card .pl-name { font-size: 12.5px; font-weight: 500; color: var(--accent-black); }
.pl-card .pl-meta {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.pl-card .pl-check {
position: absolute; top: 16px; right: 16px;
width: 22px; height: 22px;
background: rgba(255,255,255,.95);
border: 1.5px solid var(--black-alpha-24);
border-radius: 50%;
display: grid; place-items: center;
z-index: 2;
color: var(--accent-white);
}
.pl-card .pl-check svg { width: 11px; height: 11px; opacity: 0; }
.pl-card.selected .pl-check { background: var(--heat); border-color: var(--heat); }
.pl-card.selected .pl-check svg { opacity: 1; }
/* 卡片右上 actions: 编辑 + 删除 (hover 显示, check 左侧) */
.pl-card .pl-card-actions {
position: absolute;
top: 14px; right: 44px;
display: flex; gap: 4px;
z-index: 2;
opacity: 0;
transition: opacity var(--t-base);
}
.pl-card:hover .pl-card-actions { opacity: 1; }
.pl-card .pl-act {
width: 26px; height: 26px;
background: rgba(255,255,255,.95);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-sm);
display: grid; place-items: center;
cursor: pointer;
color: var(--black-alpha-72);
transition: color var(--t-base), border-color var(--t-base), background var(--t-base);
}
.pl-card .pl-act:hover { color: var(--heat); border-color: var(--heat); background: var(--surface); }
.pl-card .pl-act.danger:hover { color: var(--accent-crimson); border-color: var(--accent-crimson); }
.pl-card .pl-act svg { width: 12px; height: 12px; }
.pl-modal-f {
padding: 14px 28px;
border-top: 1px solid var(--border-faint);
display: flex; justify-content: flex-end; align-items: center; gap: 10px;
flex-shrink: 0;
}
.pl-modal-f .summary {
margin-right: auto;
font-family: var(--font-mono);
font-size: 12px;
color: var(--black-alpha-56);
letter-spacing: .02em;
}
.pl-modal-f .summary b { color: var(--heat); font-weight: 700; }
/* 模特选择 · 矩形卡 多选 */
.model-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.model-card {
position: relative;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 8px;
cursor: pointer;
display: flex; flex-direction: column; gap: 6px;
transition: background var(--t-base), border-color var(--t-base);
}
.model-card:hover { background: var(--surface); }
.model-card.selected { border-color: var(--heat); background: var(--heat-12); }
.model-card .m-thumb {
aspect-ratio: 3/4;
border-radius: var(--r-sm);
cursor: pointer;
position: relative;
}
.model-card .m-name { font-size: 12.5px; font-weight: 500; color: var(--accent-black); }
.model-card .m-tag {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.model-card .m-check {
position: absolute; top: 14px; right: 14px;
width: 22px; height: 22px;
background: rgba(255,255,255,.95);
border: 1.5px solid var(--black-alpha-24);
border-radius: 50%;
display: grid; place-items: center;
color: var(--accent-white);
z-index: 2;
}
.model-card .m-check svg { width: 11px; height: 11px; opacity: 0; }
.model-card.selected .m-check { background: var(--heat); border-color: var(--heat); }
.model-card.selected .m-check svg { opacity: 1; }
.model-upload {
grid-column: 1 / -1;
aspect-ratio: 2/1;
border: 1.5px dashed var(--heat-40);
background: var(--heat-12);
border-radius: var(--r-md);
display: flex; align-items: center; justify-content: center; gap: 6px;
cursor: pointer;
color: var(--heat);
font-size: 12.5px;
transition: background var(--t-base);
}
.model-upload:hover { background: var(--heat-20); }
.model-upload svg { width: 14px; height: 14px; }
/* 单选 pill */
.pill-row { display: flex; gap: 6px; }
.pill-row .opt {
flex: 1;
height: 32px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
font-size: 12.5px;
cursor: pointer;
font-family: inherit;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
}
.pill-row .opt:hover { color: var(--accent-black); }
.pill-row .opt.active {
background: var(--heat-12);
color: var(--heat);
border-color: var(--heat-40);
font-weight: 600;
}
/* 设置 · 子项 */
.mp-sub-h {
font-size: 12px; color: var(--black-alpha-48);
margin-bottom: 6px;
font-family: var(--font-mono); letter-spacing: .02em;
}
.mp-sub { margin-bottom: 12px; }
.mp-sub:last-child { margin-bottom: 0; }
/* 左栏底部 · 立即生成 */
.mp-cta {
margin-top: auto;
padding-top: 14px;
}
.mp-cta .btn-gen {
width: 100%;
justify-content: center;
padding: 12px;
font-size: 14px;
font-weight: 600;
}
.mp-cta .btn-gen:disabled,
.mp-cta .btn-gen.disabled {
opacity: .45; cursor: not-allowed; pointer-events: none;
}
.mp-cta-hint {
margin-top: 8px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .02em;
text-align: center;
line-height: 1.5;
}
/* ─── 右栏 · 预览 ─── */
.mp-preview {
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 18px 22px;
display: flex; flex-direction: column;
overflow-y: auto;
}
/* prompt-style summary 卡片 (引号 icon + 灰底 + 右上 meta) */
.mp-pv-h {
position: relative;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 14px 18px 14px 44px;
margin-bottom: 14px;
}
.mp-pv-h .quote-icon {
position: absolute;
top: 13px; left: 16px;
width: 18px; height: 18px;
color: var(--black-alpha-24);
}
.mp-pv-h .pv-meta {
float: right;
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .04em;
line-height: 1.5;
}
.mp-pv-h .pv-meta b { color: var(--accent-black); font-weight: 600; }
.mp-pv-h .pv-line {
font-size: 13px;
color: var(--accent-black);
line-height: 1.6;
display: flex; align-items: center;
}
.mp-pv-h .pv-line .k {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
letter-spacing: .04em;
margin-right: 8px;
min-width: 36px;
}
.mp-pv-h .pv-line .v {
font-weight: 500;
}
.mp-pv-h .pv-line .swap {
margin-left: 10px;
font-size: 11.5px; color: var(--heat);
cursor: pointer;
}
.mp-pv-h .pv-line .swap:hover { text-decoration: underline; }
.mp-pv-grid {
flex: 1;
display: flex; flex-direction: column;
gap: 18px;
overflow-y: auto;
padding-right: 4px;
align-content: start;
}
.mp-result-batch { display: flex; flex-direction: column; gap: 10px; }
.mp-batch-head {
display: flex; align-items: center; gap: 8px;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-56); letter-spacing: .04em;
}
.mp-batch-head .lab {
font-family: var(--font-mono); font-size: 10.5px;
padding: 2px 8px; border-radius: var(--r-pill);
background: var(--heat-12); color: var(--heat); font-weight: 600;
}
.mp-batch-head .lab.gen { background: var(--background-lighter); color: var(--black-alpha-56); }
.mp-batch-head .lab.rerun { background: var(--heat-12); color: var(--heat); }
.mp-batch-head .sep { color: var(--black-alpha-32); }
.mp-result-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
align-content: start;
}
@media (max-width: 1400px) {
.mp-result-grid { gap: 10px; }
}
/* 结果卡片 · 与图片创作 .io-cell 视觉一致 */
.mp-result {
position: relative;
aspect-ratio: 3/4;
border-radius: var(--r-md);
overflow: hidden;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
cursor: pointer;
transition: border-color var(--t-base);
}
.mp-result:hover { border-color: var(--black-alpha-32); }
.mp-result .mp-r-thumb {
position: absolute; inset: 0;
}
.mp-result .mp-r-thumb .ph-frame {
position: absolute; inset: 0;
display: grid; place-items: center;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-32); letter-spacing: .02em;
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
}
.mp-result.gen .mp-r-thumb .ph-frame { animation: mp-pulse 1.4s ease-in-out infinite; }
@keyframes mp-pulse {
0%, 100% { opacity: 1; }
50% { opacity: .55; }
}
.mp-result.err { border-color: var(--accent-crimson, #c43d3d); }
.mp-result.err .mp-r-thumb .ph-frame {
color: var(--accent-crimson, #c43d3d);
background: rgba(196, 61, 61, .05);
}
/* 右上 hover 操作组 · 同 spec §4.18 .gen-image-actions (整体容器 + 透明按钮) */
.mp-result .cell-ops {
position: absolute; top: 8px; right: 8px;
display: flex; gap: 2px;
padding: 2px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 2px 8px rgba(0,0,0,.08);
opacity: 0;
transition: opacity var(--t-base);
z-index: 2;
}
.mp-result:hover .cell-ops { opacity: 1; }
.mp-result .cell-ops button {
width: 28px; height: 28px;
background: transparent;
border: 0;
border-radius: 6px;
color: var(--black-alpha-56);
cursor: pointer;
display: grid; place-items: center;
transition: background var(--t-base), color var(--t-base);
}
.mp-result .cell-ops button:hover {
background: var(--black-alpha-4);
color: var(--accent-black);
}
.mp-result .cell-ops button svg { width: 14px; height: 14px; }
/* 中央采用反馈 · 同 spec §4.18 .gen-image-feedback */
.mp-result .cell-feedback {
position: absolute; inset: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
background: rgba(38, 38, 38, .88);
color: var(--accent-white);
border-radius: var(--r-md);
opacity: 0;
pointer-events: none;
transition: opacity .2s var(--t-base, ease);
z-index: 3;
}
.mp-result.show-feedback .cell-feedback { opacity: 1; }
.mp-result .cell-feedback svg { width: 20px; height: 20px; }
.mp-result .cell-feedback span { font-size: 12.5px; font-weight: 500; letter-spacing: .02em; }
/* 更多 · 下拉气泡 */
.mp-result .cell-more-wrap { position: relative; }
.mp-result .cell-more-menu {
position: absolute; top: calc(100% + 4px); right: 0;
min-width: 132px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 6px 24px rgba(0,0,0,.10);
padding: 4px;
display: none;
z-index: 12;
}
.mp-result .cell-more-wrap.open .cell-more-menu { display: block; }
.mp-result .cell-more-menu button {
width: 100%;
display: inline-flex !important; align-items: center; gap: 8px;
padding: 7px 10px !important;
background: transparent !important;
border: 0 !important; border-radius: var(--r-sm) !important;
font-size: 12.5px;
color: var(--accent-black) !important;
font-family: inherit;
text-align: left;
cursor: pointer;
backdrop-filter: none !important;
height: auto !important;
justify-content: flex-start !important;
}
.mp-result .cell-more-menu button:hover {
background: var(--background-lighter) !important;
color: var(--heat) !important;
}
.mp-result .cell-more-menu button.danger:hover {
color: var(--accent-crimson) !important;
background: var(--crimson-bg, #fdebea) !important;
}
.mp-result .cell-more-menu button svg { width: 13px !important; height: 13px !important; }
/* 已采用角标 (保留 · 仅状态指示, hover overlay 仍可见) */
.mp-result .adopt-badge {
position: absolute; top: 8px; left: 8px;
background: var(--accent-forest, #42c366);
color: #fff;
font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;
letter-spacing: .04em;
padding: 2px 8px;
border-radius: var(--r-pill);
z-index: 3;
display: none;
}
.mp-result.adopted .adopt-badge { display: inline-block; }
/* 每个批次下方的批量操作 (胶囊按钮组 · 左对齐) */
.mp-pv-batch {
margin-top: 4px;
display: flex; gap: 10px; align-items: center; justify-content: flex-start;
}
.mp-pv-batch[hidden] { display: none; }
.mp-result-batch .mp-pv-batch.batch-foot { margin-top: 0; }
/* 预览区空态 (新任务且未生成) */
.mp-pv-empty {
flex: 1;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center;
padding: 40px 24px;
gap: 6px;
}
.mp-pv-empty[hidden] { display: none; }
.mp-pv-empty .mono {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .06em;
margin-bottom: 4px;
}
.mp-pv-empty .title {
font-size: 14px;
font-weight: 600;
color: var(--accent-black);
}
.mp-pv-empty .hint {
font-size: 12.5px;
color: var(--black-alpha-48);
line-height: 1.6;
max-width: 320px;
}
.mp-pv-empty .hint b { color: var(--heat); font-weight: 600; }
/* 预览区 hidden 时收起所有内容元素 */
#pv-summary[hidden], #pv-grid[hidden], #pv-foot[hidden] { display: none; }
.mp-pv-batch .summary {
margin-right: auto;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.mp-pv-batch .summary b { color: var(--heat); font-weight: 700; }
/* 底栏按钮 · 同 spec §4.18 + image-optimize .io-msg-ops button */
.mp-pv-batch .pill-btn {
height: 30px;
padding: 0 12px;
display: inline-flex; align-items: center; gap: 6px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
color: var(--accent-black);
font-family: inherit; font-size: 12.5px;
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.mp-pv-batch .pill-btn:hover {
border-color: var(--heat-20); color: var(--heat); background: var(--heat-12);
}
.mp-pv-batch .pill-btn svg { width: 13px; height: 13px; }
.mp-pv-batch .pill-btn.icon { width: 30px; padding: 0; justify-content: center; }
/* 批次更多气泡 (全部加入资产库 / 删除该批结果) */
.mp-pv-batch .batch-more-wrap { position: relative; display: inline-flex; }
.mp-pv-batch .batch-more-menu {
position: absolute; bottom: calc(100% + 6px); right: 0;
min-width: 168px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 6px 24px rgba(0,0,0,.10);
padding: 4px;
display: none;
z-index: 12;
}
.mp-pv-batch .batch-more-wrap.open .batch-more-menu { display: block; }
.mp-pv-batch .batch-more-menu button {
width: 100%;
display: inline-flex !important; align-items: center; gap: 8px;
height: auto !important;
padding: 7px 10px !important;
background: transparent !important;
border: 0 !important; border-radius: var(--r-sm) !important;
font-size: 12.5px;
color: var(--accent-black) !important;
text-align: left;
justify-content: flex-start !important;
cursor: pointer; font-family: inherit;
}
.mp-pv-batch .batch-more-menu button:hover {
background: var(--background-lighter) !important;
color: var(--heat) !important;
}
.mp-pv-batch .batch-more-menu button.danger:hover {
color: var(--accent-crimson) !important;
background: var(--crimson-bg, #fdebea) !important;
}
.mp-pv-batch .batch-more-menu button svg { width: 13px !important; height: 13px !important; flex-shrink: 0; }
.mp-pv-foot {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--border-faint);
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-56);
letter-spacing: .02em;
line-height: 1.6;
}
.mp-pv-foot a { color: var(--heat); }
@media (max-width: 1100px) {
.mp-layout { grid-template-columns: 1fr; }
}
/* ─── 模特库全屏弹窗 (无遮罩,自适应铺满) ─── */
.ml-modal-bg {
position: fixed; inset: 0;
background: var(--surface);
z-index: 999;
display: none;
}
.ml-modal-bg.show { display: flex; }
.ml-modal {
margin: 0;
flex: 1;
background: var(--surface);
border-radius: 0;
overflow: hidden;
display: flex; flex-direction: column;
}
.ml-modal-h {
display: flex; align-items: center;
padding: 14px 28px;
border-bottom: 1px solid var(--border-faint);
flex-shrink: 0;
}
.ml-modal-h h2 { font-size: 16px; font-weight: 600; }
.ml-modal-h .x {
margin-left: auto;
width: 32px; height: 32px;
display: grid; place-items: center;
background: transparent;
border: 0; border-radius: var(--r-sm);
cursor: pointer;
color: var(--black-alpha-56);
transition: background var(--t-base), color var(--t-base);
}
.ml-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }
.ml-modal-h .x svg { width: 16px; height: 16px; }
.ml-modal-body {
flex: 1; min-height: 0;
display: grid;
grid-template-columns: 200px 1fr;
}
.ml-side {
border-right: 1px solid var(--border-faint);
padding: 18px 0;
overflow-y: auto;
}
.ml-side .ml-side-h {
padding: 0 20px 8px;
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .06em;
}
.ml-side .ml-side-item {
display: flex; align-items: center; gap: 8px;
padding: 9px 20px;
cursor: pointer;
color: var(--black-alpha-72);
font-size: 13px;
border-left: 3px solid transparent;
transition: background var(--t-base), color var(--t-base);
}
.ml-side .ml-side-item:hover { background: var(--black-alpha-4); }
.ml-side .ml-side-item.active {
background: var(--heat-12);
color: var(--accent-black);
border-left-color: var(--heat);
font-weight: 600;
}
.ml-side .ml-side-item .ct {
margin-left: auto;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48);
}
.ml-main {
overflow-y: auto;
padding: 0;
display: flex; flex-direction: column;
position: relative; /* anchor for .ml-canvas overlay */
}
/* 添加模特 · 工作台画布 · 覆盖 .ml-main 的整个区域,自带展开/收起动效 */
.ml-canvas {
position: absolute; inset: 0;
z-index: 10;
background: var(--background-base);
display: flex; flex-direction: column;
opacity: 0; visibility: hidden;
transform: scale(.94);
transform-origin: 32px 80px; /* 从左上「添加模特」卡片的视觉中心展开 */
transition: opacity .28s ease, transform .32s cubic-bezier(.18,.72,.28,1), visibility .32s;
pointer-events: none;
}
.ml-canvas.show {
opacity: 1; visibility: visible;
transform: scale(1);
pointer-events: auto;
}
.ml-canvas-h {
display: flex; align-items: center; gap: 12px;
padding: 14px 28px;
border-bottom: 1px solid var(--border-faint);
flex-shrink: 0;
background: var(--surface);
}
.ml-canvas-h .back-btn {
display: inline-flex; align-items: center; gap: 4px;
height: 28px; padding: 0 10px;
background: transparent; border: 1px solid var(--border-faint);
border-radius: var(--r-sm); color: var(--black-alpha-72);
font-family: inherit; font-size: 12px; cursor: pointer;
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.ml-canvas-h .back-btn:hover { border-color: var(--heat-40); color: var(--heat); background: var(--heat-12); }
.ml-canvas-h .back-btn svg { width: 12px; height: 12px; }
.ml-canvas-h h3 { font-size: 15px; font-weight: 600; color: var(--accent-black); margin: 0; }
.ml-canvas-h .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
.ml-canvas-h .x {
width: 28px; height: 28px; display: grid; place-items: center;
background: transparent; border: none;
border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer;
transition: background var(--t-base), color var(--t-base);
}
.ml-canvas-h .x:hover { background: var(--background-lighter); color: var(--accent-black); }
.ml-canvas-h .x svg { width: 14px; height: 14px; }
/* 画布双栏:左 AI 生成(主视觉),右 本地上传(副视觉) */
.ml-canvas-body {
flex: 1; min-height: 0;
display: grid; grid-template-columns: 2fr 1fr;
overflow: hidden;
}
.mc-ai { position: relative; display: flex; flex-direction: column; min-height: 0; background: var(--background-base); }
.mc-up { position: relative; display: flex; flex-direction: column; min-height: 0; background: var(--surface); border-left: 1px solid var(--border-faint); }
/* ── 左:AI 生成 · 完全照搬 image-optimize .io-* 输入对话框样式 ── */
.mc-stream {
flex: 1; min-height: 0;
overflow-y: auto;
/* 左右 28px · 与 .ml-canvas-h(padding: 14px 28px)版心对齐 */
padding: 28px 28px 220px;
background: var(--background-base);
}
.mc-stream-inner {
/* 取消 max-width 限制 · 让结果网格在固定画布宽度下吃满可用空间,并与头部 28px 版心对齐 */
width: 100%; margin: 0 auto;
display: flex; flex-direction: column; gap: 28px;
}
/* 空态 · 同 .io-empty */
.mc-empty {
flex: 1; min-height: 100%;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 16px;
padding: 40px;
color: var(--black-alpha-56);
text-align: center;
}
.mc-empty .badge {
font-family: var(--font-mono); font-size: 11px;
letter-spacing: .08em; color: var(--black-alpha-48);
text-transform: uppercase;
}
.mc-empty h2 {
font-size: 22px; font-weight: 600;
color: var(--accent-black);
letter-spacing: -.015em;
margin: 0;
}
.mc-empty p { font-size: 13px; max-width: 460px; line-height: 1.6; margin: 0; }
.mc-empty .ic {
width: 64px; height: 64px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--heat);
}
.mc-empty .ic svg { width: 28px; height: 28px; }
.mc-empty .examples {
margin-top: 10px;
display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
max-width: 720px;
}
.mc-empty .examples .ex {
padding: 6px 12px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-pill);
font-size: 12px;
color: var(--black-alpha-72);
cursor: pointer;
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.mc-empty .examples .ex:hover { border-color: var(--heat-20); color: var(--heat); background: var(--heat-12); }
/* 单条对话气泡 · 同 .io-msg(去掉 padding-left,让消息贴齐外层 28px 版心) */
.mc-msg { display: flex; flex-direction: column; gap: 14px; }
.mc-msg-prompt { display: flex; align-items: flex-start; gap: 12px; }
.mc-msg-prompt .quote {
flex-shrink: 0; width: 28px; height: 28px;
border-radius: var(--r-sm);
background: var(--surface); border: 1px solid var(--border-faint);
color: var(--heat); display: grid; place-items: center;
font-family: var(--font-mono); font-size: 13px;
}
.mc-msg-prompt .quote svg { width: 13px; height: 13px; }
.mc-msg-prompt .pt { flex: 1; min-width: 0; padding-top: 4px; }
.mc-msg-prompt .pt-text {
font-size: 14px; color: var(--accent-black);
line-height: 1.55; word-break: break-word;
}
.mc-msg-prompt .pt-tags {
margin-top: 8px;
display: flex; flex-wrap: wrap; gap: 6px;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); letter-spacing: .02em;
align-items: center;
}
.mc-msg-prompt .pt-tags .meta-chip {
padding: 2px 8px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
}
.mc-msg-prompt .pt-tags .sep { color: var(--black-alpha-24); }
.mc-msg-grid {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
}
@media (max-width: 1280px) { .mc-msg-grid { grid-template-columns: repeat(3, 1fr); } }
.mc-cell {
position: relative;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
overflow: hidden;
aspect-ratio: 3/4;
}
.mc-cell .ph-frame {
position: absolute; inset: 0;
display: grid; place-items: center;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-32); letter-spacing: .02em;
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
}
.mc-cell.gen .ph-frame { animation: mc-pulse 1.4s ease-in-out infinite; }
@keyframes mc-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
.mc-cell:hover { border-color: var(--black-alpha-32); }
.mc-cell.err { border-color: var(--accent-crimson, #c43d3d); }
.mc-cell.err .ph-frame { color: var(--accent-crimson, #c43d3d); background: rgba(196, 61, 61, .05); }
/* 单图右上 hover 操作组 · 同 .io-cell .cell-ops */
.mc-cell .cell-ops {
position: absolute; top: 6px; right: 6px;
display: flex; gap: 4px;
opacity: 0;
transition: opacity var(--t-base);
z-index: 2;
}
.mc-cell:hover .cell-ops { opacity: 1; }
.mc-cell .cell-ops button {
width: 26px; height: 26px;
background: rgba(255, 255, 255, .92);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--accent-black);
cursor: pointer;
display: grid; place-items: center;
backdrop-filter: blur(4px);
transition: border-color var(--t-base), color var(--t-base);
}
.mc-cell .cell-ops button:hover { border-color: var(--heat); color: var(--heat); }
.mc-cell .cell-ops button svg { width: 12px; height: 12px; }
.mc-cell .cell-more-wrap { position: relative; }
.mc-cell .cell-more-menu {
position: absolute; top: calc(100% + 4px); right: 0;
min-width: 140px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 6px 24px rgba(0,0,0,.10);
padding: 4px;
display: none;
z-index: 12;
}
.mc-cell .cell-more-wrap.open .cell-more-menu { display: block; }
.mc-cell .cell-more-menu button {
width: 100%;
display: inline-flex !important; align-items: center; gap: 8px;
height: auto !important;
padding: 7px 10px !important;
background: transparent !important;
border: 0 !important; border-radius: var(--r-sm) !important;
font-size: 12.5px;
color: var(--accent-black) !important;
font-family: inherit;
text-align: left;
cursor: pointer;
backdrop-filter: none !important;
justify-content: flex-start !important;
}
.mc-cell .cell-more-menu button:hover { background: var(--background-lighter) !important; color: var(--heat) !important; }
.mc-cell .cell-more-menu button.danger:hover { color: var(--accent-crimson) !important; background: var(--crimson-bg, #fdebea) !important; }
.mc-cell .cell-more-menu button svg { width: 13px !important; height: 13px !important; flex-shrink: 0; }
/* 操作行 · 同 .io-msg-ops(去掉 padding-left,与版心对齐) */
.mc-msg-ops { display: flex; gap: 8px; }
.mc-msg-ops button {
display: inline-flex; align-items: center; gap: 6px;
height: 30px; padding: 0 12px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
font-size: 12.5px;
color: var(--accent-black);
font-family: inherit;
cursor: pointer;
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.mc-msg-ops button:hover { border-color: var(--heat-20); color: var(--heat); background: var(--heat-12); }
.mc-msg-ops button.icon { width: 30px; padding: 0; justify-content: center; }
.mc-msg-ops button svg { width: 13px; height: 13px; }
/* 批次更多气泡 · 同 .io-msg-ops .msg-more-wrap */
.mc-msg-ops .msg-more-wrap { position: relative; }
.mc-msg-ops .msg-more-menu {
position: absolute; bottom: calc(100% + 6px); left: 0;
min-width: 168px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 6px 24px rgba(0,0,0,.10);
padding: 4px;
display: none;
z-index: 12;
}
.mc-msg-ops .msg-more-wrap.open .msg-more-menu { display: block; }
.mc-msg-ops .msg-more-menu button {
width: 100%;
display: inline-flex !important; align-items: center; gap: 8px;
height: auto !important;
padding: 7px 10px !important;
background: transparent !important;
border: 0 !important; border-radius: var(--r-sm) !important;
font-size: 12.5px;
color: var(--accent-black) !important;
text-align: left;
justify-content: flex-start !important;
cursor: pointer; font-family: inherit;
}
.mc-msg-ops .msg-more-menu button:hover { background: var(--background-lighter) !important; color: var(--heat) !important; }
.mc-msg-ops .msg-more-menu button.danger:hover { color: var(--accent-crimson) !important; background: var(--crimson-bg, #fdebea) !important; }
.mc-msg-ops .msg-more-menu button svg { width: 13px !important; height: 13px !important; flex-shrink: 0; }
/* 底部输入栏 · 完全照搬 .io-input 各项尺寸 */
.mc-input-wrap {
position: absolute; left: 0; right: 0; bottom: 0;
padding: 14px 28px 22px;
background: linear-gradient(to bottom, transparent 0, var(--background-base) 24px);
z-index: 5;
}
.mc-input {
max-width: 720px; margin: 0 auto;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: 18px;
padding: 12px 14px 10px;
display: flex; flex-direction: column; gap: 8px;
box-shadow: 0 6px 24px rgba(0,0,0,.06);
transition: border-color var(--t-base);
}
.mc-input:focus-within { border-color: var(--heat-40); }
/* 上行 · 加号 + 参考图(同一 flex 行,均 64×64,refs 容器用 display:contents 让子项直接参与上行排列) */
.mc-input-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.mc-input-top .add-btn {
flex-shrink: 0; width: 64px; height: 64px;
background: var(--background-lighter);
border: 1px dashed var(--border-faint);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-56); cursor: pointer;
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.mc-input-top .add-btn:hover { border-color: var(--heat-40); color: var(--heat); background: var(--heat-12); }
.mc-input-top .add-btn svg { width: 22px; height: 22px; }
.mc-input-refs { display: contents; }
.mc-input-ref {
position: relative; width: 64px; height: 64px;
border-radius: var(--r-md); overflow: hidden;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
flex-shrink: 0;
}
.mc-input-ref img { width: 100%; height: 100%; object-fit: cover; }
.mc-input-ref .x {
position: absolute; top: 3px; right: 3px;
width: 18px; height: 18px;
background: rgba(0,0,0,.7); color: var(--accent-white);
border: 0; border-radius: 50%;
display: grid; place-items: center;
cursor: pointer;
}
.mc-input-ref .x svg { width: 10px; height: 10px; }
/* 中行 · textarea 满宽 · 直接作为 .mc-input 的子级,自成一行 */
.mc-input textarea#mc-input-text {
width: 100%;
border: 0; outline: 0; resize: none;
background: transparent;
font-family: inherit;
font-size: 14px;
line-height: 1.5;
color: var(--accent-black);
min-height: 44px; max-height: 220px;
padding: 4px 2px;
}
.mc-input textarea#mc-input-text::placeholder { color: var(--black-alpha-48); }
/* 下行 · 参数胶囊 + 右 meta + 发送按钮(32×32 放在最右,margin-left:8) */
.mc-input-bottom { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.mc-input-bottom .param {
position: relative; display: inline-flex; align-items: center; gap: 4px;
height: 26px; padding: 0 9px;
background: var(--background-lighter);
border: 1px solid transparent;
border-radius: var(--r-pill);
font-size: 11.5px; color: var(--black-alpha-72);
cursor: pointer;
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.mc-input-bottom .param:hover { background: var(--surface); border-color: var(--border-faint); }
.mc-input-bottom .param .lbl-mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-right: 1px; }
.mc-input-bottom .param svg { width: 10px; height: 10px; opacity: .6; }
.mc-input-bottom .right-meta { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.mc-input-bottom .right-meta .val { color: var(--accent-black); }
.mc-input .send-btn {
flex-shrink: 0; width: 32px; height: 32px;
background: var(--heat); color: var(--accent-white);
border: 0; border-radius: var(--r-md); cursor: pointer;
display: grid; place-items: center;
transition: opacity var(--t-base), filter var(--t-base);
margin-left: 8px;
}
.mc-input .send-btn:hover { filter: brightness(1.05); }
.mc-input .send-btn:disabled { opacity: .4; cursor: not-allowed; }
.mc-input .send-btn svg { width: 15px; height: 15px; }
/* ── 右:副视觉 · 顶部 tab 切换(AI 生成 / 本地上传)+ 3 模块(姓名/立绘/三视图) ── */
.mc-up-tabs { display: flex; border-bottom: 1px solid var(--border-faint); flex-shrink: 0; background: var(--surface); }
.mc-up-tab {
flex: 1; height: 44px;
background: transparent; border: 0;
border-bottom: 2px solid transparent;
font-family: inherit; font-size: 13px; font-weight: 500;
color: var(--black-alpha-56); cursor: pointer;
transition: color var(--t-base), border-color var(--t-base), background var(--t-base);
}
.mc-up-tab:hover { color: var(--accent-black); background: var(--background-lighter); }
.mc-up-tab.active { color: var(--heat); border-bottom-color: var(--heat); font-weight: 600; background: var(--surface); }
.mc-up-body { flex: 1; min-height: 0; padding: 18px 20px 14px; display: flex; flex-direction: column; gap: 18px; overflow-y: auto; }
.mc-up-section { display: flex; flex-direction: column; gap: 8px; }
.mc-up-sec-h { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
/* 模特姓名 输入 */
.mc-up-name {
width: 100%; height: 36px;
padding: 0 12px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
font-family: inherit; font-size: 13.5px; color: var(--accent-black);
outline: none;
transition: border-color var(--t-base), background var(--t-base);
}
.mc-up-name:focus { border-color: var(--heat-40); background: var(--surface); }
.mc-up-name::placeholder { color: var(--black-alpha-40); }
/* 立绘模块 · AI 选中态 */
.mc-portrait-ai .empty {
aspect-ratio: 3/4; max-height: 220px;
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
background: var(--background-lighter);
text-align: center; padding: 14px;
}
.mc-portrait-ai .empty[hidden] { display: none; }
.mc-portrait-ai .picked[hidden] { display: none; }
.mc-portrait-ai .empty .ic { width: 38px; height: 38px; border-radius: 50%; background: var(--surface); border: 1px solid var(--border-faint); display: grid; place-items: center; color: var(--black-alpha-48); }
.mc-portrait-ai .empty .ic svg { width: 16px; height: 16px; }
.mc-portrait-ai .empty .desc { font-size: 12.5px; color: var(--black-alpha-72); line-height: 1.55; }
.mc-portrait-ai .empty .mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
.mc-portrait-ai .picked {
position: relative; aspect-ratio: 3/4; max-height: 280px;
background: var(--background-lighter); border: 1.5px solid var(--heat);
border-radius: var(--r-md); overflow: hidden;
}
.mc-portrait-ai .picked .ph-frame {
position: absolute; inset: 0; display: grid; place-items: center;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-32); letter-spacing: .02em;
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
}
.mc-portrait-ai .picked .ops { position: absolute; top: 8px; right: 8px; display: flex; gap: 4px; }
.mc-portrait-ai .picked .ops button {
width: 26px; height: 26px;
background: rgba(255,255,255,.92); border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
display: grid; place-items: center;
color: var(--accent-black); cursor: pointer;
transition: border-color var(--t-base), color var(--t-base);
}
.mc-portrait-ai .picked .ops button:hover { border-color: var(--heat); color: var(--heat); }
.mc-portrait-ai .picked .ops button svg { width: 12px; height: 12px; }
.mc-portrait-ai .picked .badge {
position: absolute; top: 8px; left: 8px;
background: var(--heat); color: var(--accent-white);
padding: 2px 7px; border-radius: var(--r-sm);
font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .04em;
}
/* 立绘模块 · 本地上传(多张) */
.mc-portrait-local .drop {
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
padding: 20px 14px;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
cursor: pointer;
background: var(--background-lighter);
transition: border-color var(--t-base), background var(--t-base);
text-align: center;
}
.mc-portrait-local .drop:hover, .mc-portrait-local .drop.dragover { border-color: var(--heat); background: var(--heat-12); }
.mc-portrait-local .drop .ic {
width: 32px; height: 32px;
background: var(--heat); color: var(--accent-white);
border-radius: 50%; display: grid; place-items: center;
}
.mc-portrait-local .drop .ic svg { width: 14px; height: 14px; }
.mc-portrait-local .drop .t { font-size: 12.5px; font-weight: 600; color: var(--accent-black); }
.mc-portrait-local .drop .d { font-size: 11px; color: var(--black-alpha-48); }
.mc-portrait-local .list-h { display: flex; align-items: center; gap: 4px; margin-top: 6px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
.mc-portrait-local .list-h .ct { color: var(--accent-black); font-weight: 600; }
.mc-portrait-local .list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.mc-portrait-local .list:empty { display: none; }
.mc-portrait-local .thumb {
position: relative; aspect-ratio: 3/4;
border-radius: var(--r-sm); overflow: hidden;
background: var(--background-lighter); border: 1px solid var(--border-faint);
}
.mc-portrait-local .thumb img { width: 100%; height: 100%; object-fit: cover; }
.mc-portrait-local .thumb .x {
position: absolute; top: 4px; right: 4px;
width: 20px; height: 20px;
background: rgba(0,0,0,.7); color: var(--accent-white);
border: 0; border-radius: 50%;
display: grid; place-items: center; cursor: pointer;
}
.mc-portrait-local .thumb .x svg { width: 10px; height: 10px; }
/* 三视图模块 · 始终展示 16:9 占位 · 空态时生成按钮居中覆盖 · 有版本时按钮消失,显示重跑 + 历史 */
.mc-triview .result-wrap { display: flex; flex-direction: column; gap: 8px; }
.mc-triview .result {
position: relative; aspect-ratio: 16/9;
background: var(--background-lighter); border: 1.5px solid var(--border-faint);
border-radius: var(--r-md); overflow: hidden;
transition: border-color var(--t-base);
}
.mc-triview.has-result .result { border-color: var(--heat); }
.mc-triview .result .ph-frame {
position: absolute; inset: 0; display: grid; place-items: center;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-32); letter-spacing: .02em;
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
pointer-events: none;
}
.mc-triview .result.gen .ph-frame { animation: mc-pulse 1.4s ease-in-out infinite; }
/* 居中覆盖生成按钮(无版本 / 非生成中时显示) */
.mc-triview .overlay-gen-btn {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
display: inline-flex; align-items: center; gap: 6px;
height: 36px; padding: 0 18px;
background: var(--heat); color: var(--accent-white);
border: 0; border-radius: var(--r-pill);
font-family: inherit; font-size: 13px; font-weight: 500;
cursor: pointer;
box-shadow: 0 4px 12px rgba(250, 93, 25, .28);
transition: filter var(--t-base), opacity var(--t-base), transform var(--t-base), box-shadow var(--t-base);
}
.mc-triview .overlay-gen-btn:hover:not(:disabled) {
filter: brightness(1.06);
transform: translate(-50%, -50%) scale(1.03);
box-shadow: 0 6px 16px rgba(250, 93, 25, .36);
}
.mc-triview .overlay-gen-btn:disabled {
background: var(--black-alpha-24); color: var(--surface);
cursor: not-allowed; box-shadow: none;
}
.mc-triview .overlay-gen-btn svg { width: 13px; height: 13px; }
.mc-triview .overlay-hint {
position: absolute; left: 0; right: 0; bottom: 10px;
text-align: center;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .04em;
pointer-events: none;
z-index: 2;
}
.mc-triview .result-ops { display: flex; gap: 6px; align-items: center; }
.mc-triview .result-ops .cost { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.mc-triview .result-ops button {
display: inline-flex; align-items: center; gap: 5px;
height: 28px; padding: 0 10px;
background: var(--surface); border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-family: inherit; font-size: 11.5px;
color: var(--accent-black); cursor: pointer;
transition: border-color var(--t-base), color var(--t-base);
}
.mc-triview .result-ops button:hover { border-color: var(--heat); color: var(--heat); }
.mc-triview .result-ops button svg { width: 11px; height: 11px; }
/* 历史版本 strip · 同商品详情页 popover 视觉 */
.mc-triview .history { display: flex; flex-direction: column; gap: 6px; }
.mc-triview .history .h-lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
.mc-triview .history .h-lbl .ct { color: var(--accent-black); font-weight: 600; }
.mc-triview .history .h-row { display: flex; gap: 6px; overflow-x: auto; padding: 2px; }
.mc-triview .history .h-thumb {
flex: 0 0 auto;
width: 72px; aspect-ratio: 16/9;
background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm);
position: relative; cursor: pointer;
display: grid; place-items: center; overflow: hidden;
transition: border-color var(--t-base);
}
.mc-triview .history .h-thumb:hover { border-color: var(--heat-40); }
.mc-triview .history .h-thumb.active { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }
.mc-triview .history .h-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }
.mc-triview .history .h-thumb.active .v { color: var(--heat); font-weight: 600; }
.mc-triview .history .h-thumb .badge { position: absolute; top: 2px; left: 2px; font-family: var(--font-mono); font-size: 8.5px; padding: 0 4px; line-height: 12px; background: var(--heat); color: var(--accent-white); border-radius: 2px; letter-spacing: .02em; display: none; }
.mc-triview .history .h-thumb.active .badge { display: block; }
/* 底部提交栏 */
.mc-up-foot { padding: 12px 20px 16px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 10px; flex-shrink: 0; flex-wrap: wrap; }
.mc-up-foot .stat { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mc-up-foot .stat b { color: var(--accent-black); font-weight: 600; }
.mc-up-foot .stat.ok { color: var(--heat); }
.mc-up-foot .commit-btn {
display: inline-flex; align-items: center; gap: 5px;
height: 32px; padding: 0 14px;
background: var(--heat); color: var(--accent-white);
border: 0; border-radius: var(--r-sm);
font-family: inherit; font-size: 12.5px; cursor: pointer;
transition: filter var(--t-base), opacity var(--t-base);
}
.mc-up-foot .commit-btn:hover { filter: brightness(1.05); }
.mc-up-foot .commit-btn:disabled { opacity: .4; cursor: not-allowed; filter: none; }
.mc-up-foot .commit-btn svg { width: 12px; height: 12px; }
/* 左侧结果卡 · 可点击选为立绘(仅 AI tab) */
.mc-cell { cursor: pointer; }
.mc-cell.selected { border-color: var(--heat); box-shadow: 0 0 0 2px var(--heat-12); }
.mc-cell .pick-badge { position: absolute; top: 6px; left: 6px; background: var(--heat); color: var(--accent-white); padding: 2px 7px; border-radius: var(--r-sm); font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .04em; display: none; }
.mc-cell.selected .pick-badge { display: block; }
.ml-toolbar {
padding: 14px 28px;
border-bottom: 1px solid var(--border-faint);
display: flex; align-items: center; gap: 18px;
flex-shrink: 0;
flex-wrap: wrap;
}
.ml-toolbar .btn-up {
height: 32px;
padding: 0 14px;
display: inline-flex; align-items: center; gap: 6px;
background: var(--surface);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-sm);
color: var(--accent-black);
font-family: inherit;
font-size: 12.5px;
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base);
}
.ml-toolbar .btn-up:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }
.ml-toolbar .btn-up svg { width: 14px; height: 14px; }
.ml-toolbar .chip-group {
display: inline-flex; align-items: center; gap: 6px;
}
.ml-toolbar .chip-group .lbl {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .04em;
margin-right: 4px;
}
.ml-toolbar .chip {
height: 26px;
padding: 0 12px;
border-radius: 999px;
background: transparent;
border: 1px solid var(--black-alpha-12);
color: var(--black-alpha-72);
font-size: 12px;
cursor: pointer;
font-family: inherit;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
}
.ml-toolbar .chip:hover { color: var(--accent-black); }
.ml-toolbar .chip.active {
background: var(--heat-12); color: var(--heat);
border-color: var(--heat-40); font-weight: 600;
}
.ml-scroll {
flex: 1; min-height: 0;
overflow-y: auto;
padding: 20px 28px 28px;
}
.ml-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
gap: 12px;
}
.ml-grid .model-card { padding: 10px; }
/* 「添加模特」入口卡:虚线轮廓 + 居中 + 号,与现有卡片同尺寸 */
.ml-grid .ml-upload-card {
border: 1.5px dashed var(--black-alpha-24);
background: var(--surface);
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
}
.ml-grid .ml-upload-card:hover {
border-color: var(--heat);
background: var(--heat-12);
}
.ml-grid .ml-upload-card:focus-visible {
outline: 2px solid var(--heat);
outline-offset: 2px;
}
.ml-grid .ml-upload-card .up-thumb {
aspect-ratio: 3/4;
border-radius: var(--r-sm);
background: transparent;
display: grid; place-items: center;
}
.ml-grid .ml-upload-card .up-plus {
width: 44px; height: 44px;
border-radius: 50%;
background: var(--surface);
border: 1px solid var(--black-alpha-12);
color: var(--black-alpha-56);
display: grid; place-items: center;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base), transform var(--t-base);
}
.ml-grid .ml-upload-card:hover .up-plus {
background: var(--heat); border-color: var(--heat); color: var(--accent-white);
transform: scale(1.06);
}
.ml-grid .ml-upload-card .up-plus svg { width: 22px; height: 22px; }
.ml-grid .ml-upload-card .m-name { color: var(--accent-black); }
.ml-grid .ml-upload-card:hover .m-name { color: var(--heat); }
.ml-grid .ml-upload-card .m-tag { color: var(--black-alpha-48); }
/* ─── 添加模特 · 选择 modal (AI 生成 / 本地上传) ─── */
.ml-up-choice-bg {
position: fixed; inset: 0; z-index: 1200;
background: rgba(21, 20, 15, .42);
display: none; place-items: center; padding: 16px;
}
.ml-up-choice-bg.show { display: grid; }
.ml-up-choice {
width: min(560px, 92vw);
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 16px 48px rgba(21, 20, 15, .18);
overflow: hidden;
position: relative;
}
.ml-up-choice .uc-h {
display: flex; align-items: center; gap: 12px;
padding: 18px 22px 14px;
border-bottom: 1px solid var(--border-faint);
}
.ml-up-choice .uc-h .ic-m {
width: 36px; height: 36px;
border-radius: var(--r-md);
background: var(--heat-12); color: var(--heat);
display: grid; place-items: center;
flex-shrink: 0;
}
.ml-up-choice .uc-h .ic-m svg { width: 18px; height: 18px; }
.ml-up-choice .uc-h .ti { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.ml-up-choice .uc-h .ti strong { font-size: 15px; color: var(--accent-black); font-weight: 600; }
.ml-up-choice .uc-h .ti .mono { font-family: var(--font-mono); font-size: 11.5px; color: var(--black-alpha-48); letter-spacing: .02em; }
.ml-up-choice .uc-h .uc-x {
margin-left: auto;
width: 28px; height: 28px;
background: transparent; border: 0; border-radius: var(--r-sm);
color: var(--black-alpha-56); cursor: pointer;
display: grid; place-items: center;
}
.ml-up-choice .uc-h .uc-x:hover { background: var(--background-lighter); color: var(--accent-black); }
.ml-up-choice .uc-h .uc-x svg { width: 14px; height: 14px; }
.ml-up-choice .uc-body {
display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
padding: 20px 22px 22px;
}
.ml-up-choice .uc-option {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 18px 16px;
text-align: left;
cursor: pointer;
font-family: inherit;
display: flex; flex-direction: column; gap: 10px;
transition: border-color var(--t-base), background var(--t-base), transform var(--t-base);
}
.ml-up-choice .uc-option:hover {
border-color: var(--heat);
background: var(--heat-12);
}
.ml-up-choice .uc-option .opt-ic {
width: 40px; height: 40px; border-radius: var(--r-md);
background: var(--background-lighter);
color: var(--heat); border: 1px solid var(--heat-20);
display: grid; place-items: center;
transition: background var(--t-base), color var(--t-base);
}
.ml-up-choice .uc-option:hover .opt-ic { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }
.ml-up-choice .uc-option .opt-ic svg { width: 18px; height: 18px; }
.ml-up-choice .uc-option .opt-t { font-size: 14px; font-weight: 600; color: var(--accent-black); }
.ml-up-choice .uc-option .opt-d {
font-family: var(--font-mono); font-size: 11.5px;
color: var(--black-alpha-56); letter-spacing: .02em; line-height: 1.55;
}
.ml-up-choice .uc-option .opt-tag {
margin-top: auto;
align-self: flex-start;
font-family: var(--font-mono); font-size: 10.5px;
padding: 2px 8px; border-radius: var(--r-sm);
background: var(--background-lighter);
color: var(--black-alpha-72);
letter-spacing: .04em;
}
.ml-up-choice .uc-option:hover .opt-tag { background: var(--surface); color: var(--heat); }
/* 模特详情 居中弹窗 — 参考布局 v2 (与 pipeline.html / library.html 一致) */
.md-modal-bg { position: fixed; inset: 0; background: rgba(21,20,15,.42); backdrop-filter: blur(8px); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 40px; }
.md-modal-bg.show { display: flex; }
.md-modal { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: min(1040px, 100%); max-height: calc(100vh - 80px); overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 16px 48px rgba(0,0,0,.18); }
.md-modal-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }
.md-modal-h h3 { font-size: 15px; font-weight: 600; }
.md-modal-h .ad-tag { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
.md-modal-h .x { margin-left: auto; width: 30px; height: 30px; display: grid; place-items: center; background: transparent; border: 0; cursor: pointer; color: var(--black-alpha-56); border-radius: var(--r-sm); }
.md-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }
.md-modal-h .x svg { width: 14px; height: 14px; }
.md-modal-body { padding: 20px 24px 24px; overflow-y: auto; flex: 1; }
.md-detail-grid { display: grid; grid-template-columns: 340px 1fr; gap: 24px; }
/* 左栏 · 大立绘 + 缩略图 */
.md-lead { display: flex; flex-direction: column; gap: 10px; }
.md-lead-wrap { position: relative; }
.md-lead-img { aspect-ratio: 3/4; border-radius: var(--r-md); background: var(--background-lighter); border: 1px solid var(--border-faint); position: relative; overflow: hidden; }
.md-lead-img .ph-frame { position: absolute; left: 50%; bottom: 12px; transform: translateX(-50%); background: rgba(255,255,255,.85); font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); padding: 2px 10px; border-radius: var(--r-sm); }
.md-lead-img .ph-name { position: absolute; left: 0; top: 0; padding: 12px 14px; font-size: 14px; font-weight: 600; color: var(--black-alpha-72); background: linear-gradient(180deg, rgba(255,255,255,.92), rgba(255,255,255,0)); width: 100%; }
/* 查看大图 icon · 悬浮容器才显示 · 32×32 icon-only */
.md-zoom-btn { position: absolute; right: 8px; bottom: 8px; width: 32px; height: 32px; padding: 0; background: rgba(21,20,15,.7); color: #fff; border: 0; border-radius: var(--r-sm); display: grid; place-items: center; cursor: pointer; opacity: 0; transition: opacity var(--t-base), background var(--t-base); z-index: 3; }
.md-zoom-btn:hover { background: rgba(21,20,15,.92); }
.md-zoom-btn svg { width: 14px; height: 14px; }
.md-lead-wrap:hover .md-zoom-btn,
.md-views .md-view:hover .md-zoom-btn { opacity: 1; }
.md-views .md-view { position: relative; }
.md-thumbs { display: flex; gap: 8px; }
.md-thumbs .thumb { flex: 0 0 64px; aspect-ratio: 3/4; border-radius: var(--r-sm); border: 1px solid var(--border-faint); background: var(--background-lighter); cursor: pointer; overflow: hidden; position: relative; }
.md-thumbs .thumb:hover { border-color: var(--heat-40); }
.md-thumbs .thumb.active { border-color: var(--heat); border-width: 2px; }
/* 右栏 sections */
.md-right .md-section + .md-section { margin-top: 18px; }
.md-section-h { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--accent-black); margin-bottom: 10px; }
.md-section-h .ic { width: 14px; height: 14px; color: var(--heat); display: grid; place-items: center; }
.md-section-h .ic svg { width: 14px; height: 14px; }
.md-section-h .ratio-chip { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; padding: 2px 8px; border-radius: var(--r-sm); background: var(--background-lighter); border: 1px solid var(--border-faint); color: var(--black-alpha-56); }
.md-section-h .icon-btn { width: 28px; height: 28px; display: grid; place-items: center; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer; }
.md-section-h .icon-btn:hover { color: var(--heat); border-color: var(--heat-40); }
.md-section-h .icon-btn svg { width: 12px; height: 12px; }
/* 三视图 — 始终单张 16:9 大图 */
.md-views { display: flex; flex-direction: column; gap: 8px; }
.md-views .md-view { aspect-ratio: 16/9; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-md); position: relative; display: grid; place-items: end center; overflow: hidden; }
.md-views .md-view .lbl { position: absolute; bottom: 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); background: rgba(255,255,255,.85); padding: 2px 8px; border-radius: var(--r-sm); }
/* 三视图 · 用户上传:历史版本横向 strip · 与 mc-triview history 视觉对齐 */
.md-view-versions { display: flex; gap: 6px; overflow-x: auto; padding: 2px; }
.md-view-versions .v-thumb { flex: 0 0 auto; width: 72px; aspect-ratio: 16/9; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); position: relative; cursor: pointer; display: grid; place-items: center; overflow: hidden; transition: border-color var(--t-base); }
.md-view-versions .v-thumb:hover { border-color: var(--heat-40); }
.md-view-versions .v-thumb.active { border-color: var(--heat); border-width: 2px; box-shadow: 0 0 0 2px var(--heat-12); }
.md-view-versions .v-thumb .v { font-family: var(--font-mono); font-size: 10px; color: var(--black-alpha-56); letter-spacing: .02em; }
.md-view-versions .v-thumb.active .v { color: var(--heat); font-weight: 600; }
/* 立绘大图 · 支持 <img> · cover */
.md-lead-img img.md-lead-pic { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
/* 缩略图 · 支持 <img> · 多张时允许换行(避免横向爆款) */
.md-thumbs { flex-wrap: wrap; }
.md-thumbs .thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.md-thumbs .thumb .ph-frame { position: absolute; inset: 0; display: grid; place-items: center; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px); }
/* 简介 */
.md-intro { font-size: 13px; line-height: 1.65; color: var(--black-alpha-72); margin: 0 0 12px; }
.md-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.md-tags .tag-chip { height: 26px; padding: 0 12px; display: inline-flex; align-items: center; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); font-size: 12px; color: var(--accent-black); }
.md-tags .tag-add { width: 26px; height: 26px; display: grid; place-items: center; background: var(--background-lighter); border: 1px dashed var(--black-alpha-24); border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer; }
.md-tags .tag-add:hover { border-color: var(--heat); color: var(--heat); }
.md-tags .tag-add svg { width: 12px; height: 12px; }
/* 属性表 */
.md-props { margin-top: 18px; display: grid; grid-template-columns: repeat(3, 1fr); column-gap: 24px; border-top: 1px solid var(--border-faint); padding-top: 16px; }
.md-props .prop { display: flex; align-items: baseline; padding: 10px 0; border-bottom: 1px solid var(--border-faint); font-size: 12.5px; min-height: 38px; }
.md-props .prop:nth-last-child(-n+3) { border-bottom: 0; }
.md-props .prop .k { flex: 0 0 64px; color: var(--black-alpha-56); font-family: var(--font-mono); font-size: 11px; }
.md-props .prop .v { color: var(--accent-black); font-weight: 500; word-break: break-all; }
/* footer */
.md-modal-f { padding: 14px 20px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 8px; }
.md-modal-f .foot-stats { display: flex; gap: 6px; margin-right: auto; }
.md-modal-f .stat-btn { height: 32px; padding: 0 12px; display: inline-flex; align-items: center; gap: 6px; background: transparent; border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-72); font-size: 12.5px; font-family: inherit; cursor: pointer; }
.md-modal-f .stat-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--black-alpha-24); }
.md-modal-f .stat-btn svg { width: 13px; height: 13px; }
.md-modal-f .stat-btn b { color: var(--accent-black); font-weight: 600; }
/* ─── 离开工作台 · 二次确认弹窗 ─── */
.mc-leave-bg {
position: fixed; inset: 0;
background: rgba(21,20,15,.42);
backdrop-filter: blur(8px);
z-index: 1200;
display: none;
align-items: center; justify-content: center;
padding: 40px;
}
.mc-leave-bg.show { display: flex; }
.mc-leave {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
width: 420px; max-width: 100%;
box-shadow: 0 16px 48px rgba(0,0,0,.18);
overflow: hidden;
position: relative;
}
.mc-leave .lv-h {
display: flex; align-items: center; gap: 10px;
padding: 14px 20px 10px;
}
.mc-leave .lv-h .ic {
width: 28px; height: 28px;
display: grid; place-items: center;
border-radius: var(--r-sm);
background: var(--crimson-bg);
color: var(--accent-crimson);
flex-shrink: 0;
}
.mc-leave .lv-h .ic svg { width: 16px; height: 16px; }
.mc-leave .lv-h h3 { font-size: 15px; font-weight: 600; color: var(--accent-black); margin: 0; }
.mc-leave .lv-h .mono {
margin-left: auto;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .04em;
}
.mc-leave .lv-b {
padding: 4px 20px 18px;
font-size: 13px; line-height: 1.65;
color: var(--black-alpha-72);
}
.mc-leave .lv-b b { color: var(--accent-black); font-weight: 600; }
.mc-leave .lv-f {
display: flex; align-items: center; gap: 8px;
padding: 12px 20px;
border-top: 1px solid var(--border-faint);
background: var(--background-lighter);
}
.mc-leave .lv-f .spacer { flex: 1; }
.mc-leave .lv-f .btn { height: 34px; padding: 0 14px; font-size: 13px; }
.mc-leave .btn-danger {
background: var(--accent-crimson);
color: var(--accent-white);
border-color: var(--accent-crimson);
font-weight: 600;
}
.mc-leave .btn-danger:hover {
background: var(--accent-crimson);
border-color: var(--accent-crimson);
filter: brightness(.95);
}
/* 编辑商品 drawer (复用 .drawer 基础样式, 提高 z-index 覆盖商品库) */
.pc-drawer { width: 720px; max-width: 100vw; z-index: 1101; }
.pc-drawer .drawer-b { padding: 24px 28px; }
#pc-drawer-bg.drawer-bg { z-index: 1100; }
.pc-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 18px; }
.pc-field-label { font-size: 13px; font-weight: 500; color: var(--accent-black); }
.pc-field-label .req { color: var(--accent-crimson); margin-left: 2px; }
.pc-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 18px; }
.pc-field-row > div { display: flex; flex-direction: column; gap: 6px; }
.pc-bullets {
list-style: none; padding: 0; margin: 0;
display: flex; flex-direction: column; gap: 6px;
}
.pc-bullets li {
display: flex; align-items: center; gap: 8px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
padding: 0 10px;
height: 36px;
}
.pc-bullets li.add { border-style: dashed; border-color: var(--heat-40); }
.pc-bullets li .num {
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); width: 18px; text-align: center;
flex-shrink: 0;
}
.pc-bullets li.add .num { color: var(--heat); }
.pc-bullets li input {
flex: 1; border: 0; background: transparent; outline: none;
font-size: 13px; color: var(--accent-black);
font-family: inherit;
}
.pc-bullets li input::placeholder { color: var(--black-alpha-48); }
.pc-bullets li .rm {
width: 22px; height: 22px;
background: transparent; border: 0; border-radius: var(--r-sm);
color: var(--black-alpha-48); cursor: pointer;
display: grid; place-items: center;
}
.pc-bullets li .rm:hover { color: var(--accent-crimson); background: var(--black-alpha-4); }
.pc-bullets li .rm svg { width: 11px; height: 11px; }
/* 商品图片 grid (对齐 product-detail .ov-images-grid) */
.pc-imgs {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
}
.pc-imgs .thumb {
aspect-ratio: 1 / 1;
border-radius: var(--r-sm);
background: var(--background-lighter);
border: 1px solid var(--border-faint);
position: relative;
overflow: hidden;
}
.pc-imgs .thumb .ph-frame {
position: absolute; inset: 0;
display: grid; place-items: center;
font-family: var(--font-mono); font-size: 10px;
color: var(--black-alpha-32);
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
}
.pc-imgs .thumb .rm {
position: absolute; top: 4px; right: 4px;
width: 18px; height: 18px;
background: rgba(0,0,0,.5);
color: #fff; border: 0;
border-radius: var(--r-sm);
display: grid; place-items: center;
cursor: pointer;
opacity: 0;
transition: opacity var(--t-base);
}
.pc-imgs .thumb:hover .rm { opacity: 1; }
.pc-imgs .thumb .rm svg { width: 10px; height: 10px; }
.pc-imgs .img-upload {
aspect-ratio: 1 / 1;
border-radius: var(--r-sm);
background: var(--heat-12);
border: 1.5px dashed var(--heat-40);
display: grid; place-items: center;
color: var(--heat);
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base);
}
.pc-imgs .img-upload:hover { background: var(--heat-20); border-color: var(--heat); }
.pc-imgs .img-upload svg { width: 18px; height: 18px; }
</style>
</head>
<body>
<div id="page">
<div class="mp-layout">
<!-- ===== 最左栏 · 商品空间 (单选 · 当前商品决定结果区批次) ===== -->
<aside class="mp-prod-space" id="prod-space">
<!-- 顶部 · 返回 + 折叠 (跟图片创作风格一致) -->
<div class="mp-side-top">
<button class="back-pill" type="button" onclick="history.length > 1 ? history.back() : location.href='asset-factory.html'" title="返回">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
<span>返回</span>
</button>
<button class="fold" type="button" title="折叠侧栏" style="margin-left:auto">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/></svg>
</button>
</div>
<div class="mp-ps-h">
<div class="mp-ps-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" id="ps-search-input" placeholder="搜索商品 / 分类">
</div>
</div>
<!-- 商品列表 标题行 · 右上显眼新建按钮 -->
<div class="mp-list-h">
<span class="mono">// 商品空间</span>
<button class="new-prod" type="button" id="ps-new-btn" title="新建商品">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
<span>新建商品</span>
</button>
</div>
<div class="mp-ps-list" id="ps-list"></div>
<button class="mp-ps-all" type="button" id="ps-all-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M9 4v16"/></svg>
<span>全部商品</span>
<span class="ct" id="ps-all-ct">0 个</span>
<svg style="margin-left:0" 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>
</button>
</aside>
<!-- ===== 主区 · 头部 + 参数/结果 双栏 ===== -->
<section class="mp-main">
<!-- 主区顶部 · toolbar (商品标题 + 搜索 + 筛选 · 跟图片创作一致) -->
<div class="mp-main-h">
<div class="cur-title">
<span class="crumb">// 商品空间</span>
<span class="nm placeholder" id="cur-prod-nm">未选择 · 请在左侧商品空间选一个</span>
</div>
<span class="spacer"></span>
<div class="tb-search-wrap" id="mp-search-wrap">
<button class="search-btn" type="button" title="搜索批次/模特" id="mp-search-toggle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
</button>
<input type="text" class="tb-search-input" id="mp-search-input" placeholder="搜索批次/模特/比例…" autocomplete="off">
</div>
<div class="tb-menu-wrap" data-filter="time">
<button class="tb-chip" type="button" id="mp-chip-time">
<span class="lbl">时间</span>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="tb-menu" id="mp-menu-time" role="listbox" aria-labelledby="mp-chip-time">
<button class="tb-menu-item active" type="button" data-val="all">全部时间</button>
<button class="tb-menu-item" type="button" data-val="today">今天</button>
<button class="tb-menu-item" type="button" data-val="1h">1 小时内</button>
<button class="tb-menu-item" type="button" data-val="10min">10 分钟内</button>
</div>
</div>
<div class="tb-menu-wrap" data-filter="model">
<button class="tb-chip" type="button" id="mp-chip-model">
<span class="lbl">模特</span>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="tb-menu" id="mp-menu-model" role="listbox" aria-labelledby="mp-chip-model">
<button class="tb-menu-item active" type="button" data-val="all">全部模特</button>
<div class="tb-menu-empty">暂无批次,生成后可按模特筛选</div>
</div>
</div>
</div>
<div class="mp-main-body">
<!-- 左 · 参数 -->
<div class="mp-form">
<!-- ① 选择模特 -->
<div class="mp-step">
<div class="mp-step-h">
<span class="num">1</span>
<span class="title">选择模特</span>
<span class="right" id="open-model-lib">全部模特 →</span>
</div>
<div class="model-grid" id="model-grid-mini">
<div class="model-card" data-id="m1" data-name="Ava">
<div class="m-check"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></div>
<div class="placeholder m-thumb"><span class="ph-frame">Ava</span></div>
<div class="m-name">Ava</div>
<div class="m-tag">亚洲·25岁·清新</div>
</div>
<div class="model-card" data-id="m2" data-name="Luna">
<div class="m-check"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></div>
<div class="placeholder m-thumb"><span class="ph-frame">Luna</span></div>
<div class="m-name">Luna</div>
<div class="m-tag">亚洲·22岁·学生</div>
</div>
<div class="model-card" data-id="m3" data-name="Mia">
<div class="m-check"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></div>
<div class="placeholder m-thumb"><span class="ph-frame">Mia</span></div>
<div class="m-name">Mia</div>
<div class="m-tag">混血·28岁·OL</div>
</div>
<div class="model-card" data-id="m4" data-name="Zoe">
<div class="m-check"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></div>
<div class="placeholder m-thumb"><span class="ph-frame">Zoe</span></div>
<div class="m-name">Zoe</div>
<div class="m-tag">亚洲·30岁·健身</div>
</div>
</div>
</div>
<!-- ② 生成设置 -->
<div class="mp-step">
<div class="mp-step-h">
<span class="num">2</span>
<span class="title">生成设置</span>
</div>
<div class="mp-sub">
<div class="mp-sub-h">// 生成数量 (每模特)</div>
<div class="pill-row" data-key="count">
<button type="button" class="opt active" data-val="4">4 张</button>
<button type="button" class="opt" data-val="8">8 张</button>
<button type="button" class="opt" data-val="12">12 张</button>
</div>
</div>
<div class="mp-sub">
<div class="mp-sub-h">// 图片比例</div>
<div class="pill-row" data-key="ratio">
<button type="button" class="opt active" data-val="1:1">1:1</button>
<button type="button" class="opt" data-val="3:4">3:4</button>
<button type="button" class="opt" data-val="9:16">9:16</button>
</div>
</div>
</div>
<!-- 底部 立即生成 -->
<div class="mp-cta">
<button class="btn btn-primary btn-gen" id="mp-go-btn" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 3l14 9-14 9V3z"/></svg>
立即生成 (预估 <span id="cost-total">¥1.20</span>)
</button>
<div class="mp-cta-hint">// 采用即扣费并入对应商品 AI 素材 · 未采用不扣</div>
</div>
</div>
<!-- ===== 右栏 · 预览 ===== -->
<div class="mp-preview">
<!-- 空态(新任务态 & 还没立即生成时显示) -->
<div class="mp-pv-empty" id="pv-empty">
<div class="mono">// EMPTY STATE</div>
<div class="title">还没有生成结果</div>
<div class="hint">先选商品、选模特,点击 <b>立即生成</b> 后,效果图会出现在这里</div>
</div>
<!-- pv-summary 已并入主区头部 toolbar,仅保留隐藏 DOM 让旧 JS 引用不报错 -->
<div class="mp-pv-h" id="pv-summary" hidden style="display:none">
<div class="pv-meta"><b id="pv-count">4</b> 张 · <b id="pv-ratio">1:1</b></div>
<div class="pv-line"><span class="v" id="pv-prod">未选择</span></div>
<div class="pv-line"><span class="v" id="pv-model">未选择</span><span class="swap" id="pv-swap">更换</span></div>
</div>
<div class="mp-pv-grid" id="pv-grid">
<!-- 默认占位批次; 生成后填充为真实批次 -->
<div class="mp-result-batch placeholder-batch">
<div class="mp-result-grid">
<div class="mp-result placeholder-only"><div class="mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div></div>
<div class="mp-result placeholder-only"><div class="mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div></div>
<div class="mp-result placeholder-only"><div class="mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div></div>
<div class="mp-result placeholder-only"><div class="mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div></div>
</div>
</div>
</div>
<div class="mp-pv-foot" id="pv-foot">
// 采用即扣费并入对应商品的 <a href="products.html">AI 素材库 →</a>;未采用的图不扣费、不保存
<br>// 切换左侧商品空间 · 查看其他商品的批次记录
</div>
</div>
</div><!-- /.mp-main-body -->
</section><!-- /.mp-main -->
</div>
</div>
<!-- ===== 商品库 全屏(无遮罩自适应,多选) ===== -->
<div class="pl-modal-bg" id="pl-modal-bg">
<div class="pl-modal">
<div class="pl-modal-h">
<h2>商品库</h2>
<span class="ct" id="pl-total-ct">// 共 7 个商品</span>
<div class="actions">
<button class="x" type="button" id="pl-close-btn" aria-label="关闭" style="width:32px;height:32px;display:grid;place-items:center;background:transparent;border:0;border-radius:var(--r-sm);cursor:pointer;color:var(--black-alpha-56)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg>
</button>
</div>
</div>
<div class="pl-modal-body">
<aside class="pl-side">
<div class="pl-side-h">分类</div>
<div class="pl-side-item active" data-cat="">全部 <span class="ct" id="pl-ct-all">7</span></div>
<div class="pl-side-item" data-cat="美妆个护">美妆个护 <span class="ct">2</span></div>
<div class="pl-side-item" data-cat="数码 3C">数码 3C <span class="ct">1</span></div>
<div class="pl-side-item" data-cat="食品饮料">食品饮料 <span class="ct">2</span></div>
<div class="pl-side-item" data-cat="家居家电">家居家电 <span class="ct">1</span></div>
<div class="pl-side-item" data-cat="运动户外">运动户外 <span class="ct">1</span></div>
</aside>
<div class="pl-main">
<div class="pl-toolbar">
<div class="search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" id="pl-search-input" placeholder="搜索商品名">
</div>
<button class="btn-new" type="button" id="pl-new-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
新建商品
</button>
</div>
<div class="pl-scroll">
<div class="pl-grid" id="pl-grid">
<!-- JS 渲染 -->
</div>
</div>
</div>
</div>
<div class="pl-modal-f">
<div class="summary">// 已选 <b id="pl-sel-ct">0</b> 个商品</div>
<button class="btn" type="button" id="pl-cancel-btn">取消</button>
<button class="btn btn-primary" type="button" id="pl-confirm-btn">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12l5 5L20 7"/></svg>
确认选择
</button>
</div>
</div>
</div>
<!-- ===== 编辑商品 drawer (在商品库内点编辑触发,prefilled) ===== -->
<div class="drawer-bg" id="pc-drawer-bg"></div>
<aside class="drawer pc-drawer" id="pc-drawer" role="dialog" aria-label="编辑商品" aria-hidden="true">
<div class="drawer-h">
<h3 id="pc-drawer-title">编辑商品</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="pc-field">
<label class="pc-field-label">商品名称<span class="req">*</span></label>
<input class="input" id="pcf-name" placeholder="请输入商品名称(必填)" maxlength="100">
</div>
<div class="pc-field-row">
<div>
<label class="pc-field-label">品类<span class="req">*</span></label>
<select class="select" id="pcf-cat">
<option>美妆个护</option>
<option>服饰内衣</option>
<option>食品饮料</option>
<option>家居家电</option>
<option>数码 3C</option>
<option>个护清洁</option>
<option>运动户外</option>
<option>母婴亲子</option>
</select>
</div>
<div>
<label class="pc-field-label">目标人群<span style="color:var(--black-alpha-48);margin-left:2px">(选填)</span></label>
<input class="input" id="pcf-target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
</div>
</div>
<div class="pc-field">
<label class="pc-field-label">商品图片<span style="color:var(--black-alpha-48);margin-left:2px">(<span id="pcf-imgs-ct">6</span>)</span></label>
<div class="pc-imgs" id="pcf-imgs"></div>
</div>
<div class="pc-field">
<label class="pc-field-label">核心卖点<span class="req">*</span></label>
<ul class="pc-bullets" id="pcf-bullets">
<li class="add"><span class="num">+</span><input id="pcf-add-input" placeholder="添加新卖点 · 回车确认"></li>
</ul>
</div>
</div>
<div class="drawer-f">
<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>
<!-- ===== 模特库 全屏(无遮罩自适应) ===== -->
<div class="ml-modal-bg" id="ml-modal-bg">
<div class="ml-modal">
<div class="ml-modal-h">
<h2>模特库</h2>
<button class="x" type="button" id="ml-close-btn" aria-label="关闭">
<svg 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="ml-modal-body">
<aside class="ml-side">
<div class="ml-side-h">来源</div>
<div class="ml-side-item active" data-source="all">全部 <span class="ct">12</span></div>
<div class="ml-side-item" data-source="preset">平台预设 <span class="ct">10</span></div>
<div class="ml-side-item" data-source="own">我的上传 <span class="ct">2</span></div>
</aside>
<div class="ml-main">
<div class="ml-toolbar">
<div class="chip-group" data-key="gender">
<span class="lbl">性别</span>
<button class="chip active" type="button" data-val="">全部</button>
<button class="chip" type="button" data-val="女"></button>
<button class="chip" type="button" data-val="男"></button>
</div>
<div class="chip-group" data-key="age">
<span class="lbl">年龄</span>
<button class="chip active" type="button" data-val="">全部</button>
<button class="chip" type="button" data-val="青年">青年</button>
<button class="chip" type="button" data-val="中年">中年</button>
</div>
</div>
<div class="ml-scroll">
<div class="ml-grid" id="ml-grid">
<!-- 12 个模特卡片 (placeholder) -->
</div>
</div>
<!-- 添加模特 · 工作台画布(默认隐藏,点「添加模特」卡片后展开) -->
<div class="ml-canvas" id="ml-canvas" aria-hidden="true">
<div class="ml-canvas-h">
<button class="back-btn" type="button" id="ml-canvas-back" aria-label="返回模特库">
<svg 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>
返回
</button>
<h3>添加模特</h3>
<span class="mono">// 添加模特 · 工作台</span>
<span style="flex:1;"></span>
</div>
<div class="ml-canvas-body">
<!-- 左 · AI 生成(主视觉,照搬图片创作页面的内容区) -->
<section class="mc-ai">
<div class="mc-stream" id="mc-stream">
<div class="mc-stream-inner" id="mc-stream-inner">
<div class="mc-empty">
<div class="ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 2z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>
</div>
<span class="badge">// AI · STUDIO</span>
<h2>用 AI 生成一位新模特</h2>
<p>描述外形 + 风格 + 服装,AI 会同时生成立绘 + 正/侧/背三视图,加入模特库。</p>
<div class="examples">
<button class="ex" type="button" data-ex="清新校园风女生,黑色长直发,白色 T 恤 + 牛仔短裙,室内自然光">清新校园风女生</button>
<button class="ex" type="button" data-ex="都市 OL 通勤,黑色西装套装,30 岁知性气质">都市 OL 通勤</button>
<button class="ex" type="button" data-ex="健身房教练男性,运动背心 + 短裤,健身房布景">健身教练 · 男</button>
<button class="ex" type="button" data-ex="日系简约女生,棕色短发,米色针织衫,温柔气质">日系简约</button>
</div>
</div>
</div>
</div>
<div class="mc-input-wrap">
<div class="mc-input">
<!-- 上行 · 参考图 + 加号 (同一 flex 行,均 64×64) -->
<div class="mc-input-top">
<div class="mc-input-refs" id="mc-input-refs"></div>
<button class="add-btn" type="button" id="mc-add-btn" title="上传参考图">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
</button>
<input type="file" id="mc-ai-ref-input" accept="image/*" multiple hidden>
</div>
<!-- 中行 · textarea 满宽 -->
<textarea id="mc-input-text" rows="1" placeholder="描述模特外形、年龄、风格、服饰…例如:清新校园风女生,黑色长直发"></textarea>
<!-- 下行 · 参数 + 右 meta + 发送按钮 -->
<div class="mc-input-bottom">
<div class="param"><span class="lbl-mono">比例</span><span>3:4</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></div>
<div class="param"><span class="lbl-mono">风格</span><span>默认</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></div>
<div class="param"><span class="lbl-mono">张数</span><span>4</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></div>
<span class="right-meta">预估 <span class="val">¥0.80</span> · 余额 <span class="val">¥327.40</span></span>
<button class="send-btn" type="button" id="mc-send-btn" disabled title="生成">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
</div>
</div>
</div>
</section>
<!-- 右 · 副视觉 · tab 切换(AI 生成 / 本地上传)+ 3 模块(姓名/立绘/三视图) -->
<aside class="mc-up">
<div class="mc-up-tabs">
<button class="mc-up-tab active" type="button" data-tab="ai">AI 生成</button>
<button class="mc-up-tab" type="button" data-tab="local">本地上传</button>
</div>
<div class="mc-up-body">
<!-- ① 模特姓名 -->
<div class="mc-up-section">
<div class="mc-up-sec-h">// 模特姓名</div>
<input class="mc-up-name" type="text" id="mc-up-name" placeholder="给模特起个名字…" maxlength="20">
</div>
<!-- ② 模特立绘 -->
<div class="mc-up-section">
<div class="mc-up-sec-h">// 模特立绘</div>
<!-- AI 模式:从左侧 AI 生成结果选中 -->
<div class="mc-portrait-ai" data-show="ai">
<div class="empty" id="mc-portrait-ai-empty">
<div class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="4"/><path d="M3 21c0-3.5 3-6 6-6s6 2.5 6 6"/></svg></div>
<div class="desc">在左侧 AI 生成后<br>点击想要的立绘添加到这里</div>
<div class="mono">// 待选中</div>
</div>
<div class="picked" id="mc-portrait-ai-picked" hidden>
<span class="badge">已选用</span>
<div class="ph-frame" id="mc-portrait-ai-label">模特立绘</div>
<div class="ops">
<button type="button" id="mc-portrait-ai-clear" title="移除">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
</div>
</div>
<!-- 本地模式:上传多张 -->
<div class="mc-portrait-local" data-show="local" hidden>
<div class="drop" id="mc-portrait-local-drop" tabindex="0" role="button" aria-label="点击或拖入立绘">
<div class="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-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg></div>
<div class="t">点击或拖入立绘</div>
<div class="d">支持多张 JPG / PNG / WEBP · ≤ 10MB / 张</div>
</div>
<div class="list-h">
<span>// 已上传</span>
<span class="ct" id="mc-portrait-local-count">0</span>
<span></span>
</div>
<div class="list" id="mc-portrait-local-list"></div>
<input type="file" id="mc-portrait-local-input" accept="image/*" multiple hidden>
</div>
</div>
<!-- ③ 模特三视图 · 16:9 占位始终在,空态时居中覆盖生成按钮 -->
<div class="mc-up-section mc-triview" id="mc-triview-sec">
<div class="mc-up-sec-h">// 模特三视图</div>
<div class="result-wrap">
<div class="result" id="mc-triview-result">
<div class="ph-frame" id="mc-triview-frame">三视图(正/侧/背)</div>
<!-- 空态居中按钮 + 提示 -->
<button class="overlay-gen-btn" type="button" id="mc-triview-gen-btn" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z"/></svg>
生成三视图
</button>
<div class="overlay-hint" id="mc-triview-hint">// 先选中左侧 AI 立绘</div>
</div>
<!-- 有版本时的操作行 + 历史 -->
<div class="result-ops" id="mc-triview-ops" hidden>
<button type="button" id="mc-triview-rerun"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg> 重跑</button>
<span class="cost">~¥0.30 / 次</span>
</div>
<div class="history" id="mc-triview-history" hidden>
<div class="h-lbl">// 历史版本 · <span class="ct" id="mc-triview-history-count">0</span></div>
<div class="h-row" id="mc-triview-history-row"></div>
</div>
</div>
</div>
</div>
<div class="mc-up-foot">
<span class="stat" id="mc-up-stat">// 待完成</span>
<button type="button" class="commit-btn" id="mc-up-commit" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
加入模特库
</button>
</div>
</aside>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== 添加模特 · 选择来源 ===== -->
<div class="ml-up-choice-bg" id="ml-up-choice-bg">
<div class="ml-up-choice" role="dialog" aria-label="添加模特">
<div class="uc-h">
<div class="ic-m">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="4"/><path d="M3 21c0-3.5 3-6 6-6s6 2.5 6 6"/><path d="M19 8v6M22 11h-6"/></svg>
</div>
<div class="ti">
<strong>添加模特</strong>
<span class="mono">// 选择来源 · AI 生成或本地上传</span>
</div>
<button class="uc-x" type="button" id="ml-up-x" aria-label="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="uc-body">
<button type="button" class="uc-option" id="ml-up-ai">
<span class="opt-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 2z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>
</span>
<div class="opt-t">AI 生成</div>
<div class="opt-d">描述外形 + 风格,AI 自动生成新模特形象与三视图</div>
<span class="opt-tag">[ AI · STUDIO ]</span>
</button>
<button type="button" class="uc-option" id="ml-up-local">
<span class="opt-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-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
</span>
<div class="opt-t">本地上传</div>
<div class="opt-d">上传商家真人 / 既有素材,后续可生成三视图统一镜头</div>
<span class="opt-tag">[ UPLOAD ]</span>
</button>
</div>
</div>
</div>
<input type="file" id="ml-up-file" accept="image/*" multiple hidden>
<!-- ===== 模特详情 居中弹窗 (参考布局 v2) ===== -->
<div class="md-modal-bg" id="md-modal-bg">
<div class="md-modal">
<div class="md-modal-h">
<h3 id="md-title">模特详情</h3>
<span class="ad-tag" id="md-kind">/ 人物 · 模特</span>
<button class="x" type="button" id="md-close-btn" aria-label="关闭">
<svg 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="md-modal-body">
<div class="md-detail-grid">
<!-- 左栏 · 大立绘 + 缩略图 -->
<div class="md-lead">
<div class="md-lead-wrap">
<div class="md-lead-img">
<div class="ph-name" id="md-portrait-name"></div>
<div class="ph-frame">立绘 · 3:4</div>
</div>
<button class="md-zoom-btn" type="button" id="md-lead-zoom" aria-label="查看大图" title="查看大图">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5"/></svg>
</button>
</div>
<div class="md-thumbs" id="md-thumbs"></div>
</div>
<!-- 右栏 · 三视图 + 简介 + 属性 -->
<div class="md-right">
<div class="md-section">
<div class="md-section-h">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></span>
<span class="t">三视图</span>
<span class="ratio-chip">16:9</span>
<button class="icon-btn" type="button" title="下载"><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 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
</div>
<div class="md-views">
<div class="md-view"><div class="lbl">正 / 侧 / 背 · 三视图</div></div>
</div>
</div>
<div class="md-section">
<div class="md-section-h">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h10"/></svg></span>
<span class="t">简介</span>
</div>
<p class="md-intro" id="md-intro"></p>
<div class="md-tags" id="md-tags"></div>
</div>
<div class="md-props" id="md-props"></div>
</div>
</div>
</div>
<div class="md-modal-f">
<div class="foot-stats">
<button class="stat-btn" type="button">
<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 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
下载
</button>
</div>
<button class="btn btn-primary" type="button" id="md-select">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12l5 5L20 7"/></svg>
选用此模特
</button>
</div>
</div>
</div>
<!-- ===== 缺三视图 · 保存前提醒弹窗 ===== -->
<div class="mc-leave-bg" id="mc-notri-bg" aria-hidden="true">
<div class="mc-leave" role="dialog" aria-modal="true" aria-labelledby="mc-notri-title">
<div class="lv-h">
<span class="ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01M10.3 3.86l-8.18 14.18A2 2 0 0 0 3.84 21h16.32a2 2 0 0 0 1.72-2.96L13.7 3.86a2 2 0 0 0-3.4 0z"/></svg>
</span>
<h3 id="mc-notri-title">缺三视图 · 仍要保存吗?</h3>
<span class="mono">// MISSING TRI-VIEW</span>
</div>
<div class="lv-b">
该模特尚未生成 <b>正 / 侧 / 背</b> 三视图。直接进入后续图片/视频生成时,模型缺少多角度参考,<b>角色一致性、姿态稳定性可能下降</b>
<br><br>
建议先点「<b>去生成三视图</b>」补齐(约 12s · ¥0.30);若现在不生成,后续也可以在<b>资产详情页</b>里随时补回。
</div>
<div class="lv-f">
<span class="spacer"></span>
<button class="btn" type="button" id="mc-notri-save">仍要保存</button>
<button class="btn btn-primary" type="button" id="mc-notri-gen">去生成三视图</button>
</div>
</div>
</div>
<!-- ===== 离开工作台 · 二次确认弹窗 ===== -->
<div class="mc-leave-bg" id="mc-leave-bg" aria-hidden="true">
<div class="mc-leave" role="dialog" aria-modal="true" aria-labelledby="mc-leave-title">
<div class="lv-h">
<span class="ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01M10.3 3.86l-8.18 14.18A2 2 0 0 0 3.84 21h16.32a2 2 0 0 0 1.72-2.96L13.7 3.86a2 2 0 0 0-3.4 0z"/></svg>
</span>
<h3 id="mc-leave-title">退出工作台?</h3>
<span class="mono">// UNSAVED</span>
</div>
<div class="lv-b" id="mc-leave-body">
工作台已有内容,退出后<b>不会保存</b>。可继续编辑并点「加入模特库」来保留进度。
</div>
<div class="lv-f">
<span class="spacer"></span>
<button class="btn" type="button" id="mc-leave-cancel">继续编辑</button>
<button class="btn btn-danger" type="button" id="mc-leave-confirm">不保存,退出</button>
</div>
</div>
</div>
<script src="assets/shell.js?v=202605211643"></script>
<script src="assets/new-product-drawer.js?v=202605211643"></script>
<script>
Shell.render({
active: 'asset-factory',
crumbs: [
{ label: '工作台', href: 'index.html' },
{ label: '图片生成', href: 'asset-factory.html' },
{ label: '模特上身图' }
]
});
// ─── 商品库数据 (mock,与 products.html 7 个商品对齐) ───
const PRODUCTS = [
{ id: 'p1', name: '透真玻尿酸补水面膜', cat: '美妆个护', meta: '熬夜党 · 124 素材' },
{ id: 'p2', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', meta: '通勤 · 96 素材' },
{ id: 'p3', name: '滋啦速食牛肉面 6 桶装', cat: '食品饮料', meta: '加班 · 96 素材' },
{ id: 'p4', name: '透真清透物理防晒霜', cat: '美妆个护', meta: 'SPF50 · 76 素材' },
{ id: 'p5', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', meta: '提神 · 68 素材' },
{ id: 'p6', name: '小熊 4L 可视空气炸锅', cat: '家居家电', meta: '小户型 · 54 素材' },
{ id: 'p7', name: '露露同款裸感瑜伽裤', cat: '运动户外', meta: '健身房 · 42 素材' },
];
// ─── State (单选 · 默认全空) ───
const state = {
selectedProd: null, // string | null
selectedModel: null, // string | null
count: 4,
ratio: '1:1',
};
const UNIT_PRICE = 0.30;
// ─── 商品空间 (左侧栏) 渲染 ───
let _psQuery = '';
function renderProdSpace() {
const listEl = document.getElementById('ps-list');
const ctEl = document.getElementById('ps-count');
const allCtEl = document.getElementById('ps-all-ct');
if (!listEl) return;
if (ctEl) ctEl.textContent = PRODUCTS.length;
if (allCtEl) allCtEl.textContent = PRODUCTS.length + ' 个';
const q = _psQuery.trim();
const filtered = q
? PRODUCTS.filter(p => p.name.includes(q) || p.cat.includes(q))
: PRODUCTS;
if (!filtered.length) {
listEl.innerHTML = `<div class="mp-ps-empty">// NO MATCH<br>试试其他关键词</div>`;
return;
}
listEl.innerHTML = filtered.map(p => `
<div class="mp-prod-item${state.selectedProd === p.id ? ' active' : ''}" data-id="${p.id}">
<div class="placeholder thumb"></div>
<div class="body">
<div class="nm">${p.name}</div>
<div class="sub">// ${p.cat}</div>
</div>
</div>
`).join('');
listEl.querySelectorAll('.mp-prod-item').forEach(el => {
el.addEventListener('click', () => selectProduct(el.dataset.id));
});
}
// 选中商品 (sidebar 单选 · 同步更新表单/预览/Cost)
function selectProduct(id) {
state.selectedProd = id;
// 商品空间 active 态
document.querySelectorAll('.mp-prod-item').forEach(el => {
el.classList.toggle('active', el.dataset.id === id);
});
// 当前商品 header strip
updateCurProdHeader();
// 预览区: 按商品过滤批次重渲染
if (typeof renderBatchesForCurrentProd === 'function') renderBatchesForCurrentProd();
// 同步 pv-summary 商品名
const p = PRODUCTS.find(x => x.id === id);
document.getElementById('pv-prod').textContent = p ? p.name : '未选择';
updateCost();
}
// 当前商品 header strip
function updateCurProdHeader() {
const nmEl = document.getElementById('cur-prod-nm');
const statsEl = document.getElementById('cur-prod-stats');
const batchesEl = document.getElementById('cur-prod-batches');
if (!nmEl) return;
const p = state.selectedProd ? PRODUCTS.find(x => x.id === state.selectedProd) : null;
if (!p) {
nmEl.textContent = '未选择 · 请在左侧商品空间选一个';
nmEl.classList.add('placeholder');
if (statsEl) statsEl.hidden = true;
} else {
nmEl.textContent = p.name;
nmEl.classList.remove('placeholder');
const ct = (window._countBatchesForProd ? window._countBatchesForProd(p.id) : 0);
if (batchesEl) batchesEl.textContent = ct;
if (statsEl) statsEl.hidden = false;
}
}
// 保留旧函数名 alias (兼容旧 call site)
function renderSelectedProds() {
renderProdSpace();
updateCurProdHeader();
const p = state.selectedProd ? PRODUCTS.find(x => x.id === state.selectedProd) : null;
document.getElementById('pv-prod').textContent = p ? p.name : '未选择';
updateCost();
}
// ─── 商品库全屏弹窗 (单选) ───
let _plDraft = null; // string | null
let _plCatFilter = '';
let _plQuery = '';
function renderProdLib() {
const grid = document.getElementById('pl-grid');
let list = PRODUCTS;
if (_plCatFilter) list = list.filter(p => p.cat === _plCatFilter);
if (_plQuery) list = list.filter(p => p.name.includes(_plQuery));
grid.innerHTML = list.map(p => `
<div class="pl-card${_plDraft === p.id ? ' selected' : ''}" data-id="${p.id}">
<div class="pl-card-actions">
<button class="pl-act" type="button" data-edit="${p.id}" title="编辑商品" aria-label="编辑">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
</button>
<button class="pl-act danger" type="button" data-del="${p.id}" title="删除商品" aria-label="删除">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>
</button>
</div>
<div class="pl-check"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></div>
<div class="placeholder pl-thumb"><span class="ph-frame">${p.name}</span></div>
<div class="pl-name">${p.name}</div>
<div class="pl-meta">${p.cat} · ${p.meta}</div>
</div>
`).join('');
grid.querySelectorAll('.pl-card').forEach(card => {
card.addEventListener('click', e => {
// 点击编辑/删除按钮不切换选中
if (e.target.closest('[data-edit]') || e.target.closest('[data-del]')) return;
const id = card.dataset.id;
// 单选: 选中当前,取消其他
_plDraft = (_plDraft === id) ? null : id;
grid.querySelectorAll('.pl-card').forEach(c => c.classList.toggle('selected', c.dataset.id === _plDraft));
document.getElementById('pl-sel-ct').textContent = _plDraft ? 1 : 0;
});
});
grid.querySelectorAll('[data-edit]').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
openEditProductDrawer(btn.dataset.edit);
});
});
grid.querySelectorAll('[data-del]').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const id = btn.dataset.del;
const p = PRODUCTS.find(x => x.id === id);
if (!p) return;
if (!confirm('确认删除「' + p.name + '」?\n该操作不可撤销,商品下生成的素材记录也会一并清理。')) return;
// 从 mock 数据移除
const idx = PRODUCTS.findIndex(x => x.id === id);
if (idx >= 0) PRODUCTS.splice(idx, 1);
if (_plDraft === id) _plDraft = null;
if (state.selectedProd === id) state.selectedProd = null;
renderProdLib();
renderSelectedProds();
Shell.toast('已删除', p.name);
});
});
document.getElementById('pl-sel-ct').textContent = _plDraft ? 1 : 0;
}
// ─── 编辑商品 drawer (在商品库内 prefill 数据) ───
// mock 商品扩展属性 (target + bullets),缺失则给默认值
const PRODUCT_EXTRA = {
p1: { target: '熬夜党 · 25-35 岁女性 · 敏感肌', bullets: ['72h 长效补水', '官方授权正品', '通勤补妆神器'], imgs: 6 },
p2: { target: '通勤党 · 18-30 岁 · 大学生 / 白领', bullets: ['主动降噪 35dB', '蓝牙 5.4 双设备', '32h 续航'], imgs: 6 },
p3: { target: '加班党 · 独居青年 · 一人食场景', bullets: ['一杯水即可', '原切牛肉块充足', '6 桶大箱装'], imgs: 6 },
p4: { target: '通勤防晒 · 油皮 / 敏感肌', bullets: ['SPF50+ PA++++', '物理防晒不刺激', '清透不假白'], imgs: 6 },
p5: { target: '咖啡入门 · 早八党 · 加班族', bullets: ['冷热水即溶', '原产地豆精选', '24 颗精装'], imgs: 6 },
p6: { target: '小户型 · 健康饮食 · 新手厨房', bullets: ['4L 大容量', '可视玻璃观察', '一键预设'], imgs: 6 },
p7: { target: '健身房 · 通勤穿搭 · 18-32 岁女性', bullets: ['裸感面料', '高弹收腹', '亲肤透气'], imgs: 6 },
};
// 渲染商品图片 grid · n 张占位 + 上传按钮
function renderProdImgs(n) {
const grid = document.getElementById('pcf-imgs');
const ct = document.getElementById('pcf-imgs-ct');
if (!grid) return;
if (ct) ct.textContent = n;
let html = '';
for (let i = 0; i < n; i++) {
html += `<div class="thumb"><span class="ph-frame">1:1</span><button class="rm" type="button" title="删除" data-idx="${i}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button></div>`;
}
html += `<div class="img-upload" id="pcf-img-add" title="上传图片"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg></div>`;
grid.innerHTML = html;
grid.querySelectorAll('.thumb .rm').forEach(btn => {
btn.addEventListener('click', () => {
btn.closest('.thumb').remove();
if (ct) ct.textContent = grid.querySelectorAll('.thumb').length;
});
});
const addBtn = document.getElementById('pcf-img-add');
if (addBtn) addBtn.addEventListener('click', () => {
if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('上传图片', '// 演示版暂不支持真实上传');
});
}
let _editingProdId = null;
function openEditProductDrawer(id) {
const p = PRODUCTS.find(x => x.id === id);
if (!p) return;
_editingProdId = id;
// prefill
document.getElementById('pc-drawer-title').textContent = '编辑商品 · ' + p.name;
document.getElementById('pcf-name').value = p.name;
document.getElementById('pcf-cat').value = p.cat;
const extra = PRODUCT_EXTRA[id] || { target: '', bullets: [], imgs: 0 };
document.getElementById('pcf-target').value = extra.target || '';
// 渲染商品图片 (n 张占位)
renderProdImgs(typeof extra.imgs === 'number' ? extra.imgs : 6);
// 渲染 bullets
const ul = document.getElementById('pcf-bullets');
// 移除除 .add 之外的所有 li
ul.querySelectorAll('li:not(.add)').forEach(li => li.remove());
const addLi = ul.querySelector('.add');
(extra.bullets || []).forEach((b, i) => {
const li = document.createElement('li');
li.innerHTML = `
<span class="num">${i + 1}</span>
<input value="${b.replace(/"/g, '&quot;')}">
<button class="rm" type="button" aria-label="删除"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button>
`;
ul.insertBefore(li, addLi);
li.querySelector('.rm').addEventListener('click', () => {
li.remove();
renumberBullets();
});
});
document.getElementById('pcf-add-input').value = '';
document.getElementById('pc-drawer-bg').classList.add('show');
document.getElementById('pc-drawer').classList.add('show');
document.getElementById('pc-drawer').setAttribute('aria-hidden', 'false');
}
function renumberBullets() {
const ul = document.getElementById('pcf-bullets');
[...ul.querySelectorAll('li:not(.add) .num')].forEach((s, i) => { s.textContent = i + 1; });
}
function closeEditProductDrawer() {
document.getElementById('pc-drawer-bg').classList.remove('show');
document.getElementById('pc-drawer').classList.remove('show');
document.getElementById('pc-drawer').setAttribute('aria-hidden', 'true');
_editingProdId = null;
}
// 新增 bullet · 回车
document.getElementById('pcf-add-input').addEventListener('keydown', e => {
if (e.key !== 'Enter') return;
const v = e.target.value.trim();
if (!v) return;
const ul = document.getElementById('pcf-bullets');
const addLi = ul.querySelector('.add');
const li = document.createElement('li');
li.innerHTML = `
<span class="num">0</span>
<input value="${v.replace(/"/g, '&quot;')}">
<button class="rm" type="button" aria-label="删除"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button>
`;
ul.insertBefore(li, addLi);
li.querySelector('.rm').addEventListener('click', () => { li.remove(); renumberBullets(); });
e.target.value = '';
renumberBullets();
});
document.getElementById('pc-drawer-close').addEventListener('click', closeEditProductDrawer);
document.getElementById('pc-cancel-btn').addEventListener('click', closeEditProductDrawer);
document.getElementById('pc-drawer-bg').addEventListener('click', closeEditProductDrawer);
document.getElementById('pc-save-btn').addEventListener('click', () => {
if (!_editingProdId) return;
const newName = document.getElementById('pcf-name').value.trim();
const newCat = document.getElementById('pcf-cat').value;
const newTarget = document.getElementById('pcf-target').value.trim();
if (!newName) { Shell.toast('请填写商品名称'); return; }
// 写回 PRODUCTS
const p = PRODUCTS.find(x => x.id === _editingProdId);
if (p) { p.name = newName; p.cat = newCat; }
// 写回 PRODUCT_EXTRA (含 imgs 数量)
const bullets = [...document.querySelectorAll('#pcf-bullets li:not(.add) input')].map(i => i.value.trim()).filter(Boolean);
const imgs = document.querySelectorAll('#pcf-imgs .thumb').length;
PRODUCT_EXTRA[_editingProdId] = { target: newTarget, bullets, imgs };
Shell.toast('已保存', newName);
closeEditProductDrawer();
renderProdLib();
renderSelectedProds();
});
// 全部商品 入口 (左侧栏底部 · 打开商品库 modal)
function openProdLibModal() {
_plDraft = state.selectedProd;
_plCatFilter = '';
_plQuery = '';
document.getElementById('pl-search-input').value = '';
document.querySelectorAll('.pl-side-item').forEach(x => x.classList.toggle('active', x.dataset.cat === ''));
renderProdLib();
document.getElementById('pl-modal-bg').classList.add('show');
}
document.getElementById('ps-all-btn').addEventListener('click', openProdLibModal);
// 商品空间 · 搜索框 · 新建按钮
document.getElementById('ps-search-input').addEventListener('input', e => {
_psQuery = e.target.value;
renderProdSpace();
});
document.getElementById('ps-new-btn').addEventListener('click', () => {
if (!window.NewProductDrawer) { Shell.toast('Drawer 未加载'); return; }
window.NewProductDrawer.open({
onSave: function (p) {
const product = {
id: p.id,
name: p.name,
cat: p.cat,
meta: (p.target ? p.target.split(/[ ,、、]+/)[0] : '新建') + ' · ' + p.imgs + ' 张图',
};
PRODUCTS.unshift(product);
renderProdSpace();
selectProduct(product.id);
Shell.toast('已加入商品库', '+ ' + product.name);
}
});
});
document.getElementById('pl-close-btn').addEventListener('click', () => {
document.getElementById('pl-modal-bg').classList.remove('show');
});
document.getElementById('pl-cancel-btn').addEventListener('click', () => {
document.getElementById('pl-modal-bg').classList.remove('show');
});
document.getElementById('pl-confirm-btn').addEventListener('click', () => {
if (!_plDraft) { Shell.toast('请先选择商品', '只能选 1 个'); return; }
document.getElementById('pl-modal-bg').classList.remove('show');
selectProduct(_plDraft);
});
document.getElementById('pl-new-btn').addEventListener('click', () => {
if (!window.NewProductDrawer) { Shell.toast('Drawer 未加载'); return; }
// 商品库保持 open(drawer z-index 1101 > pl-modal-bg 998 会覆盖之上)
window.NewProductDrawer.open({
onSave: function (p) {
// 把新商品注入本页 PRODUCTS,刷新商品库 + 已选列表
const product = {
id: p.id,
name: p.name,
cat: p.cat,
meta: (p.target ? p.target.split(/[ ,、、]+/)[0] : '新建') + ' · ' + p.imgs + ' 张图',
};
PRODUCTS.unshift(product);
// 单选: 新建商品直接选中(覆盖原选)
_plDraft = product.id;
// 强制 reset filter/query,保证新商品在首位可见
_plCatFilter = '';
_plQuery = '';
const searchInput = document.getElementById('pl-search-input');
if (searchInput) searchInput.value = '';
renderProdLib();
renderProdSpace();
selectProduct(product.id);
Shell.toast('已加入商品库', '+ ' + product.name);
}
});
});
document.querySelectorAll('.pl-side-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.pl-side-item').forEach(x => x.classList.remove('active'));
item.classList.add('active');
_plCatFilter = item.dataset.cat;
renderProdLib();
});
});
document.getElementById('pl-search-input').addEventListener('input', e => {
_plQuery = e.target.value.trim();
renderProdLib();
});
// ─── 模特选择 (单选) ───
function updateModelSummary() {
const id = state.selectedModel;
const card = id ? document.querySelector('.model-card[data-id="' + id + '"]') : null;
const name = card ? card.dataset.name : '';
document.getElementById('pv-model').textContent = name
? name + ' (亚洲·25岁·清新)'
: '未选择';
updateCost();
}
function updateCost() {
const hasProd = !!state.selectedProd;
const hasModel = !!state.selectedModel;
const total = (hasProd && hasModel ? 1 : 0) * state.count * UNIT_PRICE;
document.getElementById('cost-total').textContent = '¥' + total.toFixed(2);
const btn = document.getElementById('mp-go-btn');
if (!hasProd || !hasModel) btn.classList.add('disabled');
else btn.classList.remove('disabled');
}
function selectModel(id) {
state.selectedModel = (state.selectedModel === id) ? null : id;
renderModelMini();
// 同步所有出现的 model-card (lib grid 里的)
document.querySelectorAll('.ml-grid .model-card').forEach(c =>
c.classList.toggle('selected', c.dataset.id === state.selectedModel)
);
updateModelSummary();
}
/* mini grid · 动态渲染 · 选中的模特(如不在默认 4 张里)会顶替到首位 */
const MINI_DEFAULT_IDS = ['m1','m2','m3','m4'];
function renderModelMini() {
const grid = document.getElementById('model-grid-mini');
if (!grid) return;
const selId = state.selectedModel;
let ids;
if (!selId || MINI_DEFAULT_IDS.includes(selId)) {
ids = MINI_DEFAULT_IDS.slice();
} else {
ids = [selId, ...MINI_DEFAULT_IDS.slice(0, 3)];
}
grid.innerHTML = ids.map(id => {
const m = MODELS.find(x => x.id === id);
if (!m) return '';
const isSelected = m.id === selId;
return `
<div class="model-card${isSelected ? ' selected' : ''}" data-id="${m.id}" data-name="${m.name}">
<div class="placeholder m-thumb"><span class="ph-frame">${m.name}</span></div>
<div class="m-name">${m.name}</div>
<div class="m-tag">${m.gender}·${m.age}·${m.style}</div>
</div>
`;
}).join('');
grid.querySelectorAll('.model-card').forEach(card => {
card.addEventListener('click', e => {
if (e.target.closest('.m-thumb')) {
openModelDetail(card.dataset.id);
return;
}
selectModel(card.dataset.id);
});
});
}
// 注:首次 renderModelMini() 调用挪到 MODELS 声明之后,避免 TDZ
// ─── 立即生成设置 ───
document.querySelectorAll('.pill-row').forEach(row => {
row.addEventListener('click', e => {
const btn = e.target.closest('.opt');
if (!btn) return;
row.querySelectorAll('.opt').forEach(o => o.classList.remove('active'));
btn.classList.add('active');
const key = row.dataset.key;
state[key] = isNaN(+btn.dataset.val) ? btn.dataset.val : +btn.dataset.val;
if (key === 'count') document.getElementById('pv-count').textContent = btn.dataset.val;
if (key === 'ratio') document.getElementById('pv-ratio').textContent = btn.dataset.val;
updateCost();
});
});
// ─── 预览区空态 / 内容 切换 ───
function showPreviewEmpty() {
const empty = document.getElementById('pv-empty');
const sum = document.getElementById('pv-summary');
const grid = document.getElementById('pv-grid');
const foot = document.getElementById('pv-foot');
if (empty) empty.hidden = false;
if (sum) sum.hidden = true;
if (grid) grid.hidden = true;
if (foot) foot.hidden = true;
}
function showPreviewContent() {
const empty = document.getElementById('pv-empty');
const sum = document.getElementById('pv-summary');
const grid = document.getElementById('pv-grid');
const foot = document.getElementById('pv-foot');
if (empty) empty.hidden = true;
if (sum) sum.hidden = false;
if (grid) grid.hidden = false;
if (foot) foot.hidden = false;
// pv-batch 由 renderResultCards 单独控制
}
// ─── 立即生成 + 生成结果交互 (hover 重跑/采用 + 批量) ───
const RERUN_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>';
const ADOPT_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
let _batchSeq = 0;
const CELL_RERUN_SVG = '<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-6.7L21 8M21 3v5h-5"/></svg>';
const CELL_DL_SVG = '<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-4M7 10l5 5 5-5M12 15V3"/></svg>';
const CELL_MORE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></svg>';
const CELL_ADOPT_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>';
const CELL_DEL_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>';
const CELL_EDIT_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4z"/></svg>';
function buildResultCard(label) {
const div = document.createElement('div');
div.className = 'mp-result gen';
div.innerHTML = `
<div class="mp-r-thumb"><span class="ph-frame">${label || state.ratio}</span></div>
<span class="adopt-badge">已采用</span>
<div class="cell-feedback" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
<span>已采用</span>
</div>
<div class="cell-ops">
<button class="r-rerun" type="button" title="再次生成" aria-label="再次生成">${CELL_RERUN_SVG}</button>
<button class="r-dl" type="button" title="下载" aria-label="下载">${CELL_DL_SVG}</button>
<div class="cell-more-wrap">
<button class="r-more" type="button" title="更多" aria-label="更多">${CELL_MORE_SVG}</button>
<div class="cell-more-menu">
<button class="r-adopt" type="button">${CELL_ADOPT_SVG}<span>加入资产库</span></button>
<button class="r-del danger" type="button">${CELL_DEL_SVG}<span>删除</span></button>
</div>
</div>
</div>
`;
// 模拟 ~1.2s 后切到 ok 状态
setTimeout(() => {
if (div.classList.contains('gen')) div.classList.remove('gen');
}, 1200);
div.querySelector('.r-rerun').addEventListener('click', e => { e.stopPropagation(); rerunOne(div); });
div.querySelector('.r-dl').addEventListener('click', e => {
e.stopPropagation();
Shell.toast('下载', '已开始下载 · MOCK');
});
// 更多 menu 开/合
const moreBtn = div.querySelector('.r-more');
const moreWrap = div.querySelector('.cell-more-wrap');
moreBtn.addEventListener('click', e => {
e.stopPropagation();
const willOpen = !moreWrap.classList.contains('open');
document.querySelectorAll('.mp-result .cell-more-wrap.open').forEach(w => w.classList.remove('open'));
if (willOpen) moreWrap.classList.add('open');
});
div.querySelector('.r-adopt').addEventListener('click', e => {
e.stopPropagation();
moreWrap.classList.remove('open');
adoptOne(div);
});
div.querySelector('.r-del').addEventListener('click', e => {
e.stopPropagation();
moreWrap.classList.remove('open');
const batch = div.closest('.mp-result-batch');
div.remove();
if (batch) {
// 如果该批次空了, 整批移除
if (!batch.querySelectorAll('.mp-result:not(.placeholder-only)').length) batch.remove();
else updateBatchSummary();
}
Shell.toast('已删除');
});
return div;
}
function appendBatch(n, kind) {
const grid = document.getElementById('pv-grid');
_batchSeq += 1;
const batch = document.createElement('div');
batch.className = 'mp-result-batch';
batch.dataset.kind = kind;
const ts = new Date();
const tsStr = `${String(ts.getHours()).padStart(2,'0')}:${String(ts.getMinutes()).padStart(2,'0')}:${String(ts.getSeconds()).padStart(2,'0')}`;
const labCls = kind === 'gen' ? 'gen' : 'rerun';
const labTxt = kind === 'gen' ? `批次 ${_batchSeq} · 初始生成` : (kind === 'rerun-all' ? `批次 ${_batchSeq} · 全部重跑` : `批次 ${_batchSeq} · 单张重跑`);
const _curModel = state.selectedModel ? MODELS.find(x => x.id === state.selectedModel) : null;
batch.dataset.ts = String(ts.getTime());
batch.dataset.modelId = _curModel ? _curModel.id : '';
batch.dataset.modelName = _curModel ? _curModel.name : '';
batch.dataset.ratio = state.ratio || '';
batch.dataset.search = [
labTxt, _curModel ? _curModel.name : '',
_curModel ? _curModel.style : '',
state.ratio || '', n + '张'
].join(' ').toLowerCase();
batch.innerHTML = `
<div class="mp-batch-head">
<span class="lab ${labCls}">${labTxt}</span>
<span class="sep">·</span>
<span>${n} 张 · ${state.ratio}</span>
${_curModel ? `<span class="sep">·</span><span>${_curModel.name}</span>` : ''}
<span class="sep">·</span>
<span>${tsStr}</span>
</div>
<div class="mp-result-grid"></div>
<div class="mp-pv-batch batch-foot">
<button class="pill-btn edit-batch" type="button" title="重新编辑">
${CELL_EDIT_SVG}
<span>重新编辑</span>
</button>
<button class="pill-btn rerun-batch" type="button" title="再次生成这一批">
${CELL_RERUN_SVG}
<span>再次生成</span>
</button>
<div class="batch-more-wrap">
<button class="pill-btn icon batch-more" type="button" title="更多" aria-label="更多">${CELL_MORE_SVG}</button>
<div class="batch-more-menu" role="menu">
<button class="batch-save-all" type="button">${CELL_ADOPT_SVG}<span>全部加入资产库</span></button>
<button class="batch-del danger" type="button">${CELL_DEL_SVG}<span>删除该批结果</span></button>
</div>
</div>
</div>
`;
const gridInner = batch.querySelector('.mp-result-grid');
for (let i = 0; i < n; i++) gridInner.appendChild(buildResultCard());
batch.querySelector('.edit-batch').addEventListener('click', () => {
// 跳回左侧表单(模特/数量/比例) · 与图片创作「重新编辑」语义对齐
const form = document.querySelector('.mp-form');
if (form) form.scrollIntoView({ behavior: 'smooth', block: 'start' });
Shell.toast('重新编辑', '请在左侧调整模特 / 比例 / 数量后再生成');
});
batch.querySelector('.rerun-batch').addEventListener('click', () => {
appendBatch(n, 'rerun-all');
Shell.toast('再次生成', n + ' 张图重新生成中 · 新批次已追加');
});
// 提供 _adoptAll 内联函数 (给 更多 menu 的「全部加入资产库」复用)
const _adoptAll = () => {
const cards = batch.querySelectorAll('.mp-result:not(.adopted)');
if (!cards.length) { Shell.toast('该批次已全部采用'); return; }
cards.forEach(c => { c.classList.remove('gen'); c.classList.add('adopted'); });
updateBatchSummary();
Shell.toast('已全部加入资产库', cards.length + ' 张图入对应商品的 AI 素材 · 扣 ¥' + (cards.length * UNIT_PRICE).toFixed(2));
};
// 批次「更多」按钮 → 开/合 menu
const _bMoreBtn = batch.querySelector('.batch-more');
const _bMoreWrap = batch.querySelector('.batch-more-wrap');
if (_bMoreBtn && _bMoreWrap) {
_bMoreBtn.addEventListener('click', e => {
e.stopPropagation();
const willOpen = !_bMoreWrap.classList.contains('open');
document.querySelectorAll('.mp-pv-batch .batch-more-wrap.open').forEach(w => w.classList.remove('open'));
if (willOpen) _bMoreWrap.classList.add('open');
});
}
batch.querySelector('.batch-save-all').addEventListener('click', e => {
e.stopPropagation();
if (_bMoreWrap) _bMoreWrap.classList.remove('open');
_adoptAll();
});
batch.querySelector('.batch-del').addEventListener('click', e => {
e.stopPropagation();
if (_bMoreWrap) _bMoreWrap.classList.remove('open');
batch.remove();
updateBatchSummary();
Shell.toast('已删除该批结果');
});
grid.appendChild(batch);
batch.scrollIntoView({ behavior: 'smooth', block: 'end' });
updateBatchSummary();
if (typeof _refreshModelMenu === 'function') _refreshModelMenu();
if (typeof applyPvFilters === 'function') applyPvFilters();
}
function renderResultCards(n) {
const grid = document.getElementById('pv-grid');
// 首次生成:清掉默认 placeholder-batch 占位,但保留已有真实批次
// 再次「立即生成」(用户换了设置):追加新批次到底部,不再覆盖
grid.querySelectorAll('.placeholder-batch').forEach(el => el.remove());
appendBatch(n, 'gen');
}
function rerunOne(card) {
if (!card) return;
appendBatch(1, 'rerun-one');
}
function adoptOne(card) {
if (!card || card.classList.contains('adopted')) return;
card.classList.remove('gen');
card.classList.add('adopted');
// spec §4.18 · 就地中央反馈 (替代全局 toast,用户不必转头看屏幕角落)
card.classList.add('show-feedback');
setTimeout(() => card.classList.remove('show-feedback'), 1500);
updateBatchSummary();
}
// 点击页面其它位置 → 关闭单图/批次 more menu
document.addEventListener('click', e => {
if (!e.target.closest('.mp-result .cell-more-wrap')) {
document.querySelectorAll('.mp-result .cell-more-wrap.open').forEach(w => w.classList.remove('open'));
}
if (!e.target.closest('.mp-pv-batch .batch-more-wrap')) {
document.querySelectorAll('.mp-pv-batch .batch-more-wrap.open').forEach(w => w.classList.remove('open'));
}
});
function updateBatchSummary() {
// 逐批次更新「全部采用 · 已采用/总数」
document.querySelectorAll('#pv-grid .mp-result-batch').forEach(batch => {
const cards = batch.querySelectorAll('.mp-result:not(.placeholder-only)');
const adopted = batch.querySelectorAll('.mp-result.adopted').length;
const adoptedEl = batch.querySelector('.adopt-batch .adopted');
const totalEl = batch.querySelector('.adopt-batch .total');
if (adoptedEl) adoptedEl.textContent = adopted;
if (totalEl) totalEl.textContent = cards.length;
});
}
document.getElementById('mp-go-btn').addEventListener('click', () => {
if (!state.selectedModel || !state.selectedProd) return;
const grid = document.getElementById('pv-grid');
const hasReal = grid && grid.querySelector('.mp-result-batch:not(.placeholder-batch)');
const prod = PRODUCTS.find(p => p.id === state.selectedProd);
if (hasReal) {
Shell.toast('已追加批次', state.count + ' 张图新增到下方 · 旧批次保留');
} else {
Shell.toast('已提交任务', (prod ? prod.name + ' · ' : '') + state.count + ' 张图生成中');
}
showPreviewContent();
renderResultCards(state.count);
});
// 批量按钮已下沉到每个批次内部 (.rerun-batch / .adopt-batch)
// 不再有全局 #pv-rerun-all / #pv-adopt-all
// ============================================================
// 工具台头部 · 搜索 / 时间 / 模特 筛选
// ============================================================
const _pvFilter = { time: 'all', model: 'all', search: '' };
function _pvTimeMatch(ts, key) {
if (key === 'all') return true;
const now = Date.now();
const diff = now - Number(ts);
if (key === '10min') return diff <= 10 * 60 * 1000;
if (key === '1h') return diff <= 60 * 60 * 1000;
if (key === 'today') {
const a = new Date(now); const b = new Date(Number(ts));
return a.toDateString() === b.toDateString();
}
return true;
}
function applyPvFilters() {
const grid = document.getElementById('pv-grid');
if (!grid) return;
const q = (_pvFilter.search || '').trim().toLowerCase();
let visible = 0;
grid.querySelectorAll('.mp-result-batch:not(.placeholder-batch)').forEach(batch => {
let ok = true;
if (!_pvTimeMatch(batch.dataset.ts, _pvFilter.time)) ok = false;
if (ok && _pvFilter.model !== 'all' && batch.dataset.modelId !== _pvFilter.model) ok = false;
if (ok && q && !(batch.dataset.search || '').includes(q)) ok = false;
batch.dataset.hidden = ok ? '0' : '1';
if (ok) visible += 1;
});
// 占位批次:只有当无真实批次 & 无 active filter 时显示
const hasReal = !!grid.querySelector('.mp-result-batch:not(.placeholder-batch)');
const filterActive = _pvFilter.time !== 'all' || _pvFilter.model !== 'all' || q.length > 0;
grid.querySelectorAll('.placeholder-batch').forEach(ph => {
ph.dataset.hidden = (hasReal || filterActive) ? '1' : '0';
});
}
function _refreshModelMenu() {
const menu = document.getElementById('mp-menu-model');
if (!menu) return;
const grid = document.getElementById('pv-grid');
const used = new Map();
if (grid) {
grid.querySelectorAll('.mp-result-batch:not(.placeholder-batch)').forEach(b => {
const id = b.dataset.modelId; const nm = b.dataset.modelName;
if (id) used.set(id, nm || id);
});
}
const items = ['<button class="tb-menu-item' + (_pvFilter.model === 'all' ? ' active' : '') + '" type="button" data-val="all">全部模特</button>'];
if (used.size === 0) {
items.push('<div class="tb-menu-empty">暂无批次,生成后可按模特筛选</div>');
} else {
used.forEach((nm, id) => {
items.push(`<button class="tb-menu-item${_pvFilter.model === id ? ' active' : ''}" type="button" data-val="${id}">${nm}</button>`);
});
}
menu.innerHTML = items.join('');
}
function _setChipLabel(chipId, baseLabel, val, valText) {
const lbl = document.querySelector('#' + chipId + ' .lbl');
const chip = document.getElementById(chipId);
if (!lbl || !chip) return;
if (val === 'all' || !val) {
lbl.textContent = baseLabel;
chip.classList.remove('active');
} else {
lbl.textContent = baseLabel;
chip.classList.add('active');
// 触发 ::after ':' 伪元素显示已选项
chip.title = baseLabel + ':' + valText;
}
}
function _closeAllMenus(except) {
document.querySelectorAll('.mp-main-h .tb-menu-wrap.open').forEach(w => {
if (w !== except) w.classList.remove('open');
});
}
// chip 点击 → 开/合菜单
document.querySelectorAll('.mp-main-h .tb-menu-wrap').forEach(wrap => {
const chip = wrap.querySelector('.tb-chip');
chip.addEventListener('click', e => {
e.stopPropagation();
const willOpen = !wrap.classList.contains('open');
_closeAllMenus(wrap);
wrap.classList.toggle('open', willOpen);
if (willOpen && wrap.dataset.filter === 'model') _refreshModelMenu();
});
});
// 菜单项 → 选中并应用
document.querySelectorAll('#mp-menu-time, #mp-menu-model').forEach(menu => {
menu.addEventListener('click', e => {
const btn = e.target.closest('.tb-menu-item');
if (!btn) return;
const val = btn.dataset.val;
const txt = btn.textContent.trim();
const wrap = menu.closest('.tb-menu-wrap');
const key = wrap.dataset.filter;
_pvFilter[key] = val;
menu.querySelectorAll('.tb-menu-item').forEach(it => it.classList.toggle('active', it === btn));
wrap.classList.remove('open');
const baseLabel = key === 'time' ? '时间' : '模特';
_setChipLabel(key === 'time' ? 'mp-chip-time' : 'mp-chip-model', baseLabel, val, txt);
applyPvFilters();
});
});
// 点击页面其它位置关闭菜单
document.addEventListener('click', e => {
if (!e.target.closest('.mp-main-h .tb-menu-wrap')) _closeAllMenus(null);
});
// 搜索 toggle + 输入
(function setupSearch() {
const wrap = document.getElementById('mp-search-wrap');
const toggle = document.getElementById('mp-search-toggle');
const input = document.getElementById('mp-search-input');
if (!wrap || !toggle || !input) return;
toggle.addEventListener('click', e => {
e.stopPropagation();
const willExpand = !wrap.classList.contains('expanded');
wrap.classList.toggle('expanded', willExpand);
if (willExpand) setTimeout(() => input.focus(), 50);
else {
input.value = '';
_pvFilter.search = '';
applyPvFilters();
}
});
input.addEventListener('input', () => {
_pvFilter.search = input.value;
applyPvFilters();
});
input.addEventListener('keydown', e => {
if (e.key === 'Escape') {
input.value = '';
_pvFilter.search = '';
wrap.classList.remove('expanded');
applyPvFilters();
}
});
})();
// ─── 模特库 全屏 ───
const MODELS = [
{ id: 'm1', name: 'Ava', gender: '女', age: '青年', style: '清新自然', source: 'preset', used: 12, region: '东亚', skin: '白皙', height: '中等', build: '标准', hairLen: '长发', hairColor: '黑', vibe: '温柔', feature: '邻家女孩气质,微笑亲和' },
{ id: 'm2', name: 'Luna', gender: '女', age: '青年', style: '学生少女', source: 'preset', used: 8, region: '东亚', skin: '白皙', height: '偏小', build: '纤细', hairLen: '中发', hairColor: '深棕', vibe: '甜美', feature: '校园风,书卷气重' },
{ id: 'm3', name: 'Mia', gender: '女', age: '青年', style: 'OL 通勤', source: 'preset', used: 5, region: '东亚', skin: '小麦', height: '中等', build: '标准', hairLen: '短发', hairColor: '黑', vibe: '干练', feature: '都市职场气场,锐利眼神' },
{ id: 'm4', name: 'Zoe', gender: '女', age: '青年', style: '健身运动', source: 'preset', used: 9, region: '东亚', skin: '健康', height: '偏高', build: '运动', hairLen: '中发', hairColor: '栗色', vibe: '活力', feature: '马尾辫,健身房常客' },
{ id: 'm5', name: 'Iris', gender: '女', age: '中年', style: '都市精致', source: 'preset', used: 3, region: '东亚', skin: '白皙', height: '中等', build: '标准', hairLen: '中发', hairColor: '酒红', vibe: '优雅', feature: '熟女气场,精致妆容' },
{ id: 'm6', name: 'Lily', gender: '女', age: '青年', style: '甜美韩系', source: 'preset', used: 7, region: '东亚', skin: '白皙', height: '偏小', build: '纤细', hairLen: '长发', hairColor: '浅棕', vibe: '甜美', feature: '韩系混血感,微卷长发' },
{ id: 'm7', name: 'Sora', gender: '女', age: '青年', style: '日系简约', source: 'preset', used: 6, region: '东亚', skin: '白皙', height: '中等', build: '纤细', hairLen: '短发', hairColor: '黑', vibe: '清冷', feature: '日系氛围感,齐刘海' },
{ id: 'm8', name: 'Eden', gender: '男', age: '青年', style: '商务通勤', source: 'preset', used: 4, region: '东亚', skin: '健康', height: '偏高', build: '标准', hairLen: '短发', hairColor: '黑', vibe: '稳重', feature: '商务精英范,西装常驻' },
{ id: 'm9', name: 'Kai', gender: '男', age: '青年', style: '街头潮流', source: 'preset', used: 5, region: '东亚', skin: '小麦', height: '中等', build: '运动', hairLen: '中发', hairColor: '亚麻', vibe: '潮酷', feature: '街头潮人,鼻钉耳骨钉' },
{ id: 'm10', name: 'Leo', gender: '男', age: '中年', style: '熟男品质', source: 'preset', used: 2, region: '东亚', skin: '健康', height: '偏高', build: '标准', hairLen: '短发', hairColor: '微银', vibe: '沉稳', feature: '熟男魅力,胡须利落' },
{ id: 'm11', name: 'YouA', gender: '女', age: '青年', style: '我的模特', source: 'own', used: 0, region: '—', skin: '—', height: '—', build: '—', hairLen: '—', hairColor: '—', vibe: '—', feature: '用户上传素材,未生成特征' },
{ id: 'm12', name: 'YouB', gender: '女', age: '青年', style: '我的模特', source: 'own', used: 0, region: '—', skin: '—', height: '—', build: '—', hairLen: '—', hairColor: '—', vibe: '—', feature: '用户上传素材,未生成特征' },
];
function renderModelLib(filter) {
const grid = document.getElementById('ml-grid');
let list = MODELS;
if (filter.source && filter.source !== 'all') list = list.filter(m => m.source === filter.source);
if (filter.gender) list = list.filter(m => m.gender === filter.gender);
if (filter.age) list = list.filter(m => m.age === filter.age);
// 「添加模特」入口卡 · 平台预设是只读素材库,不展示入口
const uploadCard = (filter.source === 'preset') ? '' : `
<div class="model-card ml-upload-card" id="ml-upload-card" role="button" tabindex="0" aria-label="上传或生成新模特">
<div class="m-thumb up-thumb">
<div class="up-plus">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
</div>
</div>
<div class="m-name">添加模特</div>
<div class="m-tag">// AI 生成 / 本地上传</div>
</div>
`;
grid.innerHTML = uploadCard + list.map(m => `
<div class="model-card${state.selectedModel === m.id ? ' selected' : ''}" data-id="${m.id}" data-name="${m.name}">
<div class="placeholder m-thumb"><span class="ph-frame">${m.name} · ${m.style}</span></div>
<div class="m-name">${m.name}</div>
<div class="m-tag">${m.gender}·${m.age}·${m.style}</div>
</div>
`).join('');
// 绑定 click (单选) · 排除上传卡片
grid.querySelectorAll('.model-card:not(.ml-upload-card)').forEach(card => {
card.addEventListener('click', e => {
if (e.target.closest('.m-thumb')) {
openModelDetail(card.dataset.id);
return;
}
selectModel(card.dataset.id);
});
});
// 上传卡点击 → 打开选择 modal
const upCard = grid.querySelector('#ml-upload-card');
if (upCard) {
upCard.addEventListener('click', () => openUploadChoice());
upCard.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openUploadChoice(); }
});
}
}
/* ─── 添加模特 · 工作台画布(取代原 modal) ─── */
const _uploadCanvas = document.getElementById('ml-canvas');
function openUploadChoice() {
_uploadCanvas.classList.add('show');
_uploadCanvas.setAttribute('aria-hidden', 'false');
}
function _closeUploadCanvasNow() {
_uploadCanvas.classList.remove('show');
_uploadCanvas.setAttribute('aria-hidden', 'true');
}
/* 工作台是否「脏」:右侧栏任一字段已有内容 · 直接读 _mcAi / _mcLocal */
function _isWorkbenchDirty() {
if (!_uploadCanvas.classList.contains('show')) return false;
if (typeof _mcAi !== 'undefined') {
if ((_mcAi.name || '').trim()) return true;
if (_mcAi.portrait) return true;
if (_mcAi.triVersions && _mcAi.triVersions.length > 0) return true;
}
if (typeof _mcLocal !== 'undefined') {
if ((_mcLocal.name || '').trim()) return true;
if (_mcLocal.portraits && _mcLocal.portraits.length > 0) return true;
if (_mcLocal.triVersions && _mcLocal.triVersions.length > 0) return true;
}
return false;
}
/* 工作台离开 · 重置右侧栏状态(供退出 / 跳转后清场) */
function _resetWorkbenchState() {
if (typeof _mcAi !== 'undefined') {
if (_mcAi.portrait?.cellEl) _mcAi.portrait.cellEl.classList.remove('selected');
_mcAi.name = ''; _mcAi.portrait = null;
_mcAi.triVersions = []; _mcAi.triActiveIdx = -1;
}
if (typeof _mcLocal !== 'undefined') {
_mcLocal.name = ''; _mcLocal.portraits = [];
_mcLocal.triVersions = []; _mcLocal.triActiveIdx = -1;
}
const nameInput = document.getElementById('mc-up-name');
if (nameInput) nameInput.value = '';
if (typeof _renderRight === 'function') { try { _renderRight(); } catch(e) {} }
}
/* 二次确认弹窗 · 通用 · onConfirm 在用户点击「不保存,退出」后执行 */
const _leaveBg = document.getElementById('mc-leave-bg');
const _leaveBody = document.getElementById('mc-leave-body');
const _leaveCancel = document.getElementById('mc-leave-cancel');
const _leaveConfirm = document.getElementById('mc-leave-confirm');
let _leavePending = null;
function _openLeaveConfirm(mode, onConfirm) {
if (mode === 'nav') {
_leaveBody.innerHTML = '工作台已有内容,跳转到其他页面后<b>不会保存</b>。可继续编辑并点「加入模特库」来保留进度。';
} else {
_leaveBody.innerHTML = '工作台已有内容,退出后<b>不会保存</b>。可继续编辑并点「加入模特库」来保留进度。';
}
_leavePending = onConfirm || null;
_leaveBg.classList.add('show');
_leaveBg.setAttribute('aria-hidden', 'false');
}
function _closeLeaveConfirm() {
_leaveBg.classList.remove('show');
_leaveBg.setAttribute('aria-hidden', 'true');
_leavePending = null;
}
_leaveCancel.addEventListener('click', _closeLeaveConfirm);
_leaveBg.addEventListener('click', e => { if (e.target === _leaveBg) _closeLeaveConfirm(); });
_leaveConfirm.addEventListener('click', () => {
const fn = _leavePending;
_closeLeaveConfirm();
if (typeof fn === 'function') fn();
});
/* 关闭工作台 · 脏态先二次确认 */
function closeUploadChoice() {
if (_isWorkbenchDirty()) {
_openLeaveConfirm('exit', () => {
_resetWorkbenchState();
_closeUploadCanvasNow();
});
return;
}
_closeUploadCanvasNow();
}
document.getElementById('ml-canvas-x')?.addEventListener('click', closeUploadChoice);
document.getElementById('ml-canvas-back').addEventListener('click', closeUploadChoice);
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && _uploadCanvas.classList.contains('show') && !_leaveBg.classList.contains('show')) {
closeUploadChoice();
}
});
/* 全局拦截外链跳转 + 模特库内的离场操作 · 仅工作台展开且脏态时 */
document.addEventListener('click', e => {
if (!_uploadCanvas.classList.contains('show')) return;
if (!_isWorkbenchDirty()) return;
// 1) 外页跳转(侧边栏 nav · 面包屑)
const a = e.target.closest('a[href]');
if (a) {
const href = a.getAttribute('href');
if (!href || href.startsWith('#') || href.startsWith('javascript:')) return;
e.preventDefault(); e.stopPropagation();
_openLeaveConfirm('nav', () => {
_resetWorkbenchState();
_closeUploadCanvasNow();
location.href = href;
});
return;
}
// 2) 余额胶囊(inline onclick=location.href='account.html')
if (e.target.closest('.balance-chip')) {
e.preventDefault(); e.stopPropagation();
_openLeaveConfirm('nav', () => {
_resetWorkbenchState();
_closeUploadCanvasNow();
location.href = 'account.html';
});
return;
}
// 3) 模特库 X · 关闭整个模特库 modal(等同退出工作台 + 返回主页)
if (e.target.closest('#ml-close-btn')) {
e.preventDefault(); e.stopPropagation();
_openLeaveConfirm('exit', () => {
_resetWorkbenchState();
_closeUploadCanvasNow();
document.getElementById('ml-modal-bg').classList.remove('show');
});
return;
}
// 4) 模特库左侧「来源」筛选项 · 切换筛选 = 离开工作台回到列表
const sideItem = e.target.closest('.ml-side .ml-side-item');
if (sideItem) {
e.preventDefault(); e.stopPropagation();
const src = sideItem.dataset.source;
_openLeaveConfirm('nav', () => {
_resetWorkbenchState();
_closeUploadCanvasNow();
// 应用筛选 + 同步 active
_libFilter.source = src;
document.querySelectorAll('.ml-side .ml-side-item').forEach(x =>
x.classList.toggle('active', x.dataset.source === src));
renderModelLib(_libFilter);
});
return;
}
}, true); // capture · 早于 inline onclick
/* 浏览器后退 / 刷新 / 关闭 · 原生 beforeunload 兜底 */
window.addEventListener('beforeunload', e => {
if (_isWorkbenchDirty()) { e.preventDefault(); e.returnValue = ''; return ''; }
});
/* ─── 工作台画布 · 左 AI 生成 区 ─── */
const _mcInputText = document.getElementById('mc-input-text');
const _mcSendBtn = document.getElementById('mc-send-btn');
_mcInputText?.addEventListener('input', () => {
_mcSendBtn.disabled = _mcInputText.value.trim().length === 0;
// textarea 自适应高度 · 与 .io-input 一致 max-height 220
_mcInputText.style.height = 'auto';
_mcInputText.style.height = Math.min(_mcInputText.scrollHeight, 220) + 'px';
});
const _MC_SVG = {
rerun: '<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-6.7L21 8M21 3v5h-5"/></svg>',
dl: '<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-4M7 10l5 5 5-5M12 15V3"/></svg>',
more: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></svg>',
adopt: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>',
del: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>',
edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>',
saveAll: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>',
};
function _mcAppendMsg(prompt, refs) {
const inner = document.getElementById('mc-stream-inner');
if (!inner) return;
// 首次发送 · 移除空态
const empty = inner.querySelector('.mc-empty');
if (empty) empty.remove();
const safe = String(prompt).replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'})[c]);
const tags = ['3:4', '默认', '4 张'];
const tagsHtml = tags.map(t => `<span class="meta-chip">${t}</span>`).join('<span class="sep">·</span>');
const cells = Array.from({ length: 4 }, (_, i) => `
<div class="mc-cell gen" data-idx="${i}">
<div class="ph-frame">生成中 · v${i + 1}</div>
<div class="cell-ops" hidden>
<button type="button" data-act="cell-rerun" title="再次生成">${_MC_SVG.rerun}</button>
<button type="button" data-act="cell-dl" title="下载">${_MC_SVG.dl}</button>
<div class="cell-more-wrap">
<button type="button" data-act="cell-more" title="更多">${_MC_SVG.more}</button>
<div class="cell-more-menu" role="menu">
<button type="button" data-act="cell-adopt">${_MC_SVG.adopt}<span>加入模特库</span></button>
<button type="button" class="danger" data-act="cell-del">${_MC_SVG.del}<span>删除</span></button>
</div>
</div>
</div>
</div>`).join('');
const msg = document.createElement('div');
msg.className = 'mc-msg';
msg.innerHTML = `
<div class="mc-msg-prompt">
<div class="quote">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21c0-3.5 3-6 6-6s6 2.5 6 6"/><circle cx="9" cy="8" r="4"/><path d="M19 8v6M22 11h-6"/></svg>
</div>
<div class="pt">
<div class="pt-text">${safe}</div>
<div class="pt-tags">${tagsHtml}</div>
</div>
</div>
<div class="mc-msg-grid">${cells}</div>
<div class="mc-msg-ops">
<button type="button" data-act="edit">${_MC_SVG.edit}重新编辑</button>
<button type="button" data-act="rerun">${_MC_SVG.rerun}再次生成</button>
<div class="msg-more-wrap">
<button type="button" class="icon" data-act="msg-more" title="更多">${_MC_SVG.more}</button>
<div class="msg-more-menu" role="menu">
<button type="button" class="danger" data-act="msg-del">${_MC_SVG.del}<span>删除该批结果</span></button>
</div>
</div>
</div>
`;
inner.appendChild(msg);
// 模拟生成完成 · 1.6s 后去掉 .gen 动画并显示版本号 + 显示 cell-ops + 绑定选中点击
setTimeout(() => {
msg.querySelectorAll('.mc-cell').forEach((c, i) => {
c.classList.remove('gen');
const ph = c.querySelector('.ph-frame');
if (ph) ph.textContent = '模特 · v' + (i + 1);
const ops = c.querySelector('.cell-ops');
if (ops) ops.hidden = false;
});
_bindMcCellPick();
}, 1600);
_mcBindMsgEvents(msg, prompt);
// 滚到底部
const stream = document.getElementById('mc-stream');
if (stream) stream.scrollTo({ top: stream.scrollHeight, behavior: 'smooth' });
}
function _mcBindMsgEvents(msg, prompt) {
// 单图 cell-rerun → 重置该 cell 为 gen, 1.2s 后回 ok
msg.querySelectorAll('[data-act="cell-rerun"]').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const cell = b.closest('.mc-cell');
const ops = cell.querySelector('.cell-ops');
const ph = cell.querySelector('.ph-frame');
const idx = Number(cell.dataset.idx || 0);
cell.classList.add('gen');
cell.classList.remove('selected');
if (ops) ops.hidden = true;
if (ph) ph.textContent = '生成中 · v' + (idx + 1);
setTimeout(() => {
cell.classList.remove('gen');
if (ph) ph.textContent = '模特 · v' + (idx + 1);
if (ops) ops.hidden = false;
}, 1200 + Math.random() * 600);
Shell.toast('已重跑', '该图重新生成中');
});
});
msg.querySelectorAll('[data-act="cell-dl"]').forEach(b => {
b.addEventListener('click', e => { e.stopPropagation(); Shell.toast('下载', '已开始下载 · MOCK'); });
});
// 单图更多 menu 开/合
msg.querySelectorAll('[data-act="cell-more"]').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const wrap = b.closest('.cell-more-wrap');
const willOpen = !wrap.classList.contains('open');
document.querySelectorAll('.mc-cell .cell-more-wrap.open').forEach(w => w.classList.remove('open'));
if (willOpen) wrap.classList.add('open');
});
});
msg.querySelectorAll('[data-act="cell-adopt"]').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const wrap = b.closest('.cell-more-wrap');
if (wrap) wrap.classList.remove('open');
Shell.toast('已加入模特库', '已添加到「我的上传」');
});
});
msg.querySelectorAll('[data-act="cell-del"]').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const wrap = b.closest('.cell-more-wrap');
if (wrap) wrap.classList.remove('open');
const cell = b.closest('.mc-cell');
cell.remove();
if (!msg.querySelectorAll('.mc-cell').length) msg.remove();
Shell.toast('已删除');
});
});
// 批次操作
msg.querySelectorAll('[data-act="rerun"]').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
_mcAppendMsg(prompt, []);
});
});
msg.querySelectorAll('[data-act="edit"]').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const inp = document.getElementById('mc-input-text');
if (inp) {
inp.value = prompt;
inp.focus();
inp.dispatchEvent(new Event('input'));
}
});
});
// 批次更多 menu
msg.querySelectorAll('[data-act="msg-more"]').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const wrap = b.closest('.msg-more-wrap');
const willOpen = !wrap.classList.contains('open');
document.querySelectorAll('.mc-msg-ops .msg-more-wrap.open').forEach(w => w.classList.remove('open'));
if (willOpen) wrap.classList.add('open');
});
});
msg.querySelectorAll('[data-act="msg-del"]').forEach(b => {
b.addEventListener('click', e => {
e.stopPropagation();
const wrap = b.closest('.msg-more-wrap');
if (wrap) wrap.classList.remove('open');
msg.remove();
Shell.toast('已删除该批结果');
});
});
}
// 全局点击 → 关闭 mc 单图/批次 more menu
document.addEventListener('click', e => {
if (!e.target.closest('.mc-cell .cell-more-wrap')) {
document.querySelectorAll('.mc-cell .cell-more-wrap.open').forEach(w => w.classList.remove('open'));
}
if (!e.target.closest('.mc-msg-ops .msg-more-wrap')) {
document.querySelectorAll('.mc-msg-ops .msg-more-wrap.open').forEach(w => w.classList.remove('open'));
}
});
_mcSendBtn?.addEventListener('click', () => {
const txt = _mcInputText.value.trim();
if (!txt) return;
_mcAppendMsg(txt, _mcRefList.slice());
_mcInputText.value = '';
_mcInputText.style.height = 'auto';
_mcSendBtn.disabled = true;
// 清空参考图(image-optimize 同款行为)
_mcRefList = [];
_renderMcRefs();
});
// Cmd/Ctrl + Enter 发送
_mcInputText?.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault();
_mcSendBtn.click();
}
});
// 示例 chip → 填入 textarea
document.querySelectorAll('.mc-empty .examples .ex').forEach(b => {
b.addEventListener('click', () => {
_mcInputText.value = b.dataset.ex || b.textContent.trim();
_mcInputText.dispatchEvent(new Event('input'));
_mcInputText.focus();
});
});
// + 按钮上传参考图(仅作展示参考,不入库)
const _mcAiRefInput = document.getElementById('mc-ai-ref-input');
const _mcRefs = document.getElementById('mc-input-refs');
let _mcRefList = [];
document.getElementById('mc-add-btn')?.addEventListener('click', () => _mcAiRefInput.click());
_mcAiRefInput?.addEventListener('change', e => {
const files = [...(e.target.files || [])].filter(f => /^image\//.test(f.type));
files.forEach(f => _mcRefList.push({ name: f.name, url: URL.createObjectURL(f) }));
e.target.value = '';
_renderMcRefs();
});
function _renderMcRefs() {
if (!_mcRefs) return;
_mcRefs.classList.toggle('show', _mcRefList.length > 0);
_mcRefs.innerHTML = _mcRefList.map((r, i) => `
<div class="mc-input-ref">
<img src="${r.url}" alt="${r.name}">
<button class="x" data-idx="${i}" aria-label="移除"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button>
</div>`).join('');
_mcRefs.querySelectorAll('.x').forEach(x => x.addEventListener('click', () => {
_mcRefList.splice(+x.dataset.idx, 1);
_renderMcRefs();
}));
}
/* ─── 工作台画布 · 右栏 · tab 切换 + 3 模块状态机 ─── */
let _mcRightTab = 'ai';
const _mcAi = { name: '', portrait: null, triVersions: [], triActiveIdx: -1 }; // portrait: { label, cellEl }
const _mcLocal = { name: '', portraits: [], triVersions: [], triActiveIdx: -1 }; // portraits: [{file,url,name,size}]
function _mcCurState() { return _mcRightTab === 'ai' ? _mcAi : _mcLocal; }
function _hasTri(s) { return s.triVersions.length > 0 && s.triActiveIdx >= 0; }
// Tab 切换
document.querySelectorAll('.mc-up-tab').forEach(btn => {
btn.addEventListener('click', () => {
_mcRightTab = btn.dataset.tab;
document.querySelectorAll('.mc-up-tab').forEach(b => b.classList.toggle('active', b === btn));
document.querySelectorAll('.mc-up-body [data-show]').forEach(el => {
el.hidden = el.dataset.show !== _mcRightTab;
});
document.getElementById('mc-up-name').value = _mcCurState().name;
_renderRight();
});
});
// 模特姓名 输入(按当前 tab 写到对应 state)
const _mcUpName = document.getElementById('mc-up-name');
_mcUpName?.addEventListener('input', () => {
_mcCurState().name = _mcUpName.value;
_updateCommit();
});
/* ─── AI tab:立绘选中态 ─── */
const _aiEmpty = document.getElementById('mc-portrait-ai-empty');
const _aiPicked = document.getElementById('mc-portrait-ai-picked');
const _aiLabel = document.getElementById('mc-portrait-ai-label');
function _renderAiPortrait() {
if (_mcAi.portrait) {
_aiEmpty.hidden = true;
_aiPicked.hidden = false;
_aiLabel.textContent = _mcAi.portrait.label || '模特立绘';
} else {
_aiEmpty.hidden = false;
_aiPicked.hidden = true;
}
}
function _setAiPortrait(data) {
_mcAi.portrait = data;
_renderAiPortrait();
_updateTriBtn(); _updateCommit();
}
function _clearAiPortrait() {
if (_mcAi.portrait?.cellEl) _mcAi.portrait.cellEl.classList.remove('selected');
_mcAi.portrait = null;
// 立绘清空 → 三视图历史作废
_mcAi.triVersions = []; _mcAi.triActiveIdx = -1;
_renderAiPortrait();
_renderTriView();
_updateTriBtn(); _updateCommit();
}
document.getElementById('mc-portrait-ai-clear').addEventListener('click', _clearAiPortrait);
/* ─── Local tab:多张立绘 ─── */
const _lpDrop = document.getElementById('mc-portrait-local-drop');
const _lpInput = document.getElementById('mc-portrait-local-input');
const _lpList = document.getElementById('mc-portrait-local-list');
const _lpCount = document.getElementById('mc-portrait-local-count');
function _renderLocalPortraits() {
_lpCount.textContent = _mcLocal.portraits.length;
_lpList.innerHTML = _mcLocal.portraits.map((p, i) => `
<div class="thumb">
<img src="${p.url}" alt="${p.name}">
<button class="x" data-idx="${i}" aria-label="移除"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button>
</div>
`).join('');
_lpList.querySelectorAll('.x').forEach(x => x.addEventListener('click', () => {
_mcLocal.portraits.splice(+x.dataset.idx, 1);
if (_mcLocal.portraits.length === 0) {
// 立绘全部清空 → 三视图历史作废
_mcLocal.triVersions = []; _mcLocal.triActiveIdx = -1;
}
_renderLocalPortraits();
_renderTriView();
_updateTriBtn(); _updateCommit();
}));
}
function _lpAdd(files) {
const imgs = [...(files || [])].filter(f => /^image\//.test(f.type));
imgs.forEach(f => _mcLocal.portraits.push({ file: f, url: URL.createObjectURL(f), name: f.name, size: f.size }));
_renderLocalPortraits();
_updateTriBtn(); _updateCommit();
}
_lpDrop?.addEventListener('click', () => _lpInput.click());
_lpDrop?.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _lpInput.click(); } });
_lpInput?.addEventListener('change', e => { _lpAdd(e.target.files); e.target.value = ''; });
['dragenter', 'dragover'].forEach(ev => _lpDrop?.addEventListener(ev, e => { e.preventDefault(); _lpDrop.classList.add('dragover'); }));
['dragleave', 'drop'].forEach(ev => _lpDrop?.addEventListener(ev, e => { e.preventDefault(); _lpDrop.classList.remove('dragover'); }));
_lpDrop?.addEventListener('drop', e => _lpAdd(e.dataTransfer?.files));
/* ─── 三视图模块 · 16:9 占位常驻 · 空态居中覆盖按钮 · 多版本累积可切换 ─── */
const _triSec = document.getElementById('mc-triview-sec');
const _triResultImg = document.getElementById('mc-triview-result');
const _triFrame = document.getElementById('mc-triview-frame');
const _triGenBtn = document.getElementById('mc-triview-gen-btn');
const _triHint = document.getElementById('mc-triview-hint');
const _triOps = document.getElementById('mc-triview-ops');
const _triRerunBtn = document.getElementById('mc-triview-rerun');
const _triHistory = document.getElementById('mc-triview-history');
const _triHistoryRow = document.getElementById('mc-triview-history-row');
const _triHistoryCount = document.getElementById('mc-triview-history-count');
let _triGenerating = false;
function _portraitReady() {
return _mcRightTab === 'ai' ? !!_mcAi.portrait : _mcLocal.portraits.length > 0;
}
function _renderTriView() {
const s = _mcCurState();
const has = _hasTri(s);
_triSec.classList.toggle('has-result', has);
if (_triGenerating) {
// 生成中(首次或重跑):主图脉冲 · 隐藏覆盖按钮 · 历史区保留(若已有版本) · 重跑按钮禁用
_triGenBtn.hidden = true;
_triHint.hidden = true;
_triOps.hidden = !has;
_triHistory.hidden = !has;
_triFrame.textContent = '三视图生成中…';
_triResultImg.classList.add('gen');
_triRerunBtn.disabled = true;
if (has) _renderTriHistory(s);
} else if (has) {
_triGenBtn.hidden = true;
_triHint.hidden = true;
_triOps.hidden = false;
_triHistory.hidden = false;
const ver = s.triVersions[s.triActiveIdx];
_triFrame.textContent = `三视图(正/侧/背) · ${ver.label}`;
_triResultImg.classList.remove('gen');
_triRerunBtn.disabled = false;
_renderTriHistory(s);
} else {
_triGenBtn.hidden = false;
_triHint.hidden = false;
_triOps.hidden = true;
_triHistory.hidden = true;
_triFrame.textContent = '三视图(正/侧/背)';
_triResultImg.classList.remove('gen');
}
}
function _renderTriHistory(s) {
_triHistoryCount.textContent = s.triVersions.length;
_triHistoryRow.innerHTML = s.triVersions.map((ver, i) => `
<div class="h-thumb${i === s.triActiveIdx ? ' active' : ''}" data-idx="${i}" title="${ver.label} · ${ver.ts}${i === s.triActiveIdx ? ' · 当前采用' : ''}">
<span class="badge">当前</span>
<span class="v">${ver.label}</span>
</div>
`).join('');
_triHistoryRow.querySelectorAll('.h-thumb').forEach(el => {
el.addEventListener('click', () => {
const idx = Number(el.dataset.idx);
if (idx === s.triActiveIdx) return;
s.triActiveIdx = idx;
_renderTriView(); _updateCommit();
});
});
}
function _updateTriBtn() {
if (_hasTri(_mcCurState())) return; // 已有版本,按钮被 _renderTriView 隐藏
const ok = _portraitReady() && !_triGenerating;
_triGenBtn.disabled = !ok;
_triHint.textContent = _portraitReady()
? '// 一键生成正/侧/背 三视图'
: (_mcRightTab === 'ai' ? '// 先选中左侧 AI 立绘' : '// 先上传至少 1 张立绘');
}
function _startTriGen() {
if (!_portraitReady() || _triGenerating) return;
_triGenerating = true;
const stateAtStart = _mcCurState();
_renderTriView();
setTimeout(() => {
_triGenerating = false;
const now = new Date();
const ts = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
const label = 'v' + (stateAtStart.triVersions.length + 1);
stateAtStart.triVersions.push({ ts, label });
stateAtStart.triActiveIdx = stateAtStart.triVersions.length - 1;
if (stateAtStart === _mcCurState()) _renderTriView();
_updateTriBtn(); _updateCommit();
}, 1600);
}
_triGenBtn.addEventListener('click', _startTriGen);
_triRerunBtn.addEventListener('click', _startTriGen);
/* ─── 整体渲染 + 提交 ─── */
const _mcUpCommit = document.getElementById('mc-up-commit');
const _mcUpStat = document.getElementById('mc-up-stat');
function _updateCommit() {
const s = _mcCurState();
const nameOk = s.name.trim().length > 0;
const portraitOk = _portraitReady();
const triOk = _hasTri(s);
// 三视图改为「可选」: 姓名 + 立绘 即可保存
const ready = nameOk && portraitOk;
_mcUpCommit.disabled = !ready;
if (ready) {
_mcUpStat.classList.add('ok');
if (triOk) {
_mcUpStat.innerHTML = `✓ 已就绪 · <b>${s.name}</b>`;
} else {
_mcUpStat.innerHTML = `✓ 可保存 · <b>${s.name}</b> · <span style="color:var(--black-alpha-56)">建议补三视图</span>`;
}
} else {
_mcUpStat.classList.remove('ok');
const miss = [];
if (!nameOk) miss.push('姓名');
if (!portraitOk) miss.push('立绘');
_mcUpStat.innerHTML = `// 待完成 · <b>${miss.join(' / ')}</b>`;
}
}
function _renderRight() {
_renderAiPortrait();
_renderLocalPortraits();
_renderTriView();
_updateTriBtn();
_updateCommit();
}
_renderRight();
function _doMcCommit() {
const s = _mcCurState();
const baseName = s.name.trim().slice(0, 12) || 'YouNew';
const ts = Date.now().toString(36);
// 持久化:多张立绘(AI tab 1 张 label · Local tab 真实 blob URL N 张)+ 三视图历史版本(可为空)
const portraits = _mcRightTab === 'ai'
? (_mcAi.portrait ? [{ url: '', name: _mcAi.portrait.label || baseName, label: _mcAi.portrait.label || '立绘' }] : [])
: _mcLocal.portraits.map((p, i) => ({ url: p.url, name: p.name || `${baseName}-${i+1}`, label: `本地 ${i+1}` }));
const triVersions = s.triVersions.map(v => ({ ts: v.ts, label: v.label }));
const hasTri = triVersions.length > 0;
MODELS.unshift({
id: 'm-up-' + ts,
name: baseName,
gender: '女', age: '青年', style: _mcRightTab === 'ai' ? 'AI 生成' : '我的模特',
source: 'own', used: 0,
region: '—', skin: '—', height: '—', build: '—',
hairLen: '—', hairColor: '—', vibe: '—',
feature: _mcRightTab === 'ai'
? (hasTri ? 'AI 生成 · 已含三视图' : 'AI 生成 · 缺三视图')
: (hasTri ? '用户上传 · 已含三视图' : '用户上传 · 缺三视图'),
triview: hasTri ? 1 : 0,
portraits, triVersions,
});
// 重置状态
if (_mcRightTab === 'ai') {
if (_mcAi.portrait?.cellEl) _mcAi.portrait.cellEl.classList.remove('selected');
_mcAi.name = ''; _mcAi.portrait = null;
_mcAi.triVersions = []; _mcAi.triActiveIdx = -1;
} else {
_mcLocal.name = ''; _mcLocal.portraits = [];
_mcLocal.triVersions = []; _mcLocal.triActiveIdx = -1;
}
_mcUpName.value = '';
_renderRight();
_libFilter.source = 'own';
document.querySelectorAll('.ml-side .ml-side-item').forEach(x =>
x.classList.toggle('active', x.dataset.source === 'own'));
renderModelLib(_libFilter);
_resetWorkbenchState();
_closeUploadCanvasNow();
Shell.toast(
hasTri ? '已加入模特库' : '已保存 · 缺三视图',
hasTri
? `${baseName} · 来源 ${_mcRightTab === 'ai' ? 'AI 生成' : '我的上传'}`
: `${baseName} · 后续可在资产详情页补三视图`
);
}
// 缺三视图 · 确认弹窗控制
const _notriBg = document.getElementById('mc-notri-bg');
const _notriSaveBtn = document.getElementById('mc-notri-save');
const _notriGenBtn = document.getElementById('mc-notri-gen');
function _openNotriConfirm() { _notriBg.classList.add('show'); _notriBg.setAttribute('aria-hidden', 'false'); }
function _closeNotriConfirm() { _notriBg.classList.remove('show'); _notriBg.setAttribute('aria-hidden', 'true'); }
_notriBg.addEventListener('click', e => { if (e.target === _notriBg) _closeNotriConfirm(); });
_notriSaveBtn.addEventListener('click', () => { _closeNotriConfirm(); _doMcCommit(); });
_notriGenBtn.addEventListener('click', () => {
_closeNotriConfirm();
// 滚动到三视图区域并触发生成
_triSec?.scrollIntoView({ behavior: 'smooth', block: 'center' });
if (!_triGenBtn.disabled) setTimeout(() => _triGenBtn.click(), 200);
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && _notriBg.classList.contains('show')) _closeNotriConfirm();
});
_mcUpCommit?.addEventListener('click', () => {
if (_mcUpCommit.disabled) return;
const s = _mcCurState();
if (!_hasTri(s)) {
_openNotriConfirm();
return;
}
_doMcCommit();
});
/* ─── 左侧 .mc-cell 可点击选为立绘(仅 AI tab) ─── */
function _bindMcCellPick() {
document.querySelectorAll('#mc-stream .mc-cell').forEach(cell => {
if (cell.dataset.boundPick) return;
cell.dataset.boundPick = '1';
// 添加「已选用」徽标
if (!cell.querySelector('.pick-badge')) {
const b = document.createElement('span');
b.className = 'pick-badge';
b.textContent = '已选用';
cell.appendChild(b);
}
cell.addEventListener('click', () => {
if (_mcRightTab !== 'ai') {
Shell.toast('请切到「AI 生成」标签', '只有 AI 模式才能选用左侧立绘');
return;
}
if (cell.classList.contains('gen')) return;
// 取消选中
if (cell.classList.contains('selected')) {
_clearAiPortrait();
return;
}
// 切换 · 清除上一张选中
document.querySelectorAll('#mc-stream .mc-cell.selected').forEach(c => c.classList.remove('selected'));
cell.classList.add('selected');
const label = cell.querySelector('.ph-frame')?.textContent || '模特立绘';
// 重置三视图(换了立绘要重新生成)
_mcAi.triVersions = []; _mcAi.triActiveIdx = -1;
_renderTriView();
_setAiPortrait({ label, cellEl: cell });
});
});
}
/* 兼容旧 modal:#ml-up-file 仍存在,但当前未使用(保留以防外部调用) */
const _uploadFileInput = document.getElementById('ml-up-file');
let _libFilter = { source: 'all', gender: '', age: '' };
document.getElementById('open-model-lib').addEventListener('click', () => {
renderModelLib(_libFilter);
document.getElementById('ml-modal-bg').classList.add('show');
});
document.getElementById('ml-close-btn').addEventListener('click', () => {
document.getElementById('ml-modal-bg').classList.remove('show');
});
// 左侧来源 click
document.querySelectorAll('.ml-side .ml-side-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.ml-side-item[data-source]').forEach(x => x.classList.remove('active'));
item.classList.add('active');
_libFilter.source = item.dataset.source;
renderModelLib(_libFilter);
});
});
// 顶部 chip 性别/年龄 click
document.querySelectorAll('.ml-toolbar .chip-group').forEach(group => {
group.addEventListener('click', e => {
const chip = e.target.closest('.chip');
if (!chip) return;
group.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
const k = group.dataset.key;
_libFilter[k] = chip.dataset.val;
renderModelLib(_libFilter);
});
});
// ─── 模特详情 居中弹窗 ───
let _detailModelId = null;
function _mdHash(s) { let h = 0; for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0; return Math.abs(h); }
function openModelDetail(id) {
const m = MODELS.find(x => x.id === id);
if (!m) return;
_detailModelId = id;
const sourceLabel = m.source === 'preset' ? '平台预设' : '我的上传';
const seed = _mdHash(m.name);
const assetId = 'ASSET-20240520-M' + String(seed % 1000).padStart(3, '0');
const fileSize = (4 + (seed % 100) / 10).toFixed(1) + 'MB';
const fav = String(8 + seed % 80);
const dlN = 200 + seed % 1800;
const dl = dlN >= 1000 ? (dlN / 1000).toFixed(1) + 'k' : String(dlN);
const intro = m.feature || (m.name + ' · ' + m.style + '。' + sourceLabel + ' 模特,可作为商品宣传场景的人物资产。');
const tags = [m.vibe, m.style, m.age, m.hairLen, m.region].filter(Boolean);
const props = [
['性别', m.gender], ['种族', m.region], ['作品ID', assetId],
['年龄段', m.age], ['气质', m.vibe], ['创作人', '流·Studio'],
['身高', m.height], ['体格', m.build], ['文件大小', fileSize],
['发型', m.hairLen + ' · ' + m.hairColor], ['来源', sourceLabel], ['发布时间', '2024-05-20'],
];
document.getElementById('md-title').textContent = m.name;
document.getElementById('md-kind').textContent = '/ 人物 · ' + sourceLabel;
document.getElementById('md-portrait-name').textContent = m.name + ' · ' + m.style;
// 当前主立绘 / 三视图 src 缓存,供 zoom 按钮点击使用
let _mdCurLeadSrc = null;
let _mdCurLeadName = '';
let _mdCurTriLabel = '';
// 主立绘 · 移除上次的 <img>(若有),按当前选中重渲
const leadImgEl = document.querySelector('#md-modal-bg .md-lead-img');
function _mdSetLead(p) {
if (!leadImgEl) return;
const old = leadImgEl.querySelector('img.md-lead-pic');
if (old) old.remove();
if (p && p.url) {
const img = document.createElement('img');
img.className = 'md-lead-pic';
img.src = p.url; img.alt = p.name || m.name;
leadImgEl.insertBefore(img, leadImgEl.firstChild);
_mdCurLeadSrc = p.url; _mdCurLeadName = p.name || m.name;
} else {
_mdCurLeadSrc = null; _mdCurLeadName = p?.label || m.name;
}
}
// 缩略图 strip · 用户上传且有 portraits 数组 → 显示多张;平台预设 → 仅 1 张占位
const userPortraits = (m.source === 'own' && Array.isArray(m.portraits) && m.portraits.length > 0) ? m.portraits : null;
const thumbList = userPortraits || [{ label: '立绘' }];
const thumbsEl = document.getElementById('md-thumbs');
thumbsEl.innerHTML = thumbList.map((p, i) => {
const inner = p.url
? `<img src="${p.url}" alt="${(p.name||'').replace(/"/g,'&quot;')}">`
: `<span class="ph-frame">${p.label || ('v'+(i+1))}</span>`;
return `<div class="thumb${i === 0 ? ' active' : ''}" data-idx="${i}">${inner}</div>`;
}).join('');
_mdSetLead(thumbList[0]);
thumbsEl.querySelectorAll('.thumb').forEach(t => t.addEventListener('click', () => {
thumbsEl.querySelectorAll('.thumb').forEach(x => x.classList.remove('active'));
t.classList.add('active');
_mdSetLead(thumbList[+t.dataset.idx]);
}));
// 三视图 · 用户上传且有 triVersions → 保留版本,可切换;否则单张占位
const userTri = (m.source === 'own' && Array.isArray(m.triVersions) && m.triVersions.length > 0) ? m.triVersions : null;
const viewsEl = document.querySelector('#md-modal-bg .md-views');
const _zoomSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5"/></svg>';
if (viewsEl) {
if (userTri) {
const stripHtml = userTri.map((v, i) => `<div class="v-thumb${i === userTri.length - 1 ? ' active' : ''}" data-idx="${i}"><span class="v">${v.label}</span></div>`).join('');
const cur = userTri[userTri.length - 1];
_mdCurTriLabel = cur.label;
viewsEl.innerHTML = `
<div class="md-view"><div class="lbl">正 / 侧 / 背 · ${cur.label} · ${cur.ts}</div><button class="md-zoom-btn" type="button" id="md-tri-zoom" aria-label="查看大图" title="查看大图">${_zoomSvg}</button></div>
<div class="md-view-versions">${stripHtml}</div>`;
const mainView = viewsEl.querySelector('.md-view .lbl');
viewsEl.querySelectorAll('.v-thumb').forEach(t => t.addEventListener('click', () => {
viewsEl.querySelectorAll('.v-thumb').forEach(x => x.classList.remove('active'));
t.classList.add('active');
const v = userTri[+t.dataset.idx];
_mdCurTriLabel = v.label;
if (mainView) mainView.textContent = `正 / 侧 / 背 · ${v.label} · ${v.ts}`;
}));
} else {
_mdCurTriLabel = '三视图';
viewsEl.innerHTML = `<div class="md-view"><div class="lbl">正 / 侧 / 背 · 三视图</div><button class="md-zoom-btn" type="button" id="md-tri-zoom" aria-label="查看大图" title="查看大图">${_zoomSvg}</button></div>`;
}
const triZoom = viewsEl.querySelector('#md-tri-zoom');
triZoom?.addEventListener('click', e => {
e.stopPropagation();
// 三视图当前仅占位渲染,无真实图 src:用占位 + 名字提示
if (window.Shell?._openLightbox) Shell._openLightbox('', m.name + ' · 三视图 · ' + _mdCurTriLabel);
});
}
// 主立绘 zoom 按钮点击 → 打开 lightbox(无 src 时显示占位名)
document.getElementById('md-lead-zoom')?.replaceWith(document.getElementById('md-lead-zoom').cloneNode(true));
document.getElementById('md-lead-zoom')?.addEventListener('click', e => {
e.stopPropagation();
if (window.Shell?._openLightbox) Shell._openLightbox(_mdCurLeadSrc || '', _mdCurLeadName || m.name);
});
// 简介 + 标签
document.getElementById('md-intro').textContent = intro;
document.getElementById('md-tags').innerHTML = tags.map(t => `<span class="tag-chip">${t}</span>`).join('') +
`<button class="tag-add" type="button" title="添加标签"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg></button>`;
// 属性表
document.getElementById('md-props').innerHTML = props.map(([k, v]) => `<div class="prop"><span class="k">${k}</span><span class="v">${v}</span></div>`).join('');
document.getElementById('md-modal-bg').classList.add('show');
}
document.getElementById('md-close-btn').addEventListener('click', () => {
document.getElementById('md-modal-bg').classList.remove('show');
});
document.getElementById('md-select').addEventListener('click', () => {
let pickedName = '';
if (_detailModelId) {
state.selectedModel = _detailModelId;
// 重建 mini 网格 · 让选中的模特(无论是否预设)显示在首位
renderModelMini();
// 同步模特库 modal 网格里的 .selected 类
document.querySelectorAll('.ml-grid .model-card').forEach(c =>
c.classList.toggle('selected', c.dataset.id === state.selectedModel)
);
updateModelSummary();
const m = MODELS.find(x => x.id === _detailModelId);
if (m) pickedName = m.name;
}
// 关闭模特详情 + 模特库两个 modal,回到工作台主视图
document.getElementById('md-modal-bg').classList.remove('show');
document.getElementById('ml-modal-bg').classList.remove('show');
// 工作台主区域 scroll 到「选择模特」step,可视化反馈
document.getElementById('model-grid-mini')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
if (pickedName && window.Shell?.toast) {
Shell.toast('已选用模特「' + pickedName + '」', '可继续选择服装与场景');
}
});
// pv-swap 也打开模特库
document.getElementById('pv-swap').addEventListener('click', () => {
document.getElementById('open-model-lib').click();
});
// URL ?product=商品名 → 替换默认选中(从 products.html 跳过来时携带)
(function applyUrlProduct() {
const q = new URLSearchParams(location.search);
const productName = q.get('product');
if (!productName) return;
let p = PRODUCTS.find(x => x.name === productName);
if (!p) {
p = { id: 'np-' + Date.now().toString(36), name: productName, cat: '美妆个护', meta: '新建 · 待补充' };
PRODUCTS.unshift(p);
}
state.selectedProd = p.id;
})();
/* ============================================================
生成批次 (localStorage) · 按当前商品过滤 · 立即生成时追加
============================================================ */
(function () {
'use strict';
const TASK_TYPE = 'model';
const KEY = 'fs-image-tasks-' + TASK_TYPE;
let tasks = [];
function load() {
try { return JSON.parse(localStorage.getItem(KEY) || '[]'); } catch (e) { return []; }
}
function save(arr) {
try { localStorage.setItem(KEY, JSON.stringify(arr)); } catch (e) {}
}
function buildSnapshot() {
return {
selectedProd: state.selectedProd,
selectedModel: state.selectedModel,
count: state.count,
ratio: state.ratio,
};
}
function timeNow() {
const d = new Date();
return ('0'+(d.getMonth()+1)).slice(-2) + '.' + ('0'+d.getDate()).slice(-2) + ' ' + ('0'+d.getHours()).slice(-2) + ':' + ('0'+d.getMinutes()).slice(-2);
}
// 暴露给上层 (给 cur-prod header 用)
window._countBatchesForProd = function (prodId) {
return tasks.filter(t => t.snap && t.snap.selectedProd === prodId).length;
};
// 切换商品 → 清空 pv-grid (历史批次只在当前 session 持有)
// cur-prod header 的"本商品 N 批"由 _countBatchesForProd 提供累计数
window.renderBatchesForCurrentProd = function () {
const grid = document.getElementById('pv-grid');
if (!grid) return;
grid.innerHTML = '';
_batchSeq = 0;
showPreviewEmpty();
};
// 立即生成 → push 新批次 + persist + 刷新视图
document.getElementById('mp-go-btn').addEventListener('click', () => {
if (!state.selectedModel || !state.selectedProd) return;
const snap = buildSnapshot();
const _prod = PRODUCTS.find(p => p.id === state.selectedProd);
const _model = MODELS.find(m => m.id === state.selectedModel);
const _name = ((_prod && _prod.name) || '商品') + ' × ' + ((_model && _model.name) || '模特');
const task = {
id: 'task-' + Date.now(),
type: TASK_TYPE,
name: _name,
snap,
status: 'ok',
time: timeNow(),
createdAt: Date.now(),
};
tasks.push(task);
save(tasks);
// 不重渲整个 grid (保留 hover 重跑/采用的实时态),只更新 cur-prod 计数
updateCurProdHeader();
});
/* ---------- 初始化 ---------- */
tasks = load();
})();
// 初始化
renderProdSpace();
renderSelectedProds();
renderModelMini(); // MODELS 已声明,可安全调用
updateModelSummary();
updateCost();
showPreviewEmpty(); // 默认 → 右侧显示空态
// 默认选中: URL ?product= 优先, 否则选 PRODUCTS 首位 (= 最近编辑)
const defaultProdId = state.selectedProd || (PRODUCTS[0] && PRODUCTS[0].id);
if (defaultProdId) selectProduct(defaultProdId);
</script>
</body>
</html>