1962 lines
77 KiB
HTML
1962 lines
77 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">
|
||
<style>
|
||
/* viewport-fit · 工作台铺满 */
|
||
.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: 24px 28px 0; }
|
||
.page-head { flex-shrink: 0; margin-bottom: 16px; }
|
||
|
||
.mp-layout {
|
||
flex: 1; min-height: 0;
|
||
display: grid;
|
||
grid-template-columns: 220px 360px 1fr;
|
||
gap: 20px;
|
||
padding-bottom: 24px;
|
||
}
|
||
@media (max-width: 1280px) {
|
||
.mp-layout { grid-template-columns: 200px 340px 1fr; gap: 16px; }
|
||
}
|
||
|
||
/* ─── 任务栏 (最左 · 历史任务列表) ─── */
|
||
.mp-tasks-panel {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-md);
|
||
display: flex; flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
.mp-tasks-h {
|
||
display: flex; align-items: center; gap: 6px;
|
||
padding: 14px 16px 10px;
|
||
border-bottom: 1px solid var(--border-faint);
|
||
font-size: 13px; font-weight: 600; color: var(--accent-black);
|
||
}
|
||
.mp-tasks-h .ct {
|
||
font-family: var(--font-mono); font-size: 10.5px;
|
||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||
margin-left: auto;
|
||
}
|
||
.mp-tasks-h .new {
|
||
margin-left: 6px;
|
||
width: 24px; height: 24px;
|
||
display: grid; place-items: center;
|
||
background: var(--heat-12); color: var(--heat);
|
||
border: 0; border-radius: var(--r-sm);
|
||
cursor: pointer;
|
||
transition: background var(--t-base);
|
||
}
|
||
.mp-tasks-h .new:hover { background: var(--heat-20); }
|
||
.mp-tasks-h .new svg { width: 12px; height: 12px; }
|
||
.mp-tasks-list {
|
||
flex: 1; min-height: 0;
|
||
overflow-y: auto;
|
||
padding: 8px;
|
||
display: flex; flex-direction: column; gap: 6px;
|
||
}
|
||
.mp-tasks-empty {
|
||
padding: 24px 14px;
|
||
text-align: center;
|
||
font-size: 11.5px;
|
||
color: var(--black-alpha-48);
|
||
line-height: 1.55;
|
||
}
|
||
.mp-tasks-empty .mono {
|
||
font-family: var(--font-mono); font-size: 10.5px;
|
||
letter-spacing: .02em; display: block; margin-top: 4px;
|
||
}
|
||
.mp-task-card {
|
||
padding: 10px 12px;
|
||
background: var(--background-lighter);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-sm);
|
||
cursor: pointer;
|
||
transition: background var(--t-base), border-color var(--t-base);
|
||
position: relative;
|
||
}
|
||
.mp-task-card:hover { background: var(--surface); border-color: var(--black-alpha-24); }
|
||
.mp-task-card.active { background: var(--heat-12); border-color: var(--heat-20); }
|
||
.mp-task-card .nm {
|
||
font-size: 12.5px; font-weight: 600; color: var(--accent-black);
|
||
line-height: 1.35;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
padding-right: 18px;
|
||
}
|
||
.mp-task-card.active .nm { color: var(--heat); }
|
||
.mp-task-card .meta {
|
||
margin-top: 4px;
|
||
font-family: var(--font-mono); font-size: 10px;
|
||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||
display: flex; align-items: center; gap: 6px;
|
||
}
|
||
.mp-task-card .meta .pill-mini {
|
||
padding: 1px 6px; border-radius: var(--r-pill);
|
||
font-size: 9.5px; font-weight: 600; letter-spacing: .04em;
|
||
}
|
||
.mp-task-card .meta .pill-mini.gen { background: var(--heat-12); color: var(--heat); }
|
||
.mp-task-card .meta .pill-mini.ok { background: var(--forest-bg); color: var(--accent-forest); }
|
||
.mp-task-card .meta .pill-mini.err { background: var(--crimson-bg); color: var(--accent-crimson); }
|
||
.mp-task-card .x {
|
||
position: absolute; top: 6px; right: 6px;
|
||
width: 18px; height: 18px;
|
||
display: grid; place-items: center;
|
||
background: transparent; border: 0;
|
||
border-radius: var(--r-sm);
|
||
cursor: pointer;
|
||
color: var(--black-alpha-48);
|
||
opacity: 0;
|
||
transition: opacity var(--t-base), background var(--t-base), color var(--t-base);
|
||
}
|
||
.mp-task-card:hover .x { opacity: 1; }
|
||
.mp-task-card .x:hover { background: var(--black-alpha-8); color: var(--accent-crimson); }
|
||
.mp-task-card .x svg { width: 10px; height: 10px; }
|
||
|
||
/* ─── 左栏 · 表单 ─── */
|
||
.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-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; }
|
||
|
||
/* 商品库全屏弹窗 (复用 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;
|
||
}
|
||
.mp-pv-h {
|
||
display: flex; flex-direction: column; gap: 8px;
|
||
padding-bottom: 14px; margin-bottom: 16px;
|
||
border-bottom: 1px solid var(--border-faint);
|
||
}
|
||
.mp-pv-h .row {
|
||
display: flex; align-items: center; gap: 8px;
|
||
font-size: 13px; color: var(--black-alpha-72);
|
||
}
|
||
.mp-pv-h .row .k {
|
||
font-family: var(--font-mono); font-size: 11.5px;
|
||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||
}
|
||
.mp-pv-h .row .v {
|
||
color: var(--accent-black); font-weight: 500;
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
}
|
||
.mp-pv-h .row .v .thumb-sm {
|
||
width: 24px; height: 24px; border-radius: var(--r-sm);
|
||
}
|
||
.mp-pv-h .row .swap {
|
||
margin-left: auto;
|
||
font-size: 12px; color: var(--heat);
|
||
cursor: pointer;
|
||
}
|
||
.mp-pv-h .row .swap:hover { text-decoration: underline; }
|
||
|
||
.mp-pv-grid {
|
||
flex: 1;
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||
gap: 10px;
|
||
align-content: start;
|
||
}
|
||
.mp-result {
|
||
position: relative;
|
||
aspect-ratio: 3/4;
|
||
border-radius: var(--r-md);
|
||
overflow: hidden;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border-faint);
|
||
display: flex; flex-direction: column;
|
||
}
|
||
.mp-result .mp-r-thumb { flex: 1; }
|
||
.mp-result .mp-r-act {
|
||
padding: 6px 8px;
|
||
background: rgba(255,255,255,.95);
|
||
display: flex; align-items: center; gap: 6px;
|
||
font-size: 11.5px;
|
||
color: var(--black-alpha-72);
|
||
}
|
||
.mp-result .mp-r-act .add-lib {
|
||
flex: 1;
|
||
display: inline-flex; align-items: center; justify-content: center; gap: 4px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--heat-40);
|
||
border-radius: var(--r-sm);
|
||
color: var(--heat);
|
||
height: 24px;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 11px;
|
||
transition: background var(--t-base);
|
||
}
|
||
.mp-result .mp-r-act .add-lib:hover { background: var(--heat-12); }
|
||
.mp-result .mp-r-act .add-lib.added {
|
||
background: var(--accent-emerald-bg, #e6f4ec);
|
||
color: var(--accent-emerald, #1f8a51);
|
||
border-color: var(--accent-emerald-bd, #c4e3d1);
|
||
}
|
||
.mp-result .mp-r-act .add-lib svg { width: 11px; height: 11px; }
|
||
|
||
.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;
|
||
}
|
||
.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; }
|
||
|
||
/* 模特详情 居中弹窗 */
|
||
.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; }
|
||
.md-modal-bg.show { display: flex; }
|
||
.md-modal {
|
||
background: var(--surface);
|
||
border-radius: var(--r-md);
|
||
max-width: 880px; width: 92%;
|
||
max-height: 90vh;
|
||
display: flex; flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
/* 详情 body 左右分栏:立绘 + 属性 */
|
||
.md-split {
|
||
display: grid;
|
||
grid-template-columns: 280px 1fr;
|
||
gap: 24px;
|
||
margin-bottom: 22px;
|
||
}
|
||
.md-portrait {
|
||
aspect-ratio: 3/4;
|
||
border-radius: var(--r-md);
|
||
background: var(--background-lighter);
|
||
border: 1px solid var(--border-faint);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.md-portrait .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);
|
||
letter-spacing: .02em;
|
||
}
|
||
.md-portrait .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-right { display: flex; flex-direction: column; }
|
||
.md-modal-h {
|
||
padding: 16px 22px;
|
||
border-bottom: 1px solid var(--border-faint);
|
||
display: flex; align-items: center;
|
||
}
|
||
.md-modal-h h3 { font-size: 16px; font-weight: 600; }
|
||
.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); }
|
||
.md-modal-h .x svg { width: 14px; height: 14px; }
|
||
.md-modal-body {
|
||
flex: 1; min-height: 0;
|
||
overflow-y: auto;
|
||
padding: 20px 24px;
|
||
}
|
||
.md-section-h {
|
||
font-size: 13px; font-weight: 600;
|
||
color: var(--accent-black);
|
||
margin-bottom: 10px;
|
||
display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.md-section-h::before {
|
||
content: '';
|
||
width: 3px; height: 14px;
|
||
background: var(--heat);
|
||
border-radius: 2px;
|
||
}
|
||
.md-attr {
|
||
background: var(--background-lighter);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-md);
|
||
padding: 14px 18px;
|
||
margin-bottom: 0;
|
||
display: grid;
|
||
grid-template-columns: 76px 1fr 76px 1fr;
|
||
gap: 10px 14px;
|
||
font-size: 13px;
|
||
align-items: center;
|
||
}
|
||
.md-attr .k {
|
||
color: var(--black-alpha-48);
|
||
font-family: var(--font-mono); font-size: 11.5px;
|
||
letter-spacing: .02em;
|
||
}
|
||
.md-attr .v {
|
||
color: var(--accent-black);
|
||
font-weight: 500;
|
||
}
|
||
.md-feature {
|
||
background: var(--background-lighter);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-md);
|
||
padding: 12px 18px;
|
||
margin-bottom: 18px;
|
||
font-size: 13px;
|
||
color: var(--accent-black);
|
||
line-height: 1.6;
|
||
}
|
||
.md-views-h {
|
||
font-size: 13px; font-weight: 600;
|
||
color: var(--accent-black);
|
||
margin-bottom: 10px;
|
||
display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.md-views-h::before {
|
||
content: '';
|
||
width: 3px; height: 14px;
|
||
background: var(--heat);
|
||
border-radius: 2px;
|
||
}
|
||
.md-views {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 10px;
|
||
}
|
||
.md-views .md-view {
|
||
aspect-ratio: 3/4;
|
||
background: var(--background-lighter);
|
||
border: 1px solid var(--border-faint);
|
||
border-radius: var(--r-sm);
|
||
position: relative;
|
||
display: grid; place-items: end center;
|
||
}
|
||
.md-views .md-view .lbl {
|
||
position: absolute; bottom: 6px;
|
||
font-family: var(--font-mono); font-size: 10.5px;
|
||
color: var(--black-alpha-48);
|
||
background: rgba(255,255,255,.85);
|
||
padding: 1px 6px;
|
||
border-radius: var(--r-sm);
|
||
}
|
||
.md-modal-f {
|
||
padding: 14px 22px;
|
||
border-top: 1px solid var(--border-faint);
|
||
display: flex; justify-content: flex-end; gap: 10px;
|
||
}
|
||
|
||
/* 编辑商品 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; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="page">
|
||
|
||
<div class="page-head">
|
||
<div>
|
||
<h1>模特上身图</h1>
|
||
<div class="sub"><span class="mono">// 选商品 + 选模特 → 生成 AI 上身效果图</span> · 失败不扣费,采纳入库才计费</div>
|
||
</div>
|
||
<div class="actions">
|
||
<a class="btn" href="asset-factory.html">
|
||
<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="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||
返回图片生成
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mp-layout">
|
||
|
||
<!-- ===== 最左栏 · 任务历史 ===== -->
|
||
<div class="mp-tasks-panel" id="tasks-panel">
|
||
<div class="mp-tasks-h">
|
||
历史任务
|
||
<span class="ct" id="tasks-count">0</span>
|
||
<button class="new" type="button" id="tasks-new-btn" title="新建任务">
|
||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="mp-tasks-list" id="tasks-list"></div>
|
||
</div>
|
||
|
||
<!-- ===== 中栏 · 表单 ===== -->
|
||
<div class="mp-form">
|
||
|
||
<!-- ① 选择商品 (单选) -->
|
||
<div class="mp-step">
|
||
<div class="mp-step-h">
|
||
<span class="num">1</span>
|
||
<span class="title">选择商品</span>
|
||
</div>
|
||
<div class="prod-list" id="prod-list">
|
||
<!-- 已选商品 chip · JS 动态渲染 -->
|
||
</div>
|
||
<button class="prod-add" type="button" id="prod-add-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="mp-step">
|
||
<div class="mp-step-h">
|
||
<span class="num">2</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 class="model-upload" id="upload-model">
|
||
<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-4M17 8l-5-5-5 5M12 3v12"/></svg>
|
||
上传我的模特
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ③ 生成设置 -->
|
||
<div class="mp-step">
|
||
<div class="mp-step-h">
|
||
<span class="num">3</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">// 满意后点 [入资产库] 才扣费 · 失败不扣</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ===== 右栏 · 预览 ===== -->
|
||
<div class="mp-preview">
|
||
<div class="mp-pv-h">
|
||
<div class="row">
|
||
<span class="k">已选商品</span>
|
||
<span class="v"><span class="placeholder thumb-sm"></span><span id="pv-prod">补水保湿精华液</span></span>
|
||
</div>
|
||
<div class="row">
|
||
<span class="k">已选模特</span>
|
||
<span class="v" id="pv-model">Ava (亚洲·25岁·清新)</span>
|
||
<span class="swap" id="pv-swap">更换模特</span>
|
||
</div>
|
||
<div class="row">
|
||
<span class="k">生成</span>
|
||
<span class="v"><span id="pv-count">4</span> 张 · <span id="pv-ratio">1:1</span></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mp-pv-grid" id="pv-grid">
|
||
<!-- 默认 4 张占位; 生成后填充 -->
|
||
<div class="mp-result"><div class="placeholder mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div></div>
|
||
<div class="mp-result"><div class="placeholder mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div></div>
|
||
<div class="mp-result"><div class="placeholder mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div></div>
|
||
<div class="mp-result"><div class="placeholder mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div></div>
|
||
</div>
|
||
|
||
<div class="mp-pv-foot">
|
||
// 生成结果默认不入资产库,每张图采纳后点 [入资产库] 才扣费并保存
|
||
<br>// 任务进度可在 <a href="asset-factory.html">任务中心 →</a> 查看
|
||
</div>
|
||
</div>
|
||
|
||
</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 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>
|
||
<button class="btn-up" type="button" id="ml-upload-btn" style="margin-left:auto">
|
||
<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-4M17 8l-5-5-5 5M12 3v12"/></svg>
|
||
上传我的模特
|
||
</button>
|
||
</div>
|
||
<div class="ml-scroll">
|
||
<div class="ml-grid" id="ml-grid">
|
||
<!-- 12 个模特卡片 (placeholder) -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 模特详情 居中弹窗 ===== -->
|
||
<div class="md-modal-bg" id="md-modal-bg">
|
||
<div class="md-modal">
|
||
<div class="md-modal-h">
|
||
<h3 id="md-title">模特详情</h3>
|
||
<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-split">
|
||
<div class="placeholder md-portrait">
|
||
<div class="ph-name" id="md-portrait-name">—</div>
|
||
<div class="ph-frame">立绘 · 3:4</div>
|
||
</div>
|
||
<div class="md-right">
|
||
<div class="md-section-h">角色属性</div>
|
||
<div class="md-attr">
|
||
<span class="k">// 文化区域</span><span class="v" id="md-region">—</span>
|
||
<span class="k">// 气质</span> <span class="v" id="md-vibe">—</span>
|
||
<span class="k">// 性别</span> <span class="v" id="md-gender">—</span>
|
||
<span class="k">// 体格</span> <span class="v" id="md-build">—</span>
|
||
<span class="k">// 年龄段</span> <span class="v" id="md-age">—</span>
|
||
<span class="k">// 身高</span> <span class="v" id="md-height">—</span>
|
||
<span class="k">// 肤色</span> <span class="v" id="md-skin">—</span>
|
||
<span class="k">// 发长</span> <span class="v" id="md-hairLen">—</span>
|
||
<span class="k">// 发色</span> <span class="v" id="md-hairColor">—</span>
|
||
<span class="k">// 来源</span> <span class="v" id="md-source">—</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="md-views-h">全身三视图</div>
|
||
<div class="md-views">
|
||
<div class="md-view"><div class="lbl">正面</div></div>
|
||
<div class="md-view"><div class="lbl">侧面</div></div>
|
||
<div class="md-view"><div class="lbl">背面</div></div>
|
||
</div>
|
||
</div>
|
||
<div class="md-modal-f">
|
||
<button class="btn" type="button" id="md-cancel">关闭</button>
|
||
<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"></script>
|
||
<script src="assets/new-product-drawer.js"></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: 'p1', // string | null
|
||
selectedModel: null, // string | null
|
||
count: 4,
|
||
ratio: '1:1',
|
||
};
|
||
const UNIT_PRICE = 0.30;
|
||
|
||
// ─── 已选商品 渲染 (单选) ───
|
||
function renderSelectedProds() {
|
||
const list = document.getElementById('prod-list');
|
||
const id = state.selectedProd;
|
||
const p = id ? PRODUCTS.find(x => x.id === id) : null;
|
||
if (!p) {
|
||
list.innerHTML = '<div class="prod-empty">未选择商品<div class="mono">// 点击下方按钮去商品库选</div></div>';
|
||
document.getElementById('pv-prod').textContent = '未选择';
|
||
} else {
|
||
list.innerHTML = `
|
||
<div class="prod-row" data-id="${p.id}">
|
||
<div class="placeholder thumb"></div>
|
||
<div class="info"><div class="nm">${p.name}</div><div class="meta">${p.cat}</div></div>
|
||
<button class="x" type="button" data-rm="${p.id}" 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>
|
||
`;
|
||
list.querySelector('button.x[data-rm]').addEventListener('click', () => {
|
||
state.selectedProd = null;
|
||
renderSelectedProds();
|
||
});
|
||
document.getElementById('pv-prod').textContent = 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 长效补水', '官方授权正品', '通勤补妆神器'] },
|
||
p2: { target: '通勤党 · 18-30 岁 · 大学生 / 白领', bullets: ['主动降噪 35dB', '蓝牙 5.4 双设备', '32h 续航'] },
|
||
p3: { target: '加班党 · 独居青年 · 一人食场景', bullets: ['一杯水即可', '原切牛肉块充足', '6 桶大箱装'] },
|
||
p4: { target: '通勤防晒 · 油皮 / 敏感肌', bullets: ['SPF50+ PA++++', '物理防晒不刺激', '清透不假白'] },
|
||
p5: { target: '咖啡入门 · 早八党 · 加班族', bullets: ['冷热水即溶', '原产地豆精选', '24 颗精装'] },
|
||
p6: { target: '小户型 · 健康饮食 · 新手厨房', bullets: ['4L 大容量', '可视玻璃观察', '一键预设'] },
|
||
p7: { target: '健身房 · 通勤穿搭 · 18-32 岁女性', bullets: ['裸感面料', '高弹收腹', '亲肤透气'] },
|
||
};
|
||
|
||
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: [] };
|
||
document.getElementById('pcf-target').value = extra.target || '';
|
||
// 渲染 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
|
||
const bullets = [...document.querySelectorAll('#pcf-bullets li:not(.add) input')].map(i => i.value.trim()).filter(Boolean);
|
||
PRODUCT_EXTRA[_editingProdId] = { target: newTarget, bullets };
|
||
Shell.toast('已保存', newName);
|
||
closeEditProductDrawer();
|
||
renderProdLib();
|
||
renderSelectedProds();
|
||
});
|
||
|
||
document.getElementById('prod-add-btn').addEventListener('click', () => {
|
||
_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('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; }
|
||
state.selectedProd = _plDraft;
|
||
document.getElementById('pl-modal-bg').classList.remove('show');
|
||
renderSelectedProds();
|
||
});
|
||
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;
|
||
state.selectedProd = product.id;
|
||
// 强制 reset filter/query,保证新商品在首位可见
|
||
_plCatFilter = '';
|
||
_plQuery = '';
|
||
const searchInput = document.getElementById('pl-search-input');
|
||
if (searchInput) searchInput.value = '';
|
||
renderProdLib();
|
||
renderSelectedProds();
|
||
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;
|
||
// 同步所有出现的 model-card (mini grid + lib grid)
|
||
document.querySelectorAll('.model-card').forEach(c =>
|
||
c.classList.toggle('selected', c.dataset.id === state.selectedModel)
|
||
);
|
||
updateModelSummary();
|
||
}
|
||
document.querySelectorAll('#model-grid-mini .model-card').forEach(card => {
|
||
card.addEventListener('click', e => {
|
||
if (e.target.closest('.m-thumb')) {
|
||
openModelDetail(card.dataset.id);
|
||
return;
|
||
}
|
||
selectModel(card.dataset.id);
|
||
});
|
||
});
|
||
document.getElementById('upload-model').addEventListener('click', () => {
|
||
Shell.toast('上传模特', '选择本地图片 (占位)');
|
||
});
|
||
|
||
// ─── 立即生成设置 ───
|
||
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();
|
||
});
|
||
});
|
||
|
||
// ─── 立即生成 ───
|
||
document.getElementById('mp-go-btn').addEventListener('click', () => {
|
||
if (!state.selectedModel || !state.selectedProd) return;
|
||
Shell.toast('已提交任务', '可在 [任务中心] 查看进度');
|
||
// 模拟: 填充预览区为"生成中" 占位
|
||
const grid = document.getElementById('pv-grid');
|
||
grid.innerHTML = '';
|
||
for (let i = 0; i < state.count; i++) {
|
||
const div = document.createElement('div');
|
||
div.className = 'mp-result';
|
||
div.innerHTML = `
|
||
<div class="placeholder mp-r-thumb"><span class="ph-frame">生成中... ${state.ratio}</span></div>
|
||
<div class="mp-r-act">
|
||
<button class="add-lib" type="button" data-cost="${UNIT_PRICE}">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M12 5v14M5 12h14"/></svg>
|
||
入资产库 ¥${UNIT_PRICE.toFixed(2)}
|
||
</button>
|
||
</div>`;
|
||
grid.appendChild(div);
|
||
}
|
||
// 绑定每张图的入库按钮
|
||
grid.querySelectorAll('.add-lib').forEach(b => {
|
||
b.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
if (b.classList.contains('added')) return;
|
||
b.classList.add('added');
|
||
b.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12l5 5L20 7"/></svg> 已入库';
|
||
Shell.toast('已入库', '已扣 ¥' + (+b.dataset.cost).toFixed(2));
|
||
});
|
||
});
|
||
});
|
||
|
||
// ─── 模特库 全屏 ───
|
||
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);
|
||
grid.innerHTML = list.map(m => `
|
||
<div class="model-card${state.selectedModel === m.id ? ' selected' : ''}" data-id="${m.id}" data-name="${m.name}">
|
||
<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">${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').forEach(card => {
|
||
card.addEventListener('click', e => {
|
||
if (e.target.closest('.m-thumb')) {
|
||
openModelDetail(card.dataset.id);
|
||
return;
|
||
}
|
||
selectModel(card.dataset.id);
|
||
});
|
||
});
|
||
}
|
||
|
||
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');
|
||
});
|
||
document.getElementById('ml-upload-btn').addEventListener('click', () => {
|
||
Shell.toast('上传模特', '选择本地图片');
|
||
});
|
||
// 左侧来源 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 openModelDetail(id) {
|
||
const m = MODELS.find(x => x.id === id);
|
||
if (!m) return;
|
||
_detailModelId = id;
|
||
document.getElementById('md-title').textContent = m.name + ' · 模特详情';
|
||
document.getElementById('md-portrait-name').textContent = m.name + ' · ' + m.style;
|
||
document.getElementById('md-region').textContent = m.region;
|
||
document.getElementById('md-gender').textContent = m.gender;
|
||
document.getElementById('md-age').textContent = m.age;
|
||
document.getElementById('md-vibe').textContent = m.vibe;
|
||
document.getElementById('md-build').textContent = m.build;
|
||
document.getElementById('md-height').textContent = m.height;
|
||
document.getElementById('md-skin').textContent = m.skin;
|
||
document.getElementById('md-hairLen').textContent = m.hairLen;
|
||
document.getElementById('md-hairColor').textContent = m.hairColor;
|
||
document.getElementById('md-source').textContent = m.source === 'preset' ? '平台预设' : '我的上传';
|
||
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-cancel').addEventListener('click', () => {
|
||
document.getElementById('md-modal-bg').classList.remove('show');
|
||
});
|
||
document.getElementById('md-select').addEventListener('click', () => {
|
||
if (_detailModelId) {
|
||
state.selectedModel = _detailModelId;
|
||
// 同步所有 model-card (mini + lib)
|
||
document.querySelectorAll('.model-card').forEach(c =>
|
||
c.classList.toggle('selected', c.dataset.id === state.selectedModel)
|
||
);
|
||
updateModelSummary();
|
||
}
|
||
document.getElementById('md-modal-bg').classList.remove('show');
|
||
});
|
||
|
||
// 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;
|
||
})();
|
||
|
||
// 初始化
|
||
renderSelectedProds();
|
||
updateModelSummary();
|
||
updateCost();
|
||
|
||
/* ============================================================
|
||
任务历史 (localStorage) · 仅在「立即生成」时创建任务
|
||
============================================================ */
|
||
(function () {
|
||
'use strict';
|
||
const TASK_TYPE = 'model';
|
||
const KEY = 'fs-image-tasks-' + TASK_TYPE;
|
||
|
||
let tasks = [];
|
||
let currentId = null;
|
||
|
||
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 escapeHtml(s) {
|
||
return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
}
|
||
|
||
function buildSnapshot() {
|
||
return {
|
||
selectedProd: state.selectedProd,
|
||
selectedModel: state.selectedModel,
|
||
count: state.count,
|
||
ratio: state.ratio,
|
||
};
|
||
}
|
||
function autoName(snap) {
|
||
const prod = snap.selectedProd ? PRODUCTS.find(p => p.id === snap.selectedProd) : null;
|
||
const model = snap.selectedModel ? MODELS.find(m => m.id === snap.selectedModel) : null;
|
||
const left = prod ? (prod.name.length > 10 ? prod.name.slice(0, 10) + '…' : prod.name) : '未选商品';
|
||
const right = model ? model.name : '未选模特';
|
||
return `${left} × ${right}`;
|
||
}
|
||
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);
|
||
}
|
||
|
||
function renderTasksList() {
|
||
const root = document.getElementById('tasks-list');
|
||
document.getElementById('tasks-count').textContent = tasks.length;
|
||
if (!tasks.length) {
|
||
root.innerHTML = `<div class="mp-tasks-empty">还没有历史任务<span class="mono">// 调整商品/模特/参数后生成</span></div>`;
|
||
return;
|
||
}
|
||
const STATUS_LABEL = { gen: '生成中', ok: '已完成', err: '失败' };
|
||
const sorted = [...tasks].sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
||
root.innerHTML = sorted.map(t => `
|
||
<div class="mp-task-card${t.id === currentId ? ' active' : ''}" data-id="${t.id}">
|
||
<div class="nm">${escapeHtml(t.name)}</div>
|
||
<div class="meta">
|
||
<span class="pill-mini ${t.status}">${STATUS_LABEL[t.status] || t.status}</span>
|
||
<span>${escapeHtml(t.time || '')}</span>
|
||
</div>
|
||
<button class="x" type="button" data-rm="${t.id}" title="删除">
|
||
<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>
|
||
`).join('');
|
||
root.querySelectorAll('.mp-task-card').forEach(card => {
|
||
card.addEventListener('click', e => {
|
||
if (e.target.closest('button.x')) return;
|
||
loadTaskIntoForm(card.dataset.id);
|
||
});
|
||
});
|
||
root.querySelectorAll('button.x[data-rm]').forEach(btn => {
|
||
btn.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
const id = btn.dataset.rm;
|
||
const idx = tasks.findIndex(t => t.id === id);
|
||
if (idx < 0) return;
|
||
const name = tasks[idx].name;
|
||
tasks.splice(idx, 1);
|
||
save(tasks);
|
||
if (currentId === id) currentId = null;
|
||
renderTasksList();
|
||
Shell.toast('已删除任务', name);
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ---------- 把历史任务还原到表单 ---------- */
|
||
function loadTaskIntoForm(id) {
|
||
const t = tasks.find(x => x.id === id);
|
||
if (!t) return;
|
||
state.selectedProd = t.snap.selectedProd || null;
|
||
state.selectedModel = t.snap.selectedModel || null;
|
||
state.count = t.snap.count;
|
||
state.ratio = t.snap.ratio;
|
||
document.querySelectorAll('.pill-row[data-key="count"] .opt').forEach(b => b.classList.toggle('active', +b.dataset.val === state.count));
|
||
document.querySelectorAll('.pill-row[data-key="ratio"] .opt').forEach(b => b.classList.toggle('active', b.dataset.val === state.ratio));
|
||
document.querySelectorAll('.model-card').forEach(c => c.classList.toggle('selected', c.dataset.id === state.selectedModel));
|
||
renderSelectedProds();
|
||
updateModelSummary();
|
||
updateCost();
|
||
currentId = id;
|
||
renderTasksList();
|
||
}
|
||
|
||
/* ---------- 新建空白任务 (重置表单, 不写库) ---------- */
|
||
function newBlankTask() {
|
||
state.selectedProd = null;
|
||
state.selectedModel = null;
|
||
state.count = 4;
|
||
state.ratio = '1:1';
|
||
document.querySelectorAll('.pill-row[data-key="count"] .opt').forEach(b => b.classList.toggle('active', +b.dataset.val === 4));
|
||
document.querySelectorAll('.pill-row[data-key="ratio"] .opt').forEach(b => b.classList.toggle('active', b.dataset.val === '1:1'));
|
||
document.querySelectorAll('.model-card').forEach(c => c.classList.remove('selected'));
|
||
renderSelectedProds();
|
||
updateModelSummary();
|
||
updateCost();
|
||
const grid = document.getElementById('pv-grid');
|
||
if (grid) {
|
||
grid.innerHTML = '';
|
||
for (let i = 0; i < 4; i++) {
|
||
const div = document.createElement('div');
|
||
div.className = 'mp-result';
|
||
div.innerHTML = `<div class="placeholder mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div>`;
|
||
grid.appendChild(div);
|
||
}
|
||
}
|
||
currentId = null;
|
||
renderTasksList();
|
||
}
|
||
|
||
/* ---------- 「立即生成」 → 才创建任务 ---------- */
|
||
document.getElementById('mp-go-btn').addEventListener('click', () => {
|
||
if (!state.selectedModel || !state.selectedProd) return;
|
||
const snap = buildSnapshot();
|
||
const name = autoName(snap);
|
||
const time = timeNow();
|
||
if (currentId) {
|
||
const t = tasks.find(x => x.id === currentId);
|
||
if (t) { t.snap = snap; t.name = name; t.status = 'gen'; t.time = time; }
|
||
} else {
|
||
currentId = 'task-' + Date.now();
|
||
tasks.push({ id: currentId, type: TASK_TYPE, name, snap, status: 'gen', time, createdAt: Date.now() });
|
||
}
|
||
save(tasks);
|
||
renderTasksList();
|
||
});
|
||
|
||
/* ---------- 「+ 新建任务」按钮 ---------- */
|
||
document.getElementById('tasks-new-btn').addEventListener('click', newBlankTask);
|
||
|
||
/* ---------- 初始化 ---------- */
|
||
tasks = load();
|
||
renderTasksList();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|