All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
4752 lines
207 KiB
HTML
4752 lines
207 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>模特上身图 · 流·Studio</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
|
||
<style>
|
||
/* 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, '"')}">
|
||
<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, '"')}">
|
||
<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 => ({'&':'&','<':'<','>':'>'})[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,'"')}">`
|
||
: `<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>
|