AirShelf/电商AI平台/platform-cover.html
iye 04335f3269
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
feat(workbench): 三工序图片区视觉对齐 + 任务中心聚合 + 工具台头部筛选
- model-photo / platform-cover · 头部 toolbar 落地: 时间 / 模特(平台) chip 下拉 + 折叠搜索
- model-photo / platform-cover · 图片卡片样式同步图片创作 (.io-cell): bg / hover / .gen 脉冲 / .err 红框
- model-photo / platform-cover · 单图 hover overlay: 再次生成 + 下载 + 更多(加入资产库/删除)
- model-photo / platform-cover · 批次底栏: 再次生成图标统一 + 更多 menu(全部加入资产库/删除该批)
- model-photo · 修 TDZ bug: renderModelMini 调用挪到 MODELS 声明后, 解决整页崩溃
- model-photo · 去掉冗余 pv-summary, 商品自动选最近编辑, task 写入 name 字段
- image-optimize · 单图右上加再次生成图标, 加入 fs-image-tasks-image 与任务中心打通
- image-optimize · 输入区拆 3 行: + 在顶 / textarea 满宽 / 发送在底栏右; 参考图缩略与加号同 64×64
- asset-factory · 任务中心加时间 chip + image 类型 + 跳转表; 删冗余类型列
- pipeline · stage2 商品卡换商品库风格 + AI 生成三视图主 CTA + .tri-missing-badge[hidden] CSS 修复
2026-05-22 19:35:36 +08:00

