AirShelf/电商AI平台/model-photo.html
iye 8a783ca36f
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
feat(workbench): 统一立绘详情页参考布局 · 三视图全 16:9 · 工作台批次追加
详情页 (pipeline / library / model-photo)
- 统一参考布局:大立绘+缩略 strip+查看大图,右栏 三视图+简介(标签 chip)+3 列属性表
- 底部仅留「下载」+「使用该资产」,去除收藏 / 关闭

三视图固定单张 16:9
- pipeline / library / model-photo / asset-factory / product-studio 全部同步
- 移除原 actor 3 列 3:4 拆图,改为单容器 16:9

图片工作台 (model-photo / platform-cover)
- 立即生成 + 全部重跑 + 单张重跑 均追加新批次到下方,旧批次保留
- 批量按钮下沉到每批次下方,与图片网格左对齐
- hover 重跑/采用 icon 缩小至 26px,右下角横向,无遮罩层
- 立即生成后不再自动新增「编辑中」草稿卡

新建商品 drawer
- 无 onSave 回调时默认跳转 product-detail
- 卖点新增 「+ 添加卖点」按钮(输入框下方独立行,左对齐)

product-detail
- 视频项目卡片状态 pill 改为 4 态(已完成/视频生成 4/6/已归档/故事板失败)
- 移除视频卡个体「通过/不通过/归档」状态切换
- 去掉冗余「通过」status 筛选;过滤逻辑兼容缺失按钮

sidebar (shell.js)
- 图片生成补 badge 12,团队去 badge

清理
- 删除 v2/ 历史镜像目录(与 电商AI平台/ 重复,Dockerfile build context 不依赖)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 21:12:03 +08:00

