All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
- model-photo / platform-cover · 头部 toolbar 落地: 时间 / 模特(平台) chip 下拉 + 折叠搜索 - model-photo / platform-cover · 图片卡片样式同步图片创作 (.io-cell): bg / hover / .gen 脉冲 / .err 红框 - model-photo / platform-cover · 单图 hover overlay: 再次生成 + 下载 + 更多(加入资产库/删除) - model-photo / platform-cover · 批次底栏: 再次生成图标统一 + 更多 menu(全部加入资产库/删除该批) - model-photo · 修 TDZ bug: renderModelMini 调用挪到 MODELS 声明后, 解决整页崩溃 - model-photo · 去掉冗余 pv-summary, 商品自动选最近编辑, task 写入 name 字段 - image-optimize · 单图右上加再次生成图标, 加入 fs-image-tasks-image 与任务中心打通 - image-optimize · 输入区拆 3 行: + 在顶 / textarea 满宽 / 发送在底栏右; 参考图缩略与加号同 64×64 - asset-factory · 任务中心加时间 chip + image 类型 + 跳转表; 删冗余类型列 - pipeline · stage2 商品卡换商品库风格 + AI 生成三视图主 CTA + .tri-missing-badge[hidden] CSS 修复
3552 lines
152 KiB
HTML
3552 lines
152 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 操作组 (再次生成 + 下载 + 更多) */
|
||
.mp-result .cell-ops {
|
||
position: absolute; top: 6px; right: 6px;
|
||
display: flex; gap: 4px;
|
||
opacity: 0;
|
||
transition: opacity var(--t-base);
|
||
z-index: 2;
|
||
}
|
||
.mp-result:hover .cell-ops { opacity: 1; }
|
||
.mp-result .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);
|
||
}
|
||
.mp-result .cell-ops button:hover { border-color: var(--heat); color: var(--heat); }
|
||
.mp-result .cell-ops button svg { width: 12px; height: 12px; }
|
||
/* 更多 · 下拉气泡 */
|
||
.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; }
|
||
.mp-pv-batch .pill-btn {
|
||
height: 34px;
|
||
padding: 0 18px;
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--black-alpha-12);
|
||
border-radius: var(--r-pill);
|
||
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 { background: var(--background-lighter); border-color: var(--black-alpha-24); }
|
||
.mp-pv-batch .pill-btn.primary {
|
||
background: var(--heat); color: #fff; border-color: var(--heat);
|
||
}
|
||
.mp-pv-batch .pill-btn.primary:hover { filter: brightness(.94); }
|
||
.mp-pv-batch .pill-btn svg { width: 13px; height: 13px; }
|
||
.mp-pv-batch .pill-btn.icon { width: 34px; 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 内容区结构 ── */
|
||
.mc-stream {
|
||
flex: 1; min-height: 0;
|
||
overflow-y: auto;
|
||
padding: 28px 28px 200px;
|
||
}
|
||
.mc-stream-inner {
|
||
max-width: 720px; margin: 0 auto;
|
||
display: flex; flex-direction: column; gap: 28px;
|
||
}
|
||
.mc-empty {
|
||
flex: 1; min-height: 100%;
|
||
display: flex; flex-direction: column;
|
||
align-items: center; justify-content: center;
|
||
gap: 14px;
|
||
padding: 32px;
|
||
color: var(--black-alpha-56);
|
||
text-align: center;
|
||
}
|
||
.mc-empty .badge {
|
||
font-family: var(--font-mono); font-size: 10.5px;
|
||
letter-spacing: .08em; color: var(--black-alpha-48);
|
||
text-transform: uppercase;
|
||
}
|
||
.mc-empty h2 {
|
||
font-size: 20px; font-weight: 600;
|
||
color: var(--accent-black);
|
||
letter-spacing: -.015em;
|
||
margin: 0;
|
||
}
|
||
.mc-empty p { font-size: 13px; max-width: 420px; line-height: 1.6; margin: 0; }
|
||
.mc-empty .ic {
|
||
width: 56px; height: 56px;
|
||
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: 24px; height: 24px; }
|
||
.mc-empty .examples {
|
||
margin-top: 8px;
|
||
display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
|
||
max-width: 620px;
|
||
}
|
||
.mc-empty .examples .ex {
|
||
padding: 5px 11px;
|
||
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); }
|
||
|
||
.mc-input-wrap {
|
||
position: absolute; left: 0; right: 0; bottom: 0;
|
||
padding: 12px 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); }
|
||
.mc-input-refs { display: none; flex-wrap: wrap; gap: 6px; padding-bottom: 2px; }
|
||
.mc-input-refs.show { display: flex; }
|
||
.mc-input-ref { position: relative; width: 48px; height: 48px; border-radius: var(--r-sm); overflow: hidden; background: var(--background-lighter); border: 1px solid var(--border-faint); }
|
||
.mc-input-ref img { width: 100%; height: 100%; object-fit: cover; }
|
||
.mc-input-ref .x { position: absolute; top: 2px; right: 2px; width: 16px; height: 16px; 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: 9px; height: 9px; }
|
||
|
||
.mc-input-top { display: flex; align-items: flex-end; gap: 10px; }
|
||
.mc-input-top .add-btn {
|
||
flex-shrink: 0; width: 34px; height: 34px;
|
||
background: var(--background-lighter);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-md);
|
||
display: grid; place-items: center;
|
||
color: var(--black-alpha-72); 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-20); color: var(--heat); background: var(--heat-12); }
|
||
.mc-input-top .add-btn svg { width: 14px; height: 14px; }
|
||
.mc-input-top textarea {
|
||
flex: 1; border: 0; outline: 0; resize: none;
|
||
background: transparent; font-family: inherit;
|
||
font-size: 13.5px; line-height: 1.5; color: var(--accent-black);
|
||
min-height: 34px; max-height: 160px; padding: 6px 0;
|
||
}
|
||
.mc-input-top textarea::placeholder { color: var(--black-alpha-48); }
|
||
.mc-input-top .send-btn {
|
||
flex-shrink: 0; width: 34px; height: 34px;
|
||
background: var(--heat); color: var(--accent-white);
|
||
border: 0; border-radius: var(--r-md); cursor: pointer;
|
||
display: grid; place-items: center;
|
||
transition: filter var(--t-base), opacity var(--t-base);
|
||
}
|
||
.mc-input-top .send-btn:hover { filter: brightness(1.05); }
|
||
.mc-input-top .send-btn:disabled { opacity: .4; cursor: not-allowed; }
|
||
.mc-input-top .send-btn svg { width: 14px; height: 14px; }
|
||
|
||
.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: 24px; 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: 9px; height: 9px; 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-up-h { padding: 18px 22px 12px; display: flex; align-items: baseline; gap: 8px; flex-shrink: 0; }
|
||
.mc-up-h h4 { font-size: 13.5px; font-weight: 600; color: var(--accent-black); margin: 0; }
|
||
.mc-up-h .mono { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
|
||
.mc-up-body { flex: 1; min-height: 0; padding: 0 22px 14px; display: flex; flex-direction: column; gap: 12px; overflow: hidden; }
|
||
.mc-up-drop {
|
||
flex-shrink: 0;
|
||
border: 1.5px dashed var(--black-alpha-24);
|
||
border-radius: var(--r-md);
|
||
padding: 28px 16px;
|
||
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-up-drop:hover, .mc-up-drop.dragover { border-color: var(--heat); background: var(--heat-12); }
|
||
.mc-up-drop .ic {
|
||
width: 36px; height: 36px;
|
||
background: var(--heat); color: var(--accent-white);
|
||
border-radius: 50%;
|
||
display: grid; place-items: center;
|
||
}
|
||
.mc-up-drop .ic svg { width: 16px; height: 16px; }
|
||
.mc-up-drop .t { font-size: 13px; font-weight: 600; color: var(--accent-black); }
|
||
.mc-up-drop .d { font-size: 11.5px; color: var(--black-alpha-56); line-height: 1.5; }
|
||
.mc-up-drop .mono { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
|
||
|
||
.mc-up-list-h { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
||
.mc-up-list-h .lbl { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; text-transform: uppercase; }
|
||
.mc-up-list-h .ct { font-size: 12px; font-weight: 600; color: var(--accent-black); }
|
||
.mc-up-list-h .clear { margin-left: auto; background: transparent; border: 0; font-family: inherit; font-size: 11.5px; color: var(--black-alpha-56); cursor: pointer; padding: 0; }
|
||
.mc-up-list-h .clear:hover { color: var(--heat); }
|
||
|
||
.mc-up-list {
|
||
flex: 1; min-height: 0;
|
||
overflow-y: auto;
|
||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
|
||
align-content: start;
|
||
}
|
||
.mc-up-list:empty::before {
|
||
content: '// 暂未上传';
|
||
grid-column: 1 / -1;
|
||
text-align: center;
|
||
font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-32);
|
||
padding: 12px 0;
|
||
}
|
||
.mc-up-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-up-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||
.mc-up-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-up-thumb .x svg { width: 10px; height: 10px; }
|
||
.mc-up-thumb .nm { position: absolute; left: 0; right: 0; bottom: 0; padding: 4px 6px; font-family: var(--font-mono); font-size: 9.5px; color: var(--accent-white); background: linear-gradient(transparent, rgba(0,0,0,.6)); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
|
||
.mc-up-foot { padding: 12px 22px 18px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
||
.mc-up-foot .stat { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
|
||
.mc-up-foot .stat b { color: var(--accent-black); font-weight: 600; }
|
||
.mc-up-foot .commit-btn {
|
||
margin-left: auto;
|
||
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; }
|
||
.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%; }
|
||
.md-zoom-btn { position: absolute; right: 10px; bottom: 10px; height: 28px; padding: 0 12px; background: rgba(21,20,15,.7); color: #fff; border: 0; border-radius: var(--r-pill); display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-family: inherit; cursor: pointer; }
|
||
.md-zoom-btn:hover { background: rgba(21,20,15,.9); }
|
||
.md-zoom-btn svg { width: 12px; height: 12px; }
|
||
.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: block; }
|
||
.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); }
|
||
/* 简介 */
|
||
.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; }
|
||
|
||
/* 编辑商品 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.8" 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.7" 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.6"><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.7" 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.7" 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.7"><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.6"><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.7" 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="2" 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>
|
||
<button class="x" type="button" id="ml-canvas-x" aria-label="关闭">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||
</button>
|
||
</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.6" 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">
|
||
<div class="mc-input-refs" id="mc-input-refs"></div>
|
||
<div class="mc-input-top">
|
||
<button class="add-btn" type="button" id="mc-add-btn" title="上传参考图">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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>
|
||
<textarea id="mc-input-text" rows="1" placeholder="描述模特外形、年龄、风格、服饰…例如:清新校园风女生,黑色长直发"></textarea>
|
||
<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 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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 右 · 本地上传 副视觉 · 支持多张 -->
|
||
<aside class="mc-up">
|
||
<div class="mc-up-h">
|
||
<h4>本地上传</h4>
|
||
<span class="mono">// 可多张</span>
|
||
</div>
|
||
<div class="mc-up-body">
|
||
<div class="mc-up-drop" id="mc-up-drop" tabindex="0" role="button" aria-label="点击或拖入文件上传">
|
||
<div class="ic">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" 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</div>
|
||
<div class="mono">// 单张 ≤ 10MB</div>
|
||
</div>
|
||
<div class="mc-up-list-h">
|
||
<span class="lbl">// 已添加</span>
|
||
<span class="ct" id="mc-up-count">0</span>
|
||
<span class="lbl">张</span>
|
||
<button type="button" class="clear" id="mc-up-clear" hidden>清空</button>
|
||
</div>
|
||
<div class="mc-up-list" id="mc-up-list"></div>
|
||
</div>
|
||
<div class="mc-up-foot">
|
||
<span class="stat">合计 <b id="mc-up-stat">0 张 · 0.0 MB</b></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>
|
||
<input type="file" id="mc-up-input" accept="image/*" multiple hidden>
|
||
</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.7" 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.6" 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.6" 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">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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.8" 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.8" 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.8" 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.6" 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>
|
||
|
||
<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.7" 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.7" 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="2" 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.6" 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.8" 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.8" 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.8" 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.7" 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.7" 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="2"><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.7" 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.7" 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>';
|
||
|
||
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-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 rerun-batch" type="button" title="再次生成这一批">
|
||
${CELL_RERUN_SVG}
|
||
<span>再次生成</span>
|
||
</button>
|
||
<button class="pill-btn primary adopt-batch" type="button" title="采用这一批">
|
||
${ADOPT_SVG}
|
||
<span class="lab">全部采用 · <span class="adopted">0</span>/<span class="total">${n}</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('.rerun-batch').addEventListener('click', () => {
|
||
appendBatch(n, 'rerun-all');
|
||
Shell.toast('再次生成', n + ' 张图重新生成中 · 新批次已追加');
|
||
});
|
||
batch.querySelector('.adopt-batch').addEventListener('click', () => {
|
||
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');
|
||
batch.querySelector('.adopt-batch').click();
|
||
});
|
||
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');
|
||
Shell.toast('已加入资产库', '入对应商品的 AI 素材 · 扣 ¥' + UNIT_PRICE.toFixed(2));
|
||
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.6" 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 closeUploadChoice() {
|
||
_uploadCanvas.classList.remove('show');
|
||
_uploadCanvas.setAttribute('aria-hidden', 'true');
|
||
}
|
||
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')) closeUploadChoice();
|
||
});
|
||
|
||
/* ─── 工作台画布 · 左 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 自适应高度
|
||
_mcInputText.style.height = 'auto';
|
||
_mcInputText.style.height = Math.min(_mcInputText.scrollHeight, 160) + 'px';
|
||
});
|
||
_mcSendBtn?.addEventListener('click', () => {
|
||
const txt = _mcInputText.value.trim();
|
||
if (!txt) return;
|
||
Shell.toast('AI 生成已排队', '约 12s · 完成后写入「我的上传」');
|
||
_mcInputText.value = '';
|
||
_mcInputText.style.height = 'auto';
|
||
_mcSendBtn.disabled = true;
|
||
});
|
||
// 示例 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="2.4" 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();
|
||
}));
|
||
}
|
||
|
||
/* ─── 工作台画布 · 右 本地上传 区(累积多张) ─── */
|
||
const _mcUpInput = document.getElementById('mc-up-input');
|
||
const _mcUpDrop = document.getElementById('mc-up-drop');
|
||
const _mcUpList = document.getElementById('mc-up-list');
|
||
const _mcUpCount = document.getElementById('mc-up-count');
|
||
const _mcUpStat = document.getElementById('mc-up-stat');
|
||
const _mcUpCommit = document.getElementById('mc-up-commit');
|
||
const _mcUpClear = document.getElementById('mc-up-clear');
|
||
let _mcUpFiles = []; // [{ file, url, name, size }]
|
||
|
||
function _renderMcUp() {
|
||
_mcUpList.innerHTML = _mcUpFiles.map((f, i) => `
|
||
<div class="mc-up-thumb">
|
||
<img src="${f.url}" alt="${f.name}">
|
||
<button class="x" data-idx="${i}" aria-label="移除"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button>
|
||
<div class="nm">${f.name}</div>
|
||
</div>`).join('');
|
||
_mcUpList.querySelectorAll('.x').forEach(x => x.addEventListener('click', () => {
|
||
_mcUpFiles.splice(+x.dataset.idx, 1);
|
||
_renderMcUp();
|
||
}));
|
||
const n = _mcUpFiles.length;
|
||
const mb = _mcUpFiles.reduce((s, f) => s + f.size, 0) / (1024 * 1024);
|
||
_mcUpCount.textContent = n;
|
||
_mcUpStat.innerHTML = `<b>${n}</b> 张 · <b>${mb.toFixed(1)}</b> MB`;
|
||
_mcUpCommit.disabled = n === 0;
|
||
_mcUpClear.hidden = n === 0;
|
||
}
|
||
|
||
function _mcAddFiles(rawFiles) {
|
||
const files = [...(rawFiles || [])].filter(f => /^image\//.test(f.type));
|
||
if (!files.length) return;
|
||
files.forEach(f => _mcUpFiles.push({
|
||
file: f, url: URL.createObjectURL(f), name: f.name, size: f.size,
|
||
}));
|
||
_renderMcUp();
|
||
}
|
||
|
||
_mcUpDrop?.addEventListener('click', () => _mcUpInput.click());
|
||
_mcUpDrop?.addEventListener('keydown', e => {
|
||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _mcUpInput.click(); }
|
||
});
|
||
_mcUpInput?.addEventListener('change', e => {
|
||
_mcAddFiles(e.target.files);
|
||
e.target.value = '';
|
||
});
|
||
['dragenter', 'dragover'].forEach(ev => _mcUpDrop?.addEventListener(ev, e => {
|
||
e.preventDefault(); _mcUpDrop.classList.add('dragover');
|
||
}));
|
||
['dragleave', 'drop'].forEach(ev => _mcUpDrop?.addEventListener(ev, e => {
|
||
e.preventDefault(); _mcUpDrop.classList.remove('dragover');
|
||
}));
|
||
_mcUpDrop?.addEventListener('drop', e => {
|
||
_mcAddFiles(e.dataTransfer?.files);
|
||
});
|
||
|
||
_mcUpClear?.addEventListener('click', () => { _mcUpFiles = []; _renderMcUp(); });
|
||
|
||
_mcUpCommit?.addEventListener('click', () => {
|
||
if (_mcUpFiles.length === 0) return;
|
||
const n = _mcUpFiles.length;
|
||
_mcUpFiles.forEach((f, i) => {
|
||
const baseName = (f.name || 'YouNew').replace(/\.[^.]+$/, '').slice(0, 8);
|
||
const ts = Date.now().toString(36) + i;
|
||
MODELS.unshift({
|
||
id: 'm-up-' + ts,
|
||
name: baseName || 'YouNew',
|
||
gender: '女', age: '青年', style: '我的模特',
|
||
source: 'own', used: 0,
|
||
region: '—', skin: '—', height: '—', build: '—',
|
||
hairLen: '—', hairColor: '—', vibe: '—',
|
||
feature: '用户上传素材 · 等待生成三视图',
|
||
});
|
||
});
|
||
_mcUpFiles = [];
|
||
_renderMcUp();
|
||
// 切回「我的上传」+ 重渲染列表 + 关闭画布
|
||
_libFilter.source = 'own';
|
||
document.querySelectorAll('.ml-side .ml-side-item').forEach(x =>
|
||
x.classList.toggle('active', x.dataset.source === 'own'));
|
||
renderModelLib(_libFilter);
|
||
closeUploadChoice();
|
||
Shell.toast('已加入模特库', `+ ${n} 张 · 来源 我的上传`);
|
||
});
|
||
|
||
/* 兼容旧 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;
|
||
|
||
// 缩略图 strip
|
||
const thumbsEl = document.getElementById('md-thumbs');
|
||
thumbsEl.innerHTML = ['v1','v2','v3'].map((t, i) => `<div class="thumb${i === 0 ? ' active' : ''}"><span class="ph-frame">${t}</span></div>`).join('');
|
||
thumbsEl.querySelectorAll('.thumb').forEach(t => t.addEventListener('click', () => {
|
||
thumbsEl.querySelectorAll('.thumb').forEach(x => x.classList.remove('active'));
|
||
t.classList.add('active');
|
||
}));
|
||
|
||
// 简介 + 标签
|
||
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="2" 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>
|