2276 lines
91 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>
.app { height: 100vh; overflow: hidden; }
main { display: flex; flex-direction: column; min-height: 0; }
#page-content { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0; }
.pc-layout {
flex: 1; min-height: 0;
display: grid;
grid-template-columns: 260px 1fr;
}
@media (max-width: 1280px) {
.pc-layout { grid-template-columns: 240px 1fr; }
}
/* ─── 主区: flat 双区 · 用 border 分隔 ─── */
.pc-main {
display: flex; flex-direction: column;
min-height: 0;
overflow: hidden;
}
/* 主区头部 · toolbar 风格 (跟 image-optimize 一致) */
.pc-main-h {
flex-shrink: 0;
display: flex; align-items: center; gap: 10px;
padding: 12px 28px;
border-bottom: 1px solid var(--border-faint);
background: var(--surface);
}
.pc-main-h .cur-title {
display: flex; align-items: baseline; gap: 8px;
min-width: 0; max-width: 50%;
}
.pc-main-h .cur-title .crumb {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .04em;
flex-shrink: 0;
}
.pc-main-h .cur-title .nm {
font-size: 15px; font-weight: 600;
color: var(--accent-black);
letter-spacing: -.005em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pc-main-h .cur-title .nm.placeholder {
font-weight: 400; font-size: 13px;
color: var(--black-alpha-48);
}
.pc-main-h .spacer { flex: 1; }
.pc-main-h .search-btn {
width: 32px; height: 32px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
cursor: pointer;
display: grid; place-items: center;
transition: border-color var(--t-base), color var(--t-base);
}
.pc-main-h .search-btn:hover { border-color: var(--heat-20); color: var(--heat); }
.pc-main-h .search-btn svg { width: 14px; height: 14px; }
.pc-main-h .tb-chip {
display: inline-flex; align-items: center; gap: 6px;
height: 32px; padding: 0 10px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-size: 12.5px; color: var(--black-alpha-72);
font-family: inherit; cursor: pointer;
transition: border-color var(--t-base), color var(--t-base);
}
.pc-main-h .tb-chip:hover { border-color: var(--heat-20); color: var(--heat); }
.pc-main-h .tb-chip svg { width: 10px; height: 10px; opacity: .6; }
.pc-main-h .tb-chip.active {
background: var(--heat-12);
border-color: var(--heat-40);
color: var(--heat);
}
.pc-main-h .tb-chip.active svg { opacity: .8; }
/* search · 折叠图标态 + 展开输入框 */
.pc-main-h .tb-search-wrap { display: flex; align-items: center; }
.pc-main-h .tb-search-input {
width: 0; height: 32px;
padding: 0; margin: 0;
border: 1px solid transparent;
background: transparent;
border-radius: var(--r-sm);
font-size: 12.5px; color: var(--accent-black);
font-family: inherit; outline: none;
transition: width var(--t-base), padding var(--t-base), border-color var(--t-base), margin var(--t-base), background var(--t-base);
}
.pc-main-h .tb-search-wrap.expanded .tb-search-input {
width: 220px;
padding: 0 10px;
margin-left: 6px;
background: var(--surface);
border-color: var(--border-faint);
}
.pc-main-h .tb-search-wrap.expanded .tb-search-input:focus { border-color: var(--heat-40); }
.pc-main-h .tb-search-wrap.expanded .search-btn { border-color: var(--heat-40); color: var(--heat); }
/* dropdown · chip 下拉菜单 */
.pc-main-h .tb-menu-wrap { position: relative; }
.pc-main-h .tb-menu {
position: absolute;
top: calc(100% + 6px); right: 0;
min-width: 160px; max-width: 260px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
box-shadow: 0 8px 24px rgba(0,0,0,.08);
padding: 4px;
z-index: 50;
display: none;
max-height: 320px; overflow-y: auto;
}
.pc-main-h .tb-menu-wrap.open .tb-menu { display: block; }
.pc-main-h .tb-menu-item {
display: flex; align-items: center; gap: 8px;
width: 100%; padding: 7px 10px;
background: transparent; border: 0;
border-radius: 4px;
font-size: 12.5px; color: var(--black-alpha-72);
font-family: inherit; cursor: pointer; text-align: left;
transition: background var(--t-base), color var(--t-base);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pc-main-h .tb-menu-item:hover { background: var(--background-lighter); color: var(--accent-black); }
.pc-main-h .tb-menu-item.active {
background: var(--heat-12); color: var(--heat); font-weight: 500;
}
.pc-main-h .tb-menu-empty {
padding: 10px;
font-size: 11.5px; color: var(--black-alpha-48);
font-family: var(--font-mono); letter-spacing: .02em;
text-align: center;
}
/* 批次被筛选隐藏 */
.pv-platform-section[data-hidden="1"] { display: none !important; }
.pc-main-body {
flex: 1; min-height: 0;
display: grid;
grid-template-columns: 320px 1fr;
}
@media (max-width: 1280px) {
.pc-main-body { grid-template-columns: 300px 1fr; }
}
.pc-main-body > .pc-form {
border: 0; border-radius: 0;
border-right: 1px solid var(--border-faint);
background: var(--surface);
}
.pc-main-body > .pc-preview {
border: 0; border-radius: 0;
background: var(--background-base);
}
/* ─── 商品空间 (最左 · 单选 · 当前商品决定结果区批次) ─── */
.pc-prod-space {
background: var(--surface);
border-right: 1px solid var(--border-faint);
display: flex; flex-direction: column;
min-height: 0;
overflow: hidden;
}
/* 顶部工具栏: 返回 + 折叠 (跟 image-optimize 视觉一致) */
.pc-side-top {
flex-shrink: 0;
display: flex; align-items: center; gap: 8px;
padding: 14px 14px 10px;
border-bottom: 1px solid var(--border-faint);
}
.pc-side-top .back-pill {
display: inline-flex; align-items: center; gap: 6px;
height: 28px; padding: 0 12px 0 8px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-pill);
color: var(--accent-black);
font-size: 12.5px; font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.pc-side-top .back-pill:hover {
background: var(--heat-12);
border-color: var(--heat-20);
color: var(--heat);
}
.pc-side-top .back-pill svg { width: 14px; height: 14px; }
.pc-side-top .fold {
margin-left: auto;
width: 26px; height: 26px;
background: transparent; border: 0; border-radius: var(--r-sm);
display: grid; place-items: center;
color: var(--black-alpha-48); cursor: pointer;
transition: background var(--t-base), color var(--t-base);
}
.pc-side-top .fold:hover { background: var(--black-alpha-4); color: var(--accent-black); }
.pc-side-top .fold svg { width: 14px; height: 14px; }
.pc-ps-h {
flex-shrink: 0;
padding: 12px 14px 10px;
}
/* 商品列表 头部 (// 商品空间 + 新建商品 主 CTA · 右上显眼橙色) */
.pc-list-h {
flex-shrink: 0;
display: flex; align-items: center; gap: 8px;
padding: 4px 14px 10px;
}
.pc-list-h .mono {
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48); letter-spacing: .06em;
text-transform: uppercase;
}
.pc-list-h .new-prod {
margin-left: auto;
height: 28px; padding: 0 12px 0 10px;
display: inline-flex; align-items: center; gap: 6px;
background: var(--heat); color: #fff;
border: 1px solid var(--heat);
border-radius: var(--r-sm);
font-size: 12px; font-weight: 600;
font-family: inherit;
cursor: pointer;
box-shadow:
inset 0 -2px 4px rgba(250, 93, 25, 0.20),
0 1px 1px rgba(250, 93, 25, 0.12),
0 2px 4px rgba(250, 93, 25, 0.10);
transition: filter var(--t-base), transform var(--t-fast), box-shadow var(--t-base);
}
.pc-list-h .new-prod:hover {
filter: brightness(.96);
box-shadow:
inset 0 -2px 4px rgba(250, 93, 25, 0.20),
0 1px 1px rgba(250, 93, 25, 0.16),
0 4px 8px rgba(250, 93, 25, 0.20);
}
.pc-list-h .new-prod:active { transform: scale(.98); }
.pc-list-h .new-prod svg { width: 12px; height: 12px; }
.pc-ps-search {
position: relative;
height: 32px;
}
.pc-ps-search svg {
position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
width: 13px; height: 13px;
color: var(--black-alpha-48);
z-index: 2;
pointer-events: none;
}
.pc-ps-search input {
width: 100%; height: 100%;
padding: 0 10px 0 30px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-size: 12.5px; color: var(--accent-black);
font-family: inherit;
outline: none;
transition: border-color var(--t-base), background var(--t-base);
}
.pc-ps-search input:focus { border-color: var(--heat-40); background: var(--surface); }
.pc-ps-search input::placeholder { color: var(--black-alpha-48); }
.pc-ps-list {
flex: 1; min-height: 0;
overflow-y: auto;
padding: 4px 10px 10px;
display: flex; flex-direction: column; gap: 4px;
}
.pc-ps-empty {
padding: 24px 14px;
text-align: center;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .04em;
line-height: 1.7;
}
.pc-prod-item {
display: flex; align-items: center; gap: 10px;
padding: 8px;
border: 1px solid transparent;
border-radius: var(--r-sm);
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base);
}
.pc-prod-item:hover { background: var(--black-alpha-4); }
.pc-prod-item.active { background: var(--heat-12); border-color: var(--heat-20); }
.pc-prod-item .thumb {
flex-shrink: 0;
width: 36px; height: 36px;
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
overflow: hidden;
}
.pc-prod-item.active .thumb { border-color: var(--heat); }
.pc-prod-item .body { flex: 1; min-width: 0; }
.pc-prod-item .nm {
font-size: 12.5px;
color: var(--accent-black); font-weight: 500;
line-height: 1.3;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pc-prod-item.active .nm { color: var(--heat); font-weight: 600; }
.pc-prod-item .sub {
margin-top: 2px;
font-family: var(--font-mono); font-size: 10px;
color: var(--black-alpha-48); letter-spacing: .02em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pc-ps-all {
flex-shrink: 0;
margin: 0 10px 12px;
padding: 9px 12px;
background: var(--background-lighter);
border: 1px dashed var(--border-faint);
border-radius: var(--r-sm);
color: var(--black-alpha-72);
font-size: 12px;
font-family: inherit;
cursor: pointer;
display: flex; align-items: center; gap: 8px;
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
}
.pc-ps-all:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
.pc-ps-all .ct {
margin-left: auto;
color: var(--black-alpha-48);
font-family: var(--font-mono); font-size: 10.5px;
}
.pc-ps-all:hover .ct { color: var(--heat); }
.pc-ps-all svg { width: 12px; height: 12px; }
/* 左栏 表单 */
.pc-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;
}
.pc-step { margin-bottom: 22px; }
.pc-step:last-child { margin-bottom: 0; }
.pc-step-h { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.pc-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;
}
.pc-step-h .title { font-size: 14px; font-weight: 600; color: var(--accent-black); }
/* 商品选择 · 已选 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; border-radius: var(--r-sm); flex-shrink: 0; }
.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; }
/* 商品库全屏弹窗 */
.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; }
/* 编辑商品 drawer */
.pc-drawer { width: 720px; max-width: 100vw; z-index: 1101; }
.pc-drawer .drawer-b { padding: 24px 28px; }
#pc-drawer-bg.drawer-bg { z-index: 1100; }
.pc-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 18px; }
.pc-field-label { font-size: 13px; font-weight: 500; color: var(--accent-black); }
.pc-field-label .req { color: var(--accent-crimson); margin-left: 2px; }
.pc-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 18px; }
.pc-field-row > div { display: flex; flex-direction: column; gap: 6px; }
.pc-bullets {
list-style: none; padding: 0; margin: 0;
display: flex; flex-direction: column; gap: 6px;
}
.pc-bullets li {
display: flex; align-items: center; gap: 8px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
padding: 0 10px;
height: 36px;
}
.pc-bullets li.add { border-style: dashed; border-color: var(--heat-40); }
.pc-bullets li .num {
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); width: 18px; text-align: center;
flex-shrink: 0;
}
.pc-bullets li.add .num { color: var(--heat); }
.pc-bullets li input {
flex: 1; border: 0; background: transparent; outline: none;
font-size: 13px; color: var(--accent-black);
font-family: inherit;
}
.pc-bullets li input::placeholder { color: var(--black-alpha-48); }
.pc-bullets li .rm {
width: 22px; height: 22px;
background: transparent; border: 0; border-radius: var(--r-sm);
color: var(--black-alpha-48); cursor: pointer;
display: grid; place-items: center;
}
.pc-bullets li .rm:hover { color: var(--accent-crimson); background: var(--black-alpha-4); }
.pc-bullets li .rm svg { width: 11px; height: 11px; }
/* 商品图片 grid (对齐 product-detail .ov-images-grid) */
.pc-imgs {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
}
.pc-imgs .thumb {
aspect-ratio: 1 / 1;
border-radius: var(--r-sm);
background: var(--background-lighter);
border: 1px solid var(--border-faint);
position: relative;
overflow: hidden;
}
.pc-imgs .thumb .ph-frame {
position: absolute; inset: 0;
display: grid; place-items: center;
font-family: var(--font-mono); font-size: 10px;
color: var(--black-alpha-32);
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
}
.pc-imgs .thumb .rm {
position: absolute; top: 4px; right: 4px;
width: 18px; height: 18px;
background: rgba(0,0,0,.5);
color: #fff; border: 0;
border-radius: var(--r-sm);
display: grid; place-items: center;
cursor: pointer;
opacity: 0;
transition: opacity var(--t-base);
}
.pc-imgs .thumb:hover .rm { opacity: 1; }
.pc-imgs .thumb .rm svg { width: 10px; height: 10px; }
.pc-imgs .img-upload {
aspect-ratio: 1 / 1;
border-radius: var(--r-sm);
background: var(--heat-12);
border: 1.5px dashed var(--heat-40);
display: grid; place-items: center;
color: var(--heat);
cursor: pointer;
transition: background var(--t-base), border-color var(--t-base);
}
.pc-imgs .img-upload:hover { background: var(--heat-20); border-color: var(--heat); }
.pc-imgs .img-upload svg { width: 18px; height: 18px; }
.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; }
/* 平台多选卡片 */
.platform-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
.platform-card {
position: relative;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 10px 6px;
cursor: pointer;
text-align: center;
transition: background var(--t-base), border-color var(--t-base);
}
.platform-card:hover { background: var(--surface); }
.platform-card.selected { border-color: var(--heat); background: var(--heat-12); }
.platform-card .p-logo {
width: 32px; height: 32px;
margin: 0 auto 4px;
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--accent-white);
font-family: var(--font-mono);
font-size: 11px; font-weight: 700;
}
.platform-card .p-name {
font-size: 11.5px;
color: var(--accent-black);
font-weight: 500;
}
.platform-card.selected .p-name { color: var(--heat); }
.platform-card .p-check {
position: absolute; top: 4px; right: 4px;
width: 16px; height: 16px;
border-radius: 50%;
background: transparent;
border: 1.5px solid var(--black-alpha-24);
}
.platform-card.selected .p-check {
background: var(--heat); border-color: var(--heat);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23ffffff' stroke-width='2.5'%3E%3Cpolyline points='3 8 7 12 13 4'/%3E%3C/svg%3E");
background-position: center;
background-size: 10px 10px;
background-repeat: no-repeat;
border: 0;
}
/* 平台 logo 配色 */
.p-logo.dy { background: #000; }
.p-logo.tb { background: #ff6f00; }
.p-logo.tm { background: #ff0036; }
.p-logo.jd { background: #e1251b; }
.p-logo.pdd { background: #e02e24; }
.p-logo.xhs { background: #ff2741; }
.p-logo.ks { background: #ff4906; }
.p-logo.sph { background: #07c160; }
.p-logo.amz { background: #ff9900; }
.p-logo.al { background: #2c4af1; }
/* pill row */
.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; }
.pc-sub-h { font-size: 12px; color: var(--black-alpha-48); margin-bottom: 6px; font-family: var(--font-mono); letter-spacing: .02em; }
.pc-sub { margin-bottom: 12px; }
.pc-sub:last-child { margin-bottom: 0; }
/* CTA */
.pc-cta { margin-top: auto; padding-top: 14px; }
.pc-cta .btn-gen {
width: 100%; justify-content: center;
padding: 12px; font-size: 14px; font-weight: 600;
}
.pc-cta .btn-gen.disabled { opacity: .45; cursor: not-allowed; pointer-events: none; }
.pc-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;
}
/* 右栏 预览 */
.pc-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) */
.pc-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;
}
.pc-pv-h .quote-icon {
position: absolute;
top: 13px; left: 16px;
width: 18px; height: 18px;
color: var(--black-alpha-24);
}
.pc-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;
}
.pc-pv-h .pv-meta b { color: var(--accent-black); font-weight: 600; }
.pc-pv-h .pv-line {
font-size: 13px;
color: var(--accent-black);
line-height: 1.6;
display: flex; align-items: center;
}
.pc-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;
}
.pc-pv-h .pv-line .v { font-weight: 500; }
.pc-pv-h .pv-line .swap {
margin-left: 10px;
font-size: 11.5px; color: var(--heat);
cursor: pointer;
}
.pc-pv-h .pv-line .swap:hover { text-decoration: underline; }
.pv-platform-section { margin-bottom: 24px; }
.pv-platform-section:last-child { margin-bottom: 0; }
.pv-platform-section .ps-h {
display: flex; align-items: center; gap: 8px;
margin-bottom: 10px;
font-size: 13px; font-weight: 600; color: var(--accent-black);
}
.pv-platform-section .ps-h .ct {
margin-left: auto; font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); letter-spacing: .02em; font-weight: 400;
}
.pv-platform-section .ps-h .batch-lab {
font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;
padding: 2px 8px; border-radius: var(--r-pill); letter-spacing: .04em;
}
.pv-platform-section .ps-h .batch-lab.gen { background: var(--background-lighter); color: var(--black-alpha-56); }
.pv-platform-section .ps-h .batch-lab.rerun { background: var(--heat-12); color: var(--heat); }
.pv-platform-section .ps-h .sep { color: var(--black-alpha-32); font-weight: 400; }
.pv-platform-section .ps-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
@media (max-width: 1400px) {
.pv-platform-section .ps-grid { gap: 10px; }
}
/* 结果卡片 · 与图片创作 .io-cell 视觉一致 */
.pv-platform-section .ps-grid .mp-result {
position: relative;
aspect-ratio: 1;
border-radius: var(--r-md);
overflow: hidden;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
cursor: pointer;
transition: border-color var(--t-base);
}
.pv-platform-section .ps-grid .mp-result:hover { border-color: var(--black-alpha-32); }
.pv-platform-section .ps-grid .mp-result .mp-r-thumb { position: absolute; inset: 0; }
.pv-platform-section .ps-grid .mp-result .mp-r-thumb .ph-frame {
position: absolute; inset: 0;
display: grid; place-items: center;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-32); letter-spacing: .02em;
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
}
.pv-platform-section .ps-grid .mp-result.gen .mp-r-thumb .ph-frame { animation: pc-pulse 1.4s ease-in-out infinite; }
@keyframes pc-pulse {
0%, 100% { opacity: 1; }
50% { opacity: .55; }
}
.pv-platform-section .ps-grid .mp-result.err { border-color: var(--accent-crimson, #c43d3d); }
.pv-platform-section .ps-grid .mp-result.err .mp-r-thumb .ph-frame {
color: var(--accent-crimson, #c43d3d);
background: rgba(196, 61, 61, .05);
}
/* 右上 hover 操作组 */
.pv-platform-section .ps-grid .mp-result .cell-ops {
position: absolute; top: 6px; right: 6px;
display: flex; gap: 4px;
opacity: 0;
transition: opacity var(--t-base);
z-index: 2;
}
.pv-platform-section .ps-grid .mp-result:hover .cell-ops { opacity: 1; }
.pv-platform-section .ps-grid .mp-result .cell-ops button {
width: 26px; height: 26px;
background: rgba(255, 255, 255, .92);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
color: var(--accent-black);
cursor: pointer;
display: grid; place-items: center;
backdrop-filter: blur(4px);
transition: border-color var(--t-base), color var(--t-base);
}
.pv-platform-section .ps-grid .mp-result .cell-ops button:hover { border-color: var(--heat); color: var(--heat); }
.pv-platform-section .ps-grid .mp-result .cell-ops button svg { width: 12px; height: 12px; }
.pv-platform-section .ps-grid .mp-result .cell-more-wrap { position: relative; }
.pv-platform-section .ps-grid .mp-result .cell-more-menu {
position: absolute; top: calc(100% + 4px); right: 0;
min-width: 132px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 6px 24px rgba(0,0,0,.10);
padding: 4px;
display: none;
z-index: 12;
}
.pv-platform-section .ps-grid .mp-result .cell-more-wrap.open .cell-more-menu { display: block; }
.pv-platform-section .ps-grid .mp-result .cell-more-menu button {
width: 100%;
display: inline-flex !important; align-items: center; gap: 8px;
padding: 7px 10px !important;
background: transparent !important;
border: 0 !important; border-radius: var(--r-sm) !important;
font-size: 12.5px;
color: var(--accent-black) !important;
font-family: inherit;
text-align: left;
cursor: pointer;
backdrop-filter: none !important;
height: auto !important;
justify-content: flex-start !important;
}
.pv-platform-section .ps-grid .mp-result .cell-more-menu button:hover {
background: var(--background-lighter) !important;
color: var(--heat) !important;
}
.pv-platform-section .ps-grid .mp-result .cell-more-menu button.danger:hover {
color: var(--accent-crimson) !important;
background: var(--crimson-bg, #fdebea) !important;
}
.pv-platform-section .ps-grid .mp-result .cell-more-menu button svg { width: 13px !important; height: 13px !important; }
/* 已采用角标 */
.pv-platform-section .ps-grid .mp-result .adopt-badge {
position: absolute; top: 6px; left: 6px;
background: var(--accent-forest, #42c366);
color: #fff;
font-family: var(--font-mono); font-size: 10px; font-weight: 600;
letter-spacing: .04em;
padding: 1px 6px;
border-radius: var(--r-pill);
z-index: 3;
display: none;
}
.pv-platform-section .ps-grid .mp-result.adopted .adopt-badge { display: inline-block; }
/* 每批次下方的批量操作 (胶囊按钮组 · 左对齐) */
.pc-pv-batch {
margin-top: 10px;
display: flex; gap: 10px; align-items: center; justify-content: flex-start;
}
.pc-pv-batch[hidden] { display: none; }
.pv-platform-section .pc-pv-batch.batch-foot { margin-top: 10px; }
/* 预览区空态 (新任务且未生成) */
.pc-pv-empty-state {
flex: 1;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center;
padding: 40px 24px;
gap: 6px;
}
.pc-pv-empty-state[hidden] { display: none; }
.pc-pv-empty-state .mono {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--black-alpha-48);
letter-spacing: .06em;
margin-bottom: 4px;
}
.pc-pv-empty-state .title {
font-size: 14px;
font-weight: 600;
color: var(--accent-black);
}
.pc-pv-empty-state .hint {
font-size: 12.5px;
color: var(--black-alpha-48);
line-height: 1.6;
max-width: 320px;
}
.pc-pv-empty-state .hint b { color: var(--heat); font-weight: 600; }
/* 预览区 hidden 时收起所有内容元素 */
#pv-summary[hidden], #pv-results[hidden], #pv-foot[hidden] { display: none; }
.pc-pv-batch .summary {
margin-right: auto;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.pc-pv-batch .summary b { color: var(--heat); font-weight: 700; }
.pc-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);
}
.pc-pv-batch .pill-btn:hover { background: var(--background-lighter); border-color: var(--black-alpha-24); }
.pc-pv-batch .pill-btn.primary {
background: var(--heat); color: #fff; border-color: var(--heat);
}
.pc-pv-batch .pill-btn.primary:hover { filter: brightness(.94); }
.pc-pv-batch .pill-btn svg { width: 13px; height: 13px; }
.pc-pv-batch .pill-btn.icon { width: 34px; padding: 0; justify-content: center; }
/* 批次更多气泡 */
.pc-pv-batch .batch-more-wrap { position: relative; display: inline-flex; }
.pc-pv-batch .batch-more-menu {
position: absolute; bottom: calc(100% + 6px); right: 0;
min-width: 168px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
box-shadow: 0 6px 24px rgba(0,0,0,.10);
padding: 4px;
display: none;
z-index: 12;
}
.pc-pv-batch .batch-more-wrap.open .batch-more-menu { display: block; }
.pc-pv-batch .batch-more-menu button {
width: 100%;
display: inline-flex !important; align-items: center; gap: 8px;
height: auto !important;
padding: 7px 10px !important;
background: transparent !important;
border: 0 !important; border-radius: var(--r-sm) !important;
font-size: 12.5px;
color: var(--accent-black) !important;
text-align: left;
justify-content: flex-start !important;
cursor: pointer; font-family: inherit;
}
.pc-pv-batch .batch-more-menu button:hover {
background: var(--background-lighter) !important;
color: var(--heat) !important;
}
.pc-pv-batch .batch-more-menu button.danger:hover {
color: var(--accent-crimson) !important;
background: var(--crimson-bg, #fdebea) !important;
}
.pc-pv-batch .batch-more-menu button svg { width: 13px !important; height: 13px !important; flex-shrink: 0; }
.pc-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;
}
.pc-pv-foot a { color: var(--heat); }
/* 空状态 */
.pc-pv-empty {
flex: 1;
display: grid; place-items: center;
color: var(--black-alpha-48);
font-size: 13px;
text-align: center;
}
.pc-pv-empty .mono {
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); margin-top: 6px;
letter-spacing: .04em;
}
@media (max-width: 1100px) {
.pc-layout { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div id="page">
<div class="pc-layout">
<!-- ===== 最左栏 · 商品空间 (单选 · 当前商品决定结果区批次) ===== -->
<aside class="pc-prod-space" id="prod-space">
<!-- 顶部 · 返回 + 折叠 (跟图片创作风格一致) -->
<div class="pc-side-top">
<button class="back-pill" type="button" onclick="history.length > 1 ? history.back() : location.href='asset-factory.html'" title="返回">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
<span>返回</span>
</button>
<button class="fold" type="button" title="折叠侧栏" style="margin-left:auto">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/></svg>
</button>
</div>
<div class="pc-ps-h">
<div class="pc-ps-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" id="ps-search-input" placeholder="搜索商品 / 分类">
</div>
</div>
<!-- 商品列表 标题行 · 右上显眼新建按钮 -->
<div class="pc-list-h">
<span class="mono">// 商品空间</span>
<button class="new-prod" type="button" id="ps-new-btn" title="新建商品">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
<span>新建商品</span>
</button>
</div>
<div class="pc-ps-list" id="ps-list"></div>
<button class="pc-ps-all" type="button" id="ps-all-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M9 4v16"/></svg>
<span>全部商品</span>
<span class="ct" id="ps-all-ct">0 个</span>
<svg style="margin-left:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
</aside>
<!-- ===== 主区 · 头部 + 参数/结果 双栏 ===== -->
<section class="pc-main">
<!-- 主区顶部 · toolbar (商品标题 + 搜索 + 筛选 · 跟图片创作一致) -->
<div class="pc-main-h">
<div class="cur-title">
<span class="crumb">// 商品空间</span>
<span class="nm placeholder" id="cur-prod-nm">未选择 · 请在左侧商品空间选一个</span>
</div>
<span class="spacer"></span>
<div class="tb-search-wrap" id="pc-search-wrap">
<button class="search-btn" type="button" title="搜索批次/平台" id="pc-search-toggle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
</button>
<input type="text" class="tb-search-input" id="pc-search-input" placeholder="搜索批次/平台…" autocomplete="off">
</div>
<div class="tb-menu-wrap" data-filter="time">
<button class="tb-chip" type="button" id="pc-chip-time">
<span class="lbl">时间</span>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="tb-menu" id="pc-menu-time" role="listbox" aria-labelledby="pc-chip-time">
<button class="tb-menu-item active" type="button" data-val="all">全部时间</button>
<button class="tb-menu-item" type="button" data-val="today">今天</button>
<button class="tb-menu-item" type="button" data-val="1h">1 小时内</button>
<button class="tb-menu-item" type="button" data-val="10min">10 分钟内</button>
</div>
</div>
<div class="tb-menu-wrap" data-filter="platform">
<button class="tb-chip" type="button" id="pc-chip-platform">
<span class="lbl">平台</span>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="tb-menu" id="pc-menu-platform" role="listbox" aria-labelledby="pc-chip-platform">
<button class="tb-menu-item active" type="button" data-val="all">全部平台</button>
<div class="tb-menu-empty">暂无批次,生成后可按平台筛选</div>
</div>
</div>
</div>
<div class="pc-main-body">
<!-- 左 · 参数 -->
<div class="pc-form">
<!-- ① 选择平台 (单选, 10 个热门电商平台) -->
<div class="pc-step">
<div class="pc-step-h">
<span class="num">1</span>
<span class="title">选择平台</span>
</div>
<div class="platform-grid" id="platform-grid">
<div class="platform-card" data-id="dy" data-name="抖音电商">
<div class="p-check"></div>
<div class="p-logo dy"></div>
<div class="p-name">抖音电商</div>
</div>
<div class="platform-card" data-id="tb" data-name="淘宝">
<div class="p-check"></div>
<div class="p-logo tb"></div>
<div class="p-name">淘宝</div>
</div>
<div class="platform-card" data-id="tm" data-name="天猫">
<div class="p-check"></div>
<div class="p-logo tm"></div>
<div class="p-name">天猫</div>
</div>
<div class="platform-card" data-id="jd" data-name="京东">
<div class="p-check"></div>
<div class="p-logo jd"></div>
<div class="p-name">京东</div>
</div>
<div class="platform-card" data-id="pdd" data-name="拼多多">
<div class="p-check"></div>
<div class="p-logo pdd"></div>
<div class="p-name">拼多多</div>
</div>
<div class="platform-card" data-id="xhs" data-name="小红书">
<div class="p-check"></div>
<div class="p-logo xhs"></div>
<div class="p-name">小红书</div>
</div>
<div class="platform-card" data-id="ks" data-name="快手">
<div class="p-check"></div>
<div class="p-logo ks"></div>
<div class="p-name">快手</div>
</div>
<div class="platform-card" data-id="sph" data-name="视频号">
<div class="p-check"></div>
<div class="p-logo sph"></div>
<div class="p-name">视频号</div>
</div>
<div class="platform-card" data-id="amz" data-name="亚马逊">
<div class="p-check"></div>
<div class="p-logo amz">a</div>
<div class="p-name">亚马逊</div>
</div>
<div class="platform-card" data-id="al" data-name="1688">
<div class="p-check"></div>
<div class="p-logo al"></div>
<div class="p-name">1688</div>
</div>
</div>
</div>
<!-- ② 生成设置 -->
<div class="pc-step">
<div class="pc-step-h">
<span class="num">2</span>
<span class="title">生成设置</span>
</div>
<div class="pc-sub">
<div class="pc-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>
<!-- 底部 立即生成 -->
<div class="pc-cta">
<button class="btn btn-primary btn-gen" id="pc-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">¥4.00</span>)
</button>
<div class="pc-cta-hint">// 满意后点 [入资产库] 才扣费 · 失败不扣</div>
</div>
</div>
<!-- ===== 右栏 · 预览 ===== -->
<div class="pc-preview" id="pc-preview">
<!-- 空态(新任务态 & 还没立即生成时显示) -->
<div class="pc-pv-empty-state" id="pv-empty">
<div class="mono">// EMPTY STATE</div>
<div class="title">还没有生成结果</div>
<div class="hint">先选商品、选平台,点击 <b>立即生成</b> 后,效果图会出现在这里</div>
</div>
<div class="pc-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></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-platforms">未选择</span></div>
</div>
<div id="pv-results">
<!-- 默认占位 -->
</div>
<div class="pc-pv-foot" id="pv-foot">
// 采用即扣费并入对应商品的 <a href="products.html">AI 素材库 →</a>;未采用的图不扣费、不保存
<br>// 切换左侧商品空间 · 查看其他商品的批次记录
</div>
</div>
</div><!-- /.pc-main-body -->
</section><!-- /.pc-main -->
</div>
</div>
<!-- ===== 编辑商品 drawer (在商品库内点编辑触发,prefilled) ===== -->
<div class="drawer-bg" id="pc-drawer-bg"></div>
<aside class="drawer pc-drawer" id="pc-drawer" role="dialog" aria-label="编辑商品" aria-hidden="true">
<div class="drawer-h">
<h3 id="pc-drawer-title">编辑商品</h3>
<button class="x" type="button" id="pc-drawer-close" aria-label="关闭">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg>
</button>
</div>
<div class="drawer-b">
<div class="pc-field">
<label class="pc-field-label">商品名称<span class="req">*</span></label>
<input class="input" id="pcf-name" placeholder="请输入商品名称(必填)" maxlength="100">
</div>
<div class="pc-field-row">
<div>
<label class="pc-field-label">品类<span class="req">*</span></label>
<select class="select" id="pcf-cat">
<option>美妆个护</option>
<option>服饰内衣</option>
<option>食品饮料</option>
<option>家居家电</option>
<option>数码 3C</option>
<option>个护清洁</option>
<option>运动户外</option>
<option>母婴亲子</option>
</select>
</div>
<div>
<label class="pc-field-label">目标人群<span style="color:var(--black-alpha-48);margin-left:2px">(选填)</span></label>
<input class="input" id="pcf-target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
</div>
</div>
<div class="pc-field">
<label class="pc-field-label">商品图片<span style="color:var(--black-alpha-48);margin-left:2px">(<span id="pcf-imgs-ct">6</span>)</span></label>
<div class="pc-imgs" id="pcf-imgs"></div>
</div>
<div class="pc-field">
<label class="pc-field-label">核心卖点<span class="req">*</span></label>
<ul class="pc-bullets" id="pcf-bullets">
<li class="add"><span class="num">+</span><input id="pcf-add-input" placeholder="添加新卖点 · 回车确认"></li>
</ul>
</div>
</div>
<div class="drawer-f">
<button class="btn" type="button" id="pc-cancel-btn">取消</button>
<button class="btn btn-primary" type="button" id="pc-save-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
保存修改
</button>
</div>
</aside>
<!-- ===== 商品库 全屏(无遮罩自适应,多选) ===== -->
<div class="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">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"></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>
<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
selectedPlatform: null, // string | null
count: 4,
};
const UNIT_PRICE = 0.50;
// ─── 商品空间 (左侧栏) 渲染 ───
let _psQuery = '';
function renderProdSpace() {
const listEl = document.getElementById('ps-list');
const ctEl = document.getElementById('ps-count');
const allCtEl = document.getElementById('ps-all-ct');
if (!listEl) return;
if (ctEl) ctEl.textContent = PRODUCTS.length;
if (allCtEl) allCtEl.textContent = PRODUCTS.length + ' 个';
const q = _psQuery.trim();
const filtered = q
? PRODUCTS.filter(p => p.name.includes(q) || p.cat.includes(q))
: PRODUCTS;
if (!filtered.length) {
listEl.innerHTML = `<div class="pc-ps-empty">// NO MATCH<br>试试其他关键词</div>`;
return;
}
listEl.innerHTML = filtered.map(p => `
<div class="pc-prod-item${state.selectedProd === p.id ? ' active' : ''}" data-id="${p.id}">
<div class="placeholder thumb"></div>
<div class="body">
<div class="nm">${p.name}</div>
<div class="sub">// ${p.cat}</div>
</div>
</div>
`).join('');
listEl.querySelectorAll('.pc-prod-item').forEach(el => {
el.addEventListener('click', () => selectProduct(el.dataset.id));
});
}
// 选中商品 (sidebar 单选 · 同步更新表单/预览/Cost)
function selectProduct(id) {
state.selectedProd = id;
document.querySelectorAll('.pc-prod-item').forEach(el => {
el.classList.toggle('active', el.dataset.id === id);
});
updateCurProdHeader();
if (typeof renderBatchesForCurrentProd === 'function') renderBatchesForCurrentProd();
const p = PRODUCTS.find(x => x.id === id);
document.getElementById('pv-prod').textContent = p ? p.name : '未选择';
updateCost();
renderPreviewSections();
}
// 当前商品 header strip
function updateCurProdHeader() {
const nmEl = document.getElementById('cur-prod-nm');
const statsEl = document.getElementById('cur-prod-stats');
const batchesEl = document.getElementById('cur-prod-batches');
if (!nmEl) return;
const p = state.selectedProd ? PRODUCTS.find(x => x.id === state.selectedProd) : null;
if (!p) {
nmEl.textContent = '未选择 · 请在左侧商品空间选一个';
nmEl.classList.add('placeholder');
if (statsEl) statsEl.hidden = true;
} else {
nmEl.textContent = p.name;
nmEl.classList.remove('placeholder');
const ct = (window._countBatchesForProd ? window._countBatchesForProd(p.id) : 0);
if (batchesEl) batchesEl.textContent = ct;
if (statsEl) statsEl.hidden = false;
}
}
// 保留旧函数名 alias (兼容旧 call site)
function renderSelectedProds() {
renderProdSpace();
updateCurProdHeader();
const p = state.selectedProd ? PRODUCTS.find(x => x.id === state.selectedProd) : null;
document.getElementById('pv-prod').textContent = p ? p.name : '未选择';
updateCost();
renderPreviewSections();
}
// ─── 商品库全屏弹窗 (单选) ───
let _plDraft = 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;
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 数据) ───
const PRODUCT_EXTRA = {
p1: { target: '熬夜党 · 25-35 岁女性 · 敏感肌', bullets: ['72h 长效补水', '官方授权正品', '通勤补妆神器'], imgs: 6 },
p2: { target: '通勤党 · 18-30 岁 · 大学生 / 白领', bullets: ['主动降噪 35dB', '蓝牙 5.4 双设备', '32h 续航'], imgs: 6 },
p3: { target: '加班党 · 独居青年 · 一人食场景', bullets: ['一杯水即可', '原切牛肉块充足', '6 桶大箱装'], imgs: 6 },
p4: { target: '通勤防晒 · 油皮 / 敏感肌', bullets: ['SPF50+ PA++++', '物理防晒不刺激', '清透不假白'], imgs: 6 },
p5: { target: '咖啡入门 · 早八党 · 加班族', bullets: ['冷热水即溶', '原产地豆精选', '24 颗精装'], imgs: 6 },
p6: { target: '小户型 · 健康饮食 · 新手厨房', bullets: ['4L 大容量', '可视玻璃观察', '一键预设'], imgs: 6 },
p7: { target: '健身房 · 通勤穿搭 · 18-32 岁女性', bullets: ['裸感面料', '高弹收腹', '亲肤透气'], imgs: 6 },
};
// 渲染商品图片 grid · n 张占位 + 上传按钮
function renderProdImgs(n) {
const grid = document.getElementById('pcf-imgs');
const ct = document.getElementById('pcf-imgs-ct');
if (!grid) return;
if (ct) ct.textContent = n;
let html = '';
for (let i = 0; i < n; i++) {
html += `<div class="thumb"><span class="ph-frame">1:1</span><button class="rm" type="button" title="删除" data-idx="${i}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button></div>`;
}
html += `<div class="img-upload" id="pcf-img-add" title="上传图片"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg></div>`;
grid.innerHTML = html;
grid.querySelectorAll('.thumb .rm').forEach(btn => {
btn.addEventListener('click', () => {
btn.closest('.thumb').remove();
if (ct) ct.textContent = grid.querySelectorAll('.thumb').length;
});
});
const addBtn = document.getElementById('pcf-img-add');
if (addBtn) addBtn.addEventListener('click', () => {
if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('上传图片', '// 演示版暂不支持真实上传');
});
}
let _editingProdId = null;
function openEditProductDrawer(id) {
const p = PRODUCTS.find(x => x.id === id);
if (!p) return;
_editingProdId = id;
document.getElementById('pc-drawer-title').textContent = '编辑商品 · ' + p.name;
document.getElementById('pcf-name').value = p.name;
document.getElementById('pcf-cat').value = p.cat;
const extra = PRODUCT_EXTRA[id] || { target: '', bullets: [], imgs: 0 };
document.getElementById('pcf-target').value = extra.target || '';
// 渲染商品图片 (n 张占位)
renderProdImgs(typeof extra.imgs === 'number' ? extra.imgs : 6);
const ul = document.getElementById('pcf-bullets');
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;
}
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; }
const p = PRODUCTS.find(x => x.id === _editingProdId);
if (p) { p.name = newName; p.cat = newCat; }
const bullets = [...document.querySelectorAll('#pcf-bullets li:not(.add) input')].map(i => i.value.trim()).filter(Boolean);
const imgs = document.querySelectorAll('#pcf-imgs .thumb').length;
PRODUCT_EXTRA[_editingProdId] = { target: newTarget, bullets, imgs };
Shell.toast('已保存', newName);
closeEditProductDrawer();
renderProdLib();
renderSelectedProds();
});
// 全部商品 入口 (左侧栏底部 · 打开商品库 modal)
function openProdLibModal() {
_plDraft = state.selectedProd;
_plCatFilter = '';
_plQuery = '';
document.getElementById('pl-search-input').value = '';
document.querySelectorAll('.pl-side-item').forEach(x => x.classList.toggle('active', x.dataset.cat === ''));
renderProdLib();
document.getElementById('pl-modal-bg').classList.add('show');
}
document.getElementById('ps-all-btn').addEventListener('click', openProdLibModal);
// 商品空间 · 搜索框 · 新建按钮
document.getElementById('ps-search-input').addEventListener('input', e => {
_psQuery = e.target.value;
renderProdSpace();
});
document.getElementById('ps-new-btn').addEventListener('click', () => {
if (!window.NewProductDrawer) { Shell.toast('Drawer 未加载'); return; }
window.NewProductDrawer.open({
onSave: function (p) {
const product = {
id: p.id,
name: p.name,
cat: p.cat,
meta: (p.target ? p.target.split(/[ ,、、]+/)[0] : '新建') + ' · ' + p.imgs + ' 张图',
};
PRODUCTS.unshift(product);
renderProdSpace();
selectProduct(product.id);
Shell.toast('已加入商品库', '+ ' + product.name);
}
});
});
document.getElementById('pl-close-btn').addEventListener('click', () => {
document.getElementById('pl-modal-bg').classList.remove('show');
});
document.getElementById('pl-cancel-btn').addEventListener('click', () => {
document.getElementById('pl-modal-bg').classList.remove('show');
});
document.getElementById('pl-confirm-btn').addEventListener('click', () => {
if (!_plDraft) { Shell.toast('请先选择商品', '只能选 1 个'); return; }
document.getElementById('pl-modal-bg').classList.remove('show');
selectProduct(_plDraft);
});
document.getElementById('pl-new-btn').addEventListener('click', () => {
if (!window.NewProductDrawer) { Shell.toast('Drawer 未加载'); return; }
window.NewProductDrawer.open({
onSave: function (p) {
const product = {
id: p.id,
name: p.name,
cat: p.cat,
meta: (p.target ? p.target.split(/[ ,、、]+/)[0] : '新建') + ' · ' + p.imgs + ' 张图',
};
PRODUCTS.unshift(product);
_plDraft = product.id;
_plCatFilter = '';
_plQuery = '';
const searchInput = document.getElementById('pl-search-input');
if (searchInput) searchInput.value = '';
renderProdLib();
renderProdSpace();
selectProduct(product.id);
Shell.toast('已加入商品库', '+ ' + product.name);
}
});
});
document.querySelectorAll('.pl-side-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.pl-side-item').forEach(x => x.classList.remove('active'));
item.classList.add('active');
_plCatFilter = item.dataset.cat;
renderProdLib();
});
});
document.getElementById('pl-search-input').addEventListener('input', e => {
_plQuery = e.target.value.trim();
renderProdLib();
});
// 平台单选
function updatePlatforms() {
const id = state.selectedPlatform;
const c = id ? document.querySelector('.platform-card[data-id="' + id + '"]') : null;
document.getElementById('pv-platforms').textContent = c ? c.dataset.name : '未选择';
updateCost();
renderPreviewSections();
}
function updateCost() {
const hasProd = !!state.selectedProd;
const hasPlat = !!state.selectedPlatform;
const total = (hasProd && hasPlat ? 1 : 0) * state.count * UNIT_PRICE;
document.getElementById('cost-total').textContent = '¥' + total.toFixed(2);
const btn = document.getElementById('pc-go-btn');
if (!hasProd || !hasPlat) btn.classList.add('disabled');
else btn.classList.remove('disabled');
document.getElementById('pv-count').textContent = state.count + ' 张';
}
document.querySelectorAll('.platform-card').forEach(card => {
card.addEventListener('click', () => {
const id = card.dataset.id;
state.selectedPlatform = (state.selectedPlatform === id) ? null : id;
document.querySelectorAll('.platform-card').forEach(c =>
c.classList.toggle('selected', c.dataset.id === state.selectedPlatform)
);
updatePlatforms();
});
});
// 数量
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');
state.count = +btn.dataset.val;
updateCost();
renderPreviewSections();
});
});
// ─── 预览区空态 / 内容 切换 ───
function showPreviewEmpty() {
const empty = document.getElementById('pv-empty');
const sum = document.getElementById('pv-summary');
const results = document.getElementById('pv-results');
const foot = document.getElementById('pv-foot');
if (empty) empty.hidden = false;
if (sum) sum.hidden = true;
if (results) results.hidden = true;
if (foot) foot.hidden = true;
}
function showPreviewContent() {
const empty = document.getElementById('pv-empty');
const sum = document.getElementById('pv-summary');
const results = document.getElementById('pv-results');
const foot = document.getElementById('pv-foot');
if (empty) empty.hidden = true;
if (sum) sum.hidden = false;
if (results) results.hidden = false;
if (foot) foot.hidden = false;
// pv-batch 由 renderResultCards 单独控制
}
// 渲染右侧预览 (占位 — 切平台/数量时显示)
function renderPreviewSections() {
const container = document.getElementById('pv-results');
const id = state.selectedPlatform;
const c = id ? document.querySelector('.platform-card[data-id="' + id + '"]') : null;
if (!c) {
container.innerHTML = '<div class="pc-pv-empty">请先选择平台<div class="mono">// PICK A PLATFORM</div></div>';
return;
}
container.innerHTML = `
<div class="pv-platform-section">
<div class="ps-h">
<span>${c.dataset.name} 套图预览</span>
<span class="ct">${state.count} 张</span>
</div>
<div class="ps-grid">
${Array(state.count).fill(0).map(() => `
<div class="mp-result placeholder-only">
<div class="mp-r-thumb"><span class="ph-frame">待生成</span></div>
</div>
`).join('')}
</div>
</div>
`;
}
// 渲染生成结果 (点立即生成时调,带 hover overlay + 批量 bar)
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>';
const CELL_RERUN_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 11-3-6.7L21 8M21 3v5h-5"/></svg>';
const CELL_DL_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>';
const CELL_MORE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></svg>';
const CELL_ADOPT_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>';
const CELL_DEL_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg>';
let _batchSeq = 0;
function appendBatch(n, kind) {
const container = document.getElementById('pv-results');
const id = state.selectedPlatform;
const c = id ? document.querySelector('.platform-card[data-id="' + id + '"]') : null;
if (!c) return;
_batchSeq += 1;
const ts = new Date();
const tsStr = `${String(ts.getHours()).padStart(2,'0')}:${String(ts.getMinutes()).padStart(2,'0')}:${String(ts.getSeconds()).padStart(2,'0')}`;
const labCls = kind === 'gen' ? 'gen' : 'rerun';
const labTxt = kind === 'gen' ? `批次 ${_batchSeq} · 初始生成` : (kind === 'rerun-all' ? `批次 ${_batchSeq} · 全部重跑` : `批次 ${_batchSeq} · 单张重跑`);
const section = document.createElement('div');
section.className = 'pv-platform-section';
section.dataset.kind = kind;
section.dataset.ts = String(ts.getTime());
section.dataset.platformId = id || '';
section.dataset.platformName = c.dataset.name || '';
section.dataset.search = [labTxt, c.dataset.name || '', n + '张'].join(' ').toLowerCase();
section.innerHTML = `
<div class="ps-h">
<span class="batch-lab ${labCls}">${labTxt}</span>
<span class="sep">·</span>
<span>${c.dataset.name} 套图</span>
<span class="ct">${n} 张 · ${tsStr}</span>
</div>
<div class="ps-grid">
${Array(n).fill(0).map((_, i) => `
<div class="mp-result gen" data-idx="${i}">
<div class="mp-r-thumb"><span class="ph-frame">${c.dataset.name}</span></div>
<span class="adopt-badge">已采用</span>
<div class="cell-ops">
<button class="r-rerun" type="button" title="再次生成" aria-label="再次生成">${CELL_RERUN_SVG}</button>
<button class="r-dl" type="button" title="下载" aria-label="下载">${CELL_DL_SVG}</button>
<div class="cell-more-wrap">
<button class="r-more" type="button" title="更多" aria-label="更多">${CELL_MORE_SVG}</button>
<div class="cell-more-menu">
<button class="r-adopt" type="button">${CELL_ADOPT_SVG}<span>加入资产库</span></button>
<button class="r-del danger" type="button">${CELL_DEL_SVG}<span>删除</span></button>
</div>
</div>
</div>
</div>
`).join('')}
</div>
<div class="pc-pv-batch batch-foot">
<button class="pill-btn rerun-batch" type="button" title="再次生成这一批">
${CELL_RERUN_SVG}
<span>再次生成</span>
</button>
<button class="pill-btn primary adopt-batch" type="button" title="采用这一批">
${ADOPT_SVG}
<span>全部采用 · <span class="adopted">0</span>/<span class="total">${n}</span></span>
</button>
<div class="batch-more-wrap">
<button class="pill-btn icon batch-more" type="button" title="更多" aria-label="更多">${CELL_MORE_SVG}</button>
<div class="batch-more-menu" role="menu">
<button class="batch-save-all" type="button">${CELL_ADOPT_SVG}<span>全部加入资产库</span></button>
<button class="batch-del danger" type="button">${CELL_DEL_SVG}<span>删除该批结果</span></button>
</div>
</div>
</div>
`;
container.appendChild(section);
section.querySelectorAll('.mp-result.gen').forEach(card => {
setTimeout(() => card.classList.remove('gen'), 1200);
});
section.querySelectorAll('.r-rerun').forEach(b => b.addEventListener('click', e => {
e.stopPropagation();
rerunOne(b.closest('.mp-result'));
}));
section.querySelectorAll('.r-dl').forEach(b => b.addEventListener('click', e => {
e.stopPropagation();
Shell.toast('下载', '已开始下载 · MOCK');
}));
// 更多 menu 开/合
section.querySelectorAll('.r-more').forEach(b => b.addEventListener('click', e => {
e.stopPropagation();
const wrap = b.closest('.cell-more-wrap');
const willOpen = !wrap.classList.contains('open');
document.querySelectorAll('.pv-platform-section .cell-more-wrap.open').forEach(w => w.classList.remove('open'));
if (willOpen) wrap.classList.add('open');
}));
section.querySelectorAll('.r-adopt').forEach(b => b.addEventListener('click', e => {
e.stopPropagation();
const wrap = b.closest('.cell-more-wrap');
if (wrap) wrap.classList.remove('open');
adoptOne(b.closest('.mp-result'));
}));
section.querySelectorAll('.r-del').forEach(b => b.addEventListener('click', e => {
e.stopPropagation();
const wrap = b.closest('.cell-more-wrap');
if (wrap) wrap.classList.remove('open');
const card = b.closest('.mp-result');
const sec = card.closest('.pv-platform-section');
card.remove();
if (sec && !sec.querySelectorAll('.mp-result:not(.placeholder-only)').length) sec.remove();
else updateBatchSummary();
Shell.toast('已删除');
}));
section.querySelector('.rerun-batch').addEventListener('click', () => {
appendBatch(n, 'rerun-all');
Shell.toast('再次生成', n + ' 张图重新生成中 · 新批次已追加');
});
section.querySelector('.adopt-batch').addEventListener('click', () => {
const cards = section.querySelectorAll('.mp-result:not(.adopted)');
if (!cards.length) { Shell.toast('该批次已全部采用'); return; }
cards.forEach(c => { c.classList.remove('gen'); c.classList.add('adopted'); });
updateBatchSummary();
Shell.toast('已全部采用', cards.length + ' 张图入对应商品的 AI 素材 · 扣 ¥' + (cards.length * UNIT_PRICE).toFixed(2));
});
// 批次「更多」按钮 → 开/合 menu
const _bMoreBtn = section.querySelector('.batch-more');
const _bMoreWrap = section.querySelector('.batch-more-wrap');
if (_bMoreBtn && _bMoreWrap) {
_bMoreBtn.addEventListener('click', e => {
e.stopPropagation();
const willOpen = !_bMoreWrap.classList.contains('open');
document.querySelectorAll('.pc-pv-batch .batch-more-wrap.open').forEach(w => w.classList.remove('open'));
if (willOpen) _bMoreWrap.classList.add('open');
});
}
section.querySelector('.batch-save-all').addEventListener('click', e => {
e.stopPropagation();
if (_bMoreWrap) _bMoreWrap.classList.remove('open');
section.querySelector('.adopt-batch').click();
});
section.querySelector('.batch-del').addEventListener('click', e => {
e.stopPropagation();
if (_bMoreWrap) _bMoreWrap.classList.remove('open');
section.remove();
updateBatchSummary();
Shell.toast('已删除该批结果');
});
section.scrollIntoView({ behavior: 'smooth', block: 'end' });
updateBatchSummary();
if (typeof _refreshPlatformMenu === 'function') _refreshPlatformMenu();
if (typeof applyPvFilters === 'function') applyPvFilters();
}
function renderResultCards() {
const container = document.getElementById('pv-results');
// 首次生成清掉占位 placeholder-batch,保留已有真实批次(再次「立即生成」追加新批次)
container.querySelectorAll('.placeholder-batch').forEach(el => el.remove());
appendBatch(state.count, 'gen');
}
function rerunOne(card) {
if (!card) return;
appendBatch(1, 'rerun-one');
}
function adoptOne(card) {
if (!card || card.classList.contains('adopted')) return;
card.classList.remove('gen');
card.classList.add('adopted');
Shell.toast('已加入资产库', '入对应商品的 AI 素材 · 扣 ¥' + UNIT_PRICE.toFixed(2));
updateBatchSummary();
}
// 点击页面其它位置 → 关闭单图/批次 more menu
document.addEventListener('click', e => {
if (!e.target.closest('.pv-platform-section .cell-more-wrap')) {
document.querySelectorAll('.pv-platform-section .cell-more-wrap.open').forEach(w => w.classList.remove('open'));
}
if (!e.target.closest('.pc-pv-batch .batch-more-wrap')) {
document.querySelectorAll('.pc-pv-batch .batch-more-wrap.open').forEach(w => w.classList.remove('open'));
}
});
function updateBatchSummary() {
document.querySelectorAll('#pv-results .pv-platform-section').forEach(section => {
const cards = section.querySelectorAll('.mp-result:not(.placeholder-only)');
const adopted = section.querySelectorAll('.mp-result.adopted').length;
const adoptedEl = section.querySelector('.adopt-batch .adopted');
const totalEl = section.querySelector('.adopt-batch .total');
if (adoptedEl) adoptedEl.textContent = adopted;
if (totalEl) totalEl.textContent = cards.length;
});
}
// 立即生成
document.getElementById('pc-go-btn').addEventListener('click', () => {
if (!state.selectedPlatform || !state.selectedProd) return;
const container = document.getElementById('pv-results');
const hasReal = container && container.querySelector('.mp-result-batch:not(.placeholder-batch)');
const prod = PRODUCTS.find(p => p.id === state.selectedProd);
if (hasReal) {
Shell.toast('已追加批次', state.count + ' 张图新增到下方 · 旧批次保留');
} else {
Shell.toast('已提交任务', (prod ? prod.name + ' · ' : '') + state.count + ' 张图生成中');
}
showPreviewContent();
renderResultCards();
});
// ============================================================
// 工具台头部 · 搜索 / 时间 / 平台 筛选
// ============================================================
const _pvFilter = { time: 'all', platform: 'all', search: '' };
function _pvTimeMatch(ts, key) {
if (key === 'all') return true;
const now = Date.now();
const diff = now - Number(ts);
if (key === '10min') return diff <= 10 * 60 * 1000;
if (key === '1h') return diff <= 60 * 60 * 1000;
if (key === 'today') {
const a = new Date(now); const b = new Date(Number(ts));
return a.toDateString() === b.toDateString();
}
return true;
}
function applyPvFilters() {
const container = document.getElementById('pv-results');
if (!container) return;
const q = (_pvFilter.search || '').trim().toLowerCase();
container.querySelectorAll('.pv-platform-section:not(.placeholder-batch)').forEach(sec => {
let ok = true;
if (!_pvTimeMatch(sec.dataset.ts, _pvFilter.time)) ok = false;
if (ok && _pvFilter.platform !== 'all' && sec.dataset.platformId !== _pvFilter.platform) ok = false;
if (ok && q && !(sec.dataset.search || '').includes(q)) ok = false;
sec.dataset.hidden = ok ? '0' : '1';
});
const hasReal = !!container.querySelector('.pv-platform-section:not(.placeholder-batch)');
const filterActive = _pvFilter.time !== 'all' || _pvFilter.platform !== 'all' || q.length > 0;
container.querySelectorAll('.placeholder-batch').forEach(ph => {
ph.dataset.hidden = (hasReal || filterActive) ? '1' : '0';
});
}
function _refreshPlatformMenu() {
const menu = document.getElementById('pc-menu-platform');
if (!menu) return;
const container = document.getElementById('pv-results');
const used = new Map();
if (container) {
container.querySelectorAll('.pv-platform-section:not(.placeholder-batch)').forEach(s => {
const id = s.dataset.platformId; const nm = s.dataset.platformName;
if (id) used.set(id, nm || id);
});
}
const items = ['<button class="tb-menu-item' + (_pvFilter.platform === 'all' ? ' active' : '') + '" type="button" data-val="all">全部平台</button>'];
if (used.size === 0) {
items.push('<div class="tb-menu-empty">暂无批次,生成后可按平台筛选</div>');
} else {
used.forEach((nm, id) => {
items.push(`<button class="tb-menu-item${_pvFilter.platform === id ? ' active' : ''}" type="button" data-val="${id}">${nm}</button>`);
});
}
menu.innerHTML = items.join('');
}
function _setChipLabel(chipId, baseLabel, val) {
const chip = document.getElementById(chipId);
if (!chip) return;
if (val === 'all' || !val) chip.classList.remove('active');
else chip.classList.add('active');
}
function _closeAllMenus(except) {
document.querySelectorAll('.pc-main-h .tb-menu-wrap.open').forEach(w => {
if (w !== except) w.classList.remove('open');
});
}
document.querySelectorAll('.pc-main-h .tb-menu-wrap').forEach(wrap => {
const chip = wrap.querySelector('.tb-chip');
chip.addEventListener('click', e => {
e.stopPropagation();
const willOpen = !wrap.classList.contains('open');
_closeAllMenus(wrap);
wrap.classList.toggle('open', willOpen);
if (willOpen && wrap.dataset.filter === 'platform') _refreshPlatformMenu();
});
});
document.querySelectorAll('#pc-menu-time, #pc-menu-platform').forEach(menu => {
menu.addEventListener('click', e => {
const btn = e.target.closest('.tb-menu-item');
if (!btn) return;
const val = btn.dataset.val;
const wrap = menu.closest('.tb-menu-wrap');
const key = wrap.dataset.filter;
_pvFilter[key] = val;
menu.querySelectorAll('.tb-menu-item').forEach(it => it.classList.toggle('active', it === btn));
wrap.classList.remove('open');
_setChipLabel(key === 'time' ? 'pc-chip-time' : 'pc-chip-platform', key === 'time' ? '时间' : '平台', val);
applyPvFilters();
});
});
document.addEventListener('click', e => {
if (!e.target.closest('.pc-main-h .tb-menu-wrap')) _closeAllMenus(null);
});
(function setupSearch() {
const wrap = document.getElementById('pc-search-wrap');
const toggle = document.getElementById('pc-search-toggle');
const input = document.getElementById('pc-search-input');
if (!wrap || !toggle || !input) return;
toggle.addEventListener('click', e => {
e.stopPropagation();
const willExpand = !wrap.classList.contains('expanded');
wrap.classList.toggle('expanded', willExpand);
if (willExpand) setTimeout(() => input.focus(), 50);
else {
input.value = '';
_pvFilter.search = '';
applyPvFilters();
}
});
input.addEventListener('input', () => {
_pvFilter.search = input.value;
applyPvFilters();
});
input.addEventListener('keydown', e => {
if (e.key === 'Escape') {
input.value = '';
_pvFilter.search = '';
wrap.classList.remove('expanded');
applyPvFilters();
}
});
})();
// URL ?product=商品名 → 替换默认选中(从 products.html 跳过来时携带)
(function applyUrlProduct() {
const q = new URLSearchParams(location.search);
const productName = q.get('product');
if (!productName) return;
let p = PRODUCTS.find(x => x.name === productName);
if (!p) {
p = { id: 'np-' + Date.now().toString(36), name: productName, cat: '美妆个护', meta: '新建 · 待补充' };
PRODUCTS.unshift(p);
}
state.selectedProd = p.id;
})();
/* ============================================================
生成批次 (localStorage) · 按当前商品过滤 · 立即生成时追加
============================================================ */
(function () {
'use strict';
const TASK_TYPE = 'platform';
const KEY = 'fs-image-tasks-' + TASK_TYPE;
let tasks = [];
function load() {
try { return JSON.parse(localStorage.getItem(KEY) || '[]'); } catch (e) { return []; }
}
function save(arr) {
try { localStorage.setItem(KEY, JSON.stringify(arr)); } catch (e) {}
}
function buildSnapshot() {
return {
selectedProd: state.selectedProd,
selectedPlatform: state.selectedPlatform,
count: state.count,
};
}
function timeNow() {
const d = new Date();
return ('0'+(d.getMonth()+1)).slice(-2) + '.' + ('0'+d.getDate()).slice(-2) + ' ' + ('0'+d.getHours()).slice(-2) + ':' + ('0'+d.getMinutes()).slice(-2);
}
// 暴露给上层 (给 cur-prod header 用)
window._countBatchesForProd = function (prodId) {
return tasks.filter(t => t.snap && t.snap.selectedProd === prodId).length;
};
// 切换商品 → 清空 pv-results (历史批次只在当前 session 持有)
window.renderBatchesForCurrentProd = function () {
const container = document.getElementById('pv-results');
if (!container) return;
container.innerHTML = '';
showPreviewEmpty();
};
// 立即生成 → push 新批次 + persist + 刷新 cur-prod 计数
document.getElementById('pc-go-btn').addEventListener('click', () => {
if (!state.selectedPlatform || !state.selectedProd) return;
const snap = buildSnapshot();
const _prod = PRODUCTS.find(p => p.id === state.selectedProd);
const _platCard = document.querySelector('.platform-card[data-id="' + state.selectedPlatform + '"]');
const _platName = (_platCard && _platCard.dataset.name) || '平台';
const _name = ((_prod && _prod.name) || '商品') + ' × ' + _platName;
const task = {
id: 'task-' + Date.now(),
type: TASK_TYPE,
name: _name,
snap,
status: 'ok',
time: timeNow(),
createdAt: Date.now(),
};
tasks.push(task);
save(tasks);
updateCurProdHeader();
});
tasks = load();
})();
// 初始
renderProdSpace();
renderSelectedProds();
updatePlatforms();
showPreviewEmpty(); // 默认 → 右侧显示空态
// 默认选中: URL ?product= 优先, 否则选 PRODUCTS 首位 (= 最近编辑)
const defaultProdId = state.selectedProd || (PRODUCTS[0] && PRODUCTS[0].id);
if (defaultProdId) selectProduct(defaultProdId);
</script>
</body>
</html>