2222 lines
95 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>模特上身图 · 流·Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
<style>
/* viewport-fit · 工作台铺满 */
.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-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; }
}
.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; position: relative; }
/* hover 浮层 (重跑 + 采用) — 无遮罩,右下角横向小 icon */
.mp-r-overlay {
position: absolute; right: 8px; bottom: 8px;
display: flex; align-items: center;
gap: 6px;
opacity: 0; pointer-events: none;
transition: opacity var(--t-base);
z-index: 2;
}
.mp-result:hover .mp-r-overlay { opacity: 1; pointer-events: auto; }
.mp-r-overlay button {
width: 26px; height: 26px;
border-radius: 50%;
background: rgba(255,255,255,.96);
border: 1px solid var(--border-faint);
cursor: pointer;
display: grid; place-items: center;
color: var(--accent-black);
box-shadow: 0 2px 6px rgba(21,20,15,.16);
transition: background var(--t-base), color var(--t-base), transform var(--t-base);
}
.mp-r-overlay button:hover { background: var(--heat); color: #fff; border-color: var(--heat); transform: scale(1.06); }
.mp-r-overlay button svg { width: 12px; height: 12px; }
/* 已采用角标 */
.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-result.adopted .mp-r-overlay { display: none; }
.mp-result.regenerating .mp-r-thumb::before {
content: '生成中…';
position: absolute; inset: 0;
background: rgba(255,255,255,.7);
display: grid; place-items: center;
font-family: var(--font-mono); font-size: 11.5px;
color: var(--heat); letter-spacing: .04em;
z-index: 1;
}
/* 每个批次下方的批量操作 (胶囊按钮组 · 左对齐) */
.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-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; }
/* 模特详情 居中弹窗 — 参考布局 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; }
</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" 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 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>
</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">// 采用即扣费并入对应商品 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>
<div class="mp-pv-h" id="pv-summary">
<svg class="quote-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9 7c-2.76 0-5 2.24-5 5 0 1.84 1 3.45 2.48 4.32C5.99 17.36 4.99 18 4 18v2c3.31 0 6-2.69 6-6V8c0-.55-.45-1-1-1zm9 0c-2.76 0-5 2.24-5 5 0 1.84 1 3.45 2.48 4.32-.49 1.04-1.49 1.68-2.48 1.68v2c3.31 0 6-2.69 6-6V8c0-.55-.45-1-1-1z"/></svg>
<div class="pv-meta"><b id="pv-count">4</b> 张 · <b id="pv-ratio">1:1</b></div>
<div class="pv-line"><span class="k">商品</span><span class="v" id="pv-prod">未选择</span></div>
<div class="pv-line"><span class="k">模特</span><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="placeholder mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div></div>
<div class="mp-result placeholder-only"><div class="placeholder mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div></div>
<div class="mp-result placeholder-only"><div class="placeholder mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div></div>
<div class="mp-result placeholder-only"><div class="placeholder 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>// 任务进度可在 <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>
</div>
<div class="ml-scroll">
<div class="ml-grid" id="ml-grid">
<!-- 12 个模特卡片 (placeholder) -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== 模特详情 居中弹窗 (参考布局 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;
// ─── 已选商品 渲染 (单选) ───
function renderSelectedProds() {
const list = document.getElementById('prod-list');
const addBtn = document.getElementById('prod-add-btn');
const id = state.selectedProd;
const p = id ? PRODUCTS.find(x => x.id === id) : null;
if (!p) {
list.innerHTML = ''; // 不显示占位文字, 由 + 按钮提示
if (addBtn) addBtn.hidden = false;
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="swap" type="button" data-swap="${p.id}" title="切换商品" aria-label="切换商品">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><polyline points="7 23 3 19 7 15"/><path d="M21 5H9a4 4 0 0 0-4 4M3 19h12a4 4 0 0 0 4-4"/></svg>
</button>
<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();
});
list.querySelector('button.swap[data-swap]').addEventListener('click', () => {
addBtn.click(); // 复用 + 按钮的逻辑: 打开商品库
});
if (addBtn) addBtn.hidden = true;
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, '&quot;')}">
<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, '&quot;')}">
<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.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;
function buildResultCard(label) {
const div = document.createElement('div');
div.className = 'mp-result regenerating';
div.innerHTML = `
<div class="placeholder mp-r-thumb"><span class="ph-frame">${label || state.ratio}</span></div>
<div class="mp-r-overlay">
<button class="r-rerun" type="button" title="重跑这一张" aria-label="重跑">${RERUN_SVG}</button>
<button class="r-adopt" type="button" title="采用这一张" aria-label="采用">${ADOPT_SVG}</button>
</div>
<span class="adopt-badge">已采用</span>
`;
setTimeout(() => div.classList.remove('regenerating'), 1200);
div.querySelector('.r-rerun').addEventListener('click', e => { e.stopPropagation(); rerunOne(div); });
div.querySelector('.r-adopt').addEventListener('click', e => { e.stopPropagation(); adoptOne(div); });
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} · 单张重跑`);
batch.innerHTML = `
<div class="mp-batch-head">
<span class="lab ${labCls}">${labTxt}</span>
<span class="sep">·</span>
<span>${n} 张 · ${state.ratio}</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="重跑这一批">
${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>
`;
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('regenerating'); c.classList.add('adopted'); });
updateBatchSummary();
Shell.toast('已全部采用', cards.length + ' 张图入对应商品的 AI 素材 · 扣 ¥' + (cards.length * UNIT_PRICE).toFixed(2));
});
grid.appendChild(batch);
batch.scrollIntoView({ behavior: 'smooth', block: 'end' });
updateBatchSummary();
}
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('regenerating');
card.classList.add('adopted');
Shell.toast('已采用', '入对应商品的 AI 素材 · 扣 ¥' + UNIT_PRICE.toFixed(2));
updateBatchSummary();
}
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)');
if (hasReal) {
Shell.toast('已追加批次', state.count + ' 张图新增到下方 · 旧批次保留');
} else {
Shell.toast('已提交任务', '可在 [任务中心] 查看进度');
}
showPreviewContent();
renderResultCards(state.count);
});
// 批量按钮已下沉到每个批次内部 (.rerun-batch / .adopt-batch)
// 不再有全局 #pv-rerun-all / #pv-adopt-all
// ─── 模特库 全屏 ───
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');
});
// 左侧来源 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', () => {
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();
showPreviewEmpty(); // 默认新任务态 → 右侧显示空态
/* ============================================================
任务历史 (localStorage) · 仅在「立即生成」时创建任务
============================================================ */
(function () {
'use strict';
const TASK_TYPE = 'model';
const KEY = 'fs-image-tasks-' + TASK_TYPE;
let tasks = [];
let currentId = null;
let _draftSnap = null; // 临时新任务的状态缓存 (切到真实任务前保存, 切回时恢复)
function saveDraftSnap() {
_draftSnap = {
selectedProd: state.selectedProd,
selectedModel: state.selectedModel,
count: state.count,
ratio: state.ratio,
};
}
function resetPreviewToPlaceholder() {
const grid = document.getElementById('pv-grid');
if (grid) {
grid.innerHTML = '';
const wrap = document.createElement('div');
wrap.className = 'mp-result-batch placeholder-batch';
const inner = document.createElement('div');
inner.className = 'mp-result-grid';
for (let i = 0; i < 4; i++) {
const div = document.createElement('div');
div.className = 'mp-result placeholder-only';
div.innerHTML = `<div class="placeholder mp-r-thumb"><span class="ph-frame">待生成 · 1:1</span></div>`;
inner.appendChild(div);
}
wrap.appendChild(inner);
grid.appendChild(wrap);
}
_batchSeq = 0;
}
function applyDraftToForm() {
// 从 _draftSnap 恢复 (没缓存就用默认空态)
const snap = _draftSnap || { selectedProd: null, selectedModel: null, count: 4, ratio: '1:1' };
state.selectedProd = snap.selectedProd;
state.selectedModel = snap.selectedModel;
state.count = snap.count;
state.ratio = 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();
resetPreviewToPlaceholder();
showPreviewEmpty(); // 临时任务态: 右侧空态
}
function backToDraft() {
if (currentId === null) return; // 已经在 draft 态, 无需切换
applyDraftToForm();
currentId = null;
renderTasksList();
}
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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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;
// 临时新任务卡 — 仅在 draft 态 (currentId=null) 或有暂存的 _draftSnap 时显示
// 立即生成后 currentId 指向真实任务,_draftSnap 也未设置 → 不自动追加新草稿
const isDraft = (currentId === null);
const hasDraft = isDraft || _draftSnap !== null;
const draftHtml = hasDraft ? `
<div class="mp-task-card draft${isDraft ? ' active' : ''}" data-draft="1">
<div class="nm">新任务</div>
<div class="meta">
<span class="pill-mini gen">编辑中</span>
</div>
</div>
` : '';
const STATUS_LABEL = { gen: '生成中', ok: '已完成', err: '失败' };
const sorted = [...tasks].sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
const savedHtml = 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.innerHTML = draftHtml + savedHtml;
// draft 卡点击 → 切回临时态
const draftCard = root.querySelector('.mp-task-card.draft');
if (draftCard) draftCard.addEventListener('click', backToDraft);
// 真实任务卡点击 → loadTaskIntoForm
root.querySelectorAll('.mp-task-card:not(.draft)').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;
if (currentId === null) saveDraftSnap(); // 切走 draft 前保存当前编辑状态
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();
showPreviewContent(); // 真实任务: 显示预览内容
renderResultCards(state.count);// 重新渲染结果卡 (hover overlay + 批量 bar)
currentId = id;
renderTasksList();
}
/* ---------- 新建空白任务 (重置表单, 不写库) ---------- */
function newBlankTask() {
_draftSnap = null; // 清空之前的 draft 缓存
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();
resetPreviewToPlaceholder();
showPreviewEmpty(); // 新任务态: 右侧空态
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();
// URL ?taskId=xxx → 从任务中心跳过来时自动载入
(function applyUrlTaskId() {
const q = new URLSearchParams(location.search);
const tid = q.get('taskId');
if (tid && tasks.some(t => t.id === tid)) loadTaskIntoForm(tid);
})();
})();
</script>
</body>
</html>