AirShelf/v2/products.html
UI 设计 e293aa43be
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
feat(v2): 添加 V2.1 设计稿目录 · 团队/设置页 · pipeline 多项 mock 优化
2026-05-21 16:18:28 +08:00

1788 lines
78 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">
<style>
/* ─── 全局 viewport 高度链 (让右侧 toolbar/分页吸顶吸底) ─── */
/* 整页滚动 · 头部 H1+actions sticky 固定 · 其他随页面滚 */
.page-head {
position: sticky;
top: 0;
z-index: 5;
background: var(--background-base);
padding-top: 4px;
margin-top: -4px;
}
/* ─── 主区 (普通文档流) ─── */
.products-main { display: flex; flex-direction: column; }
.products-main .result-meta {
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 10px;
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
}
/* ─── 商品分类多选 chip · menu 内多选项 + 计数 + 全选/全清 ─── */
.chip-wrap[data-key="cat"] .chip-menu {
min-width: 220px;
padding: 4px;
}
.chip-wrap[data-key="cat"] .mi-all {
border-bottom: 1px solid var(--border-faint);
margin-bottom: 4px;
padding-bottom: 4px;
}
.chip-wrap[data-key="cat"] .mi .cat-count {
margin-left: auto;
font-family: var(--font-mono); font-size: 11px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.chip-wrap[data-key="cat"] .mi.selected .cat-count { color: var(--heat); }
.chip .chip-count {
display: inline-flex; align-items: center; justify-content: center;
height: 18px; min-width: 18px; padding: 0 5px;
background: var(--heat-12); color: var(--heat);
border: 1px solid var(--heat-20);
border-radius: var(--r-pill);
font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;
letter-spacing: .02em;
margin-left: 2px;
}
.product-grid-wrap {
/* 滚动区与外栏视觉对齐 */
margin: 0 -8px;
padding: 2px 8px 24px;
}
/* ─── 生成类型选择 modal ─── */
.gen-choice-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.gen-choice-card {
display: flex; flex-direction: column; gap: 10px;
padding: 18px 16px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
cursor: pointer;
text-align: left;
font-family: inherit;
transition: background var(--t-base), border-color var(--t-base), transform var(--t-base);
}
.gen-choice-card:hover {
border-color: var(--heat);
background: var(--surface);
transform: translateY(-1px);
}
.gen-choice-card .gc-ic {
width: 36px; height: 36px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
display: grid; place-items: center;
color: var(--black-alpha-72);
}
.gen-choice-card:hover .gc-ic { color: var(--heat); border-color: var(--heat-20); background: var(--heat-12); }
.gen-choice-card .gc-ic svg { width: 18px; height: 18px; }
.gen-choice-card .gc-t { font-size: 14px; font-weight: 600; color: var(--accent-black); }
.gen-choice-card .gc-d { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; line-height: 1.45; }
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
}
.product-card {
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
cursor: pointer;
transition: background .15s, border-color .15s;
position: relative;
overflow: hidden;
display: flex; flex-direction: column;
}
.product-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
.product-thumb { aspect-ratio: 1.4 / 1; }
.product-body {
padding: 14px 14px 12px;
flex: 1;
}
.product-name {
font-size: 14px; font-weight: 600;
color: var(--accent-black);
line-height: 1.3;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.product-cat {
display: inline-flex; align-items: center;
margin-top: 8px;
padding: 2px 8px;
background: var(--background-lighter);
color: var(--black-alpha-72);
border-radius: var(--r-sm);
font-size: 11.5px;
}
.product-date {
font-family: var(--font-mono);
font-size: 11px;
color: var(--black-alpha-48);
margin-top: 10px;
letter-spacing: .02em;
}
/* ─── 卡片底栏 · V2.1 克制版 (线图标 + mono 文本) ─── */
.product-footer {
display: flex; align-items: center; gap: 8px;
padding: 10px 12px;
border-top: 1px solid var(--border-faint);
font-size: 11.5px;
color: var(--black-alpha-56);
background: var(--background-base);
}
.product-footer .stat {
display: inline-flex; align-items: center; gap: 5px;
padding: 3px 8px;
border-radius: var(--r-sm);
font-family: var(--font-mono);
letter-spacing: .02em;
white-space: nowrap; /* 防止"素材"等中文被挤压成竖排 */
flex-shrink: 0;
border: 1px solid transparent;
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
}
.product-footer .stat[data-type] { cursor: pointer; }
.product-footer .stat[data-type]:hover {
background: var(--heat-12);
color: var(--heat);
border-color: var(--heat-20);
}
.product-footer .stat[data-type]:hover svg { color: var(--heat); }
.product-footer .stat[data-type]:hover b { color: var(--heat); }
.product-footer .stat svg {
width: 13px; height: 13px;
color: var(--black-alpha-48);
flex-shrink: 0;
transition: color var(--t-base);
}
.product-footer .stat b {
color: var(--accent-black);
font-weight: 600;
transition: color var(--t-base);
}
/* default ↔ hover 文本切换 */
.product-footer .stat .stat-hover { display: none; }
.product-footer .stat[data-type]:hover .stat-default { display: none; }
.product-footer .stat[data-type]:hover .stat-hover { display: inline; }
.product-footer .sep {
color: var(--black-alpha-24);
font-family: var(--font-mono);
flex-shrink: 0;
}
/* ─── 底部分页 (吸底) ─── */
.pagination {
flex-shrink: 0;
display: flex; align-items: center; gap: 16px;
padding: 14px 0;
border-top: 1px solid var(--border-faint);
background: var(--background-base);
font-size: 12.5px;
color: var(--black-alpha-56);
}
.pagination[hidden] { display: none; }
.pagination .page-size { transition: border-color var(--t-base), color var(--t-base); }
.pagination .page-size:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
.pagination .pages .ellipsis {
min-width: 22px; height: 30px;
display: inline-flex; align-items: center; justify-content: center;
color: var(--black-alpha-48);
font-family: var(--font-mono);
}
.pagination .total { font-family: var(--font-mono); letter-spacing: .02em; }
.pagination .page-size {
display: inline-flex; align-items: center; gap: 4px;
height: 30px;
padding: 0 10px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
cursor: pointer;
font-family: inherit;
font-size: 12.5px;
color: var(--black-alpha-72);
}
.pagination .page-size svg { width: 10px; height: 10px; opacity: .6; }
.pagination .pages {
display: inline-flex; gap: 4px;
margin-left: auto;
}
.pagination .pages button {
min-width: 30px; height: 30px;
padding: 0 8px;
border: 1px solid var(--border-faint);
background: var(--surface);
border-radius: var(--r-sm);
cursor: pointer;
font-size: 12.5px;
color: var(--black-alpha-72);
font-family: inherit;
transition: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.pagination .pages button:hover:not(.active):not(:disabled) { border-color: var(--black-alpha-32); color: var(--accent-black); }
.pagination .pages button.active {
background: var(--heat);
color: var(--accent-white);
border-color: var(--heat);
font-weight: 600;
}
.pagination .pages button:disabled { opacity: .4; cursor: not-allowed; }
.pagination .jump {
display: inline-flex; align-items: center; gap: 6px;
color: var(--black-alpha-56);
}
.pagination .jump input {
width: 44px; height: 30px;
border: 1px solid var(--border-faint);
background: var(--surface);
border-radius: var(--r-sm);
text-align: center;
font-size: 12.5px;
color: var(--accent-black);
font-family: inherit;
outline: none;
transition: border-color var(--t-base);
}
.pagination .jump input:focus { border-color: var(--heat-40); }
/* 响应式 */
@media (max-width: 900px) {
.toolbar .search-inline { max-width: 100%; flex-basis: 100%; }
}
/* ============================================================
新建商品 drawer · 复用 .drawer (restraint.css)
在 products.html 上原地打开, 后面保留商品网格作为上下文
============================================================ */
.pc-drawer { width: 820px; max-width: 100vw; }
.pc-drawer .drawer-h h3 { font-size: 16px; font-weight: 600; }
.pc-drawer .drawer-b { padding: 24px 28px; }
.pc-drawer .drawer-b .form-card {
background: transparent; border: 0; padding: 0; border-radius: 0;
}
.pc-drawer .drawer-f {
padding: 14px 24px; background: var(--surface); align-items: center;
}
.pc-drawer .drawer-f .btn-guide {
margin-right: auto;
display: inline-flex; align-items: center; gap: 6px;
font-size: 13px; color: var(--black-alpha-56);
background: transparent; border: 0; cursor: pointer;
padding: 8px 10px; border-radius: var(--r-md);
font-family: inherit;
transition: background var(--t-base), color var(--t-base);
}
.pc-drawer .drawer-f .btn-guide:hover { color: var(--accent-black); background: var(--black-alpha-4); }
.pc-drawer .drawer-f .btn-guide svg { width: 14px; height: 14px; }
/* form-card · 表单容器(drawer 内被去外观,直接铺) */
.form-card .form-h {
font-size: 15px; font-weight: 600; color: var(--accent-black);
margin-bottom: 18px; padding-bottom: 12px;
border-bottom: 1px solid var(--border-faint);
}
.form-card .field { margin-bottom: 16px; }
.form-card .field:last-child { margin-bottom: 0; }
.form-card .field-row {
display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px;
}
.form-card .field-label {
display: block; font-size: 13px; font-weight: 500;
color: var(--accent-black); margin-bottom: 6px;
}
.form-card .field-label .req { color: var(--heat); margin-left: 2px; }
.form-card .field-label .opt {
color: var(--black-alpha-48); font-weight: 400; font-size: 12px; margin-left: 6px;
}
.form-card .input,
.form-card .select {
width: 100%; height: 38px;
background: var(--background-lighter);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-md);
padding: 0 14px;
font-size: 13.5px; color: var(--accent-black);
outline: none; font-family: inherit;
transition: border-color var(--t-base);
}
.form-card .input:focus,
.form-card .select:focus {
border-color: var(--heat-40);
box-shadow: inset 0 0 0 1px var(--heat-40);
}
/* 商品主图 · 上传(左) + 示例(右) */
.form-card .pf-upload-row {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
gap: 16px; align-items: stretch;
}
.form-card .pf-upload-zone {
border: 1.5px dashed var(--black-alpha-24);
border-radius: var(--r-md);
padding: 28px 20px;
background: var(--background-lighter);
cursor: pointer; text-align: center;
transition: border-color var(--t-base), background var(--t-base);
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-height: 180px;
}
.form-card .pf-upload-zone:hover { border-color: var(--heat); background: var(--heat-8); }
.form-card .pf-upload-zone .uz-ic {
width: 44px; height: 44px;
margin: 0 auto 10px;
background: var(--surface);
border: 1px solid var(--heat-20);
border-radius: var(--r-md);
color: var(--heat);
display: grid; place-items: center;
}
.form-card .pf-upload-zone .uz-ic svg { width: 20px; height: 20px; }
.form-card .pf-upload-zone .uz-t { font-size: 14px; color: var(--accent-black); font-weight: 500; }
.form-card .pf-upload-zone .uz-t strong { color: var(--heat); font-weight: 600; }
.form-card .pf-upload-zone .uz-d {
margin-top: 8px;
font-family: var(--font-mono); font-size: 11.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
/* 示例图 · 纵向卡片 */
.form-card .pf-example {
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
padding: 16px;
display: flex; flex-direction: column; gap: 10px;
}
.form-card .pf-example .ex-h {
font-size: 13px; font-weight: 600; color: var(--accent-black);
}
.form-card .pf-example .ex-grid {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
}
.form-card .pf-example .ex-grid .ex-thumb {
aspect-ratio: 1;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
overflow: hidden; position: relative;
display: grid; place-items: center;
color: var(--black-alpha-32);
}
.form-card .pf-example .ex-grid .ex-thumb svg { width: 22px; height: 22px; }
.form-card .pf-example .ex-grid .ex-thumb::after {
content: ''; position: absolute; inset: 0;
background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(0,0,0,.03) 6px 7px);
pointer-events: none;
}
.form-card .pf-example .ex-d {
font-size: 12px; color: var(--black-alpha-56); line-height: 1.5;
}
.form-card .pf-grid {
display: grid; grid-template-columns: repeat(5, 1fr);
gap: 8px; margin-top: 12px;
}
.form-card .pf-grid:empty { display: none; }
.form-card .pf-thumb {
aspect-ratio: 1;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
position: relative; overflow: hidden; cursor: pointer;
}
.form-card .pf-thumb img { width: 100%; height: 100%; object-fit: cover; }
.form-card .pf-thumb .pf-x {
position: absolute; top: 4px; right: 4px;
width: 22px; height: 22px;
background: rgba(0,0,0,.7); color: var(--accent-white);
border: 0; border-radius: 50%; cursor: pointer;
display: grid; place-items: center;
opacity: 0; transition: opacity var(--t-base);
}
.form-card .pf-thumb:hover .pf-x { opacity: 1; }
.form-card .pf-thumb .pf-x svg { width: 11px; height: 11px; }
/* 核心卖点 · bullet-list */
.form-card .bullet-list { list-style: none; padding: 0; margin: 0; }
.form-card .bullet-list .bl-item,
.form-card .bullet-list .bl-add {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px;
background: var(--background-lighter);
border: 1px solid var(--border-faint);
border-radius: var(--r-md);
margin-bottom: 6px;
font-size: 13.5px;
}
.form-card .bullet-list .bl-add { background: transparent; border-style: dashed; }
.form-card .bullet-list .num {
width: 22px; height: 22px;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
font-family: var(--font-mono);
font-size: 11px; color: var(--heat); font-weight: 700;
display: grid; place-items: center; flex-shrink: 0;
}
.form-card .bullet-list .bl-text { flex: 1; color: var(--accent-black); }
.form-card .bullet-list .bl-input {
flex: 1; background: transparent; border: 0; outline: none;
font-size: 13.5px; color: var(--accent-black); font-family: inherit;
}
.form-card .bullet-list .bl-x {
width: 22px; height: 22px;
color: var(--black-alpha-48);
cursor: pointer; display: grid; place-items: center;
border-radius: var(--r-sm);
transition: color var(--t-base), background var(--t-base);
}
.form-card .bullet-list .bl-x:hover { color: var(--accent-crimson); background: var(--crimson-bg); }
.form-card .bullet-list .bl-x svg { width: 11px; height: 11px; }
@media (max-width: 900px) {
.pc-drawer .drawer-b .pf-upload-row { grid-template-columns: 1fr; }
}
/* ============================================================
视图切换 · 网格 / 列表
============================================================ */
.view-tog {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--border-faint);
border-radius: var(--r-sm);
padding: 2px;
flex-shrink: 0;
}
.view-tog button {
width: 30px; height: 28px;
display: grid; place-items: center;
border: 0;
background: transparent;
color: var(--black-alpha-48);
cursor: pointer;
border-radius: 4px;
transition: background var(--t-base), color var(--t-base);
}
.view-tog button:hover { color: var(--accent-black); }
.view-tog button.active {
background: var(--accent-black);
color: var(--accent-white);
}
.view-tog button svg { width: 13px; height: 13px; }
/* 列表视图: 把网格改为单列, 每行横向布局 */
.product-grid.list-view { display: flex; flex-direction: column; gap: 8px; }
.product-grid.list-view .product-card {
display: grid;
grid-template-columns: 100px 1fr auto;
gap: 16px;
padding: 12px 16px;
align-items: center;
}
.product-grid.list-view .product-thumb {
width: 100px; height: 70px; aspect-ratio: auto;
margin: 0; border-radius: var(--r-sm);
}
.product-grid.list-view .product-body {
padding: 0;
display: flex; align-items: center; gap: 14px;
}
.product-grid.list-view .product-name { font-size: 14px; }
.product-grid.list-view .product-cat,
.product-grid.list-view .product-date {
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--black-alpha-48);
letter-spacing: .02em;
margin-top: 0;
}
.product-grid.list-view .product-footer {
border-top: 0;
padding: 0;
background: transparent;
gap: 6px;
}
/* ============================================================
批量编辑模式
============================================================ */
.product-card .card-check {
position: absolute;
top: 10px; left: 10px;
width: 22px; height: 22px;
border-radius: 50%;
background: var(--surface);
border: 2px solid var(--black-alpha-32);
display: none;
place-items: center;
color: var(--accent-white);
z-index: 5;
pointer-events: none;
}
.product-card .card-check svg { width: 11px; height: 11px; opacity: 0; }
body.edit-mode .product-card { cursor: pointer; }
body.edit-mode .product-card .card-check { display: grid; }
body.edit-mode .product-card.selected .card-check {
background: var(--heat);
border-color: var(--heat);
}
body.edit-mode .product-card.selected .card-check svg { opacity: 1; }
body.edit-mode .product-card.selected {
border-color: var(--heat);
box-shadow: 0 0 0 1px var(--heat) inset;
}
/* 编辑模式下,卡片底部 stat 和 more-btn 静默 (不能点击跳转) */
body.edit-mode .product-footer .stat,
body.edit-mode .product-card .card-del-btn { opacity: 0 !important; pointer-events: none !important; }
/* 浮动 action bar */
.bulk-bar {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--accent-black);
color: var(--accent-white);
border-radius: var(--r-md);
padding: 10px 14px 10px 18px;
display: none;
align-items: center;
gap: 16px;
box-shadow: 0 8px 24px rgba(0,0,0,.18);
z-index: 100;
font-size: 13px;
}
body.edit-mode .bulk-bar { display: inline-flex; }
.bulk-bar .ct { font-family: var(--font-mono); letter-spacing: .02em; }
.bulk-bar .ct b { color: var(--heat); font-weight: 700; padding: 0 3px; }
.bulk-bar .sep { width: 1px; height: 18px; background: rgba(255,255,255,.16); }
.bulk-bar button {
height: 30px;
padding: 0 12px;
background: transparent;
border: 1px solid rgba(255,255,255,.24);
border-radius: var(--r-sm);
color: var(--accent-white);
font-size: 12.5px;
font-family: inherit;
cursor: pointer;
display: inline-flex; align-items: center; gap: 5px;
transition: background var(--t-base), border-color var(--t-base);
}
.bulk-bar button:hover { background: rgba(255,255,255,.08); }
.bulk-bar button.danger {
background: var(--accent-crimson, #c43d3d);
border-color: var(--accent-crimson, #c43d3d);
}
.bulk-bar button.danger:hover { filter: brightness(1.06); }
.bulk-bar button svg { width: 12px; height: 12px; }
.bulk-bar .clear-sel {
color: rgba(255,255,255,.6);
font-size: 12px;
cursor: pointer;
background: none;
border: 0;
padding: 4px 6px;
}
.bulk-bar .clear-sel:hover { color: var(--accent-white); }
/* edit-mode 下「编辑商品」按钮变成「完成」 */
.btn-edit-toggle.active {
background: var(--accent-black);
color: var(--accent-white);
border-color: var(--accent-black);
}
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>商品库</h1>
<div class="sub"><span class="mono">// <span id="sku-count">0</span> SKU</span> · 商品信息会作为脚本和资产生成的素材</div>
</div>
<div class="actions">
<button class="btn btn-edit-toggle" type="button" id="edit-toggle-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>
<span class="btn-edit-label">管理商品</span>
</button>
<button class="btn btn-primary" type="button" id="open-new-product">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 5v14M5 12h14"/></svg>
新建商品
</button>
</div>
</div>
<!-- ===== 主区 (三段式) ===== -->
<div class="products-main">
<!-- 顶部固定: toolbar + meta -->
<div class="toolbar">
<div class="search-inline">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input class="input" id="search-input" placeholder="搜索商品名称、品牌">
</div>
<div class="chip-wrap" data-key="cat">
<button class="chip" type="button">
<span class="chip-label">商品分类</span>
<svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<div class="chip-menu"></div>
</div>
<div class="chip-wrap" data-key="date">
<button class="chip" type="button"><span class="chip-label">创建时间</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<button class="clear-filters" id="clear-filters" type="button" hidden>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>
清空筛选
</button>
</div>
<div class="result-meta" id="result-meta">
<span>// 显示 <span class="count">7</span> / 7 个商品</span>
<div class="view-tog" style="margin-left:auto" id="view-tog">
<button type="button" class="active" data-view="grid" title="网格视图">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><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"/><rect x="14" y="14" width="7" height="7"/></svg>
</button>
<button type="button" data-view="list" title="列表视图">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
</div>
</div>
<!-- 中间滚动: 商品网格 -->
<div class="product-grid-wrap">
<div class="product-grid" id="product-grid">
<div class="product-card" data-cat="美妆个护" data-name="透真玻尿酸补水面膜" data-tags="熬夜党 敏感肌" data-added="1" data-assets="124" data-videos="36" data-date="2026-05-15" onclick="location.href='product-detail.html?t='+Date.now()">
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除商品" onclick="event.stopPropagation();" data-action="delete-product"><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 class="placeholder product-thumb"><span class="ph-frame">补水面膜 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">透真玻尿酸补水面膜</div>
<div class="product-cat">美妆个护</div>
<div class="product-date">2026-05-15 创建</div>
</div>
<div class="product-footer">
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 17l-5-5-9 9"/></svg>
素材 <b>124</b>
</span>
<span class="sep">·</span>
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
视频 <b>36</b>
</span>
</div>
</div>
<div class="product-card" data-cat="数码 3C" data-name="南卡 Lite Pro 蓝牙耳机" data-tags="通勤 运动" data-added="2" data-assets="96" data-videos="28" data-date="2026-05-12" onclick="location.href='product-detail.html?t='+Date.now()">
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除商品" onclick="event.stopPropagation();" data-action="delete-product"><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 class="placeholder product-thumb"><span class="ph-frame">蓝牙耳机 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">南卡 Lite Pro 蓝牙耳机</div>
<div class="product-cat">数码 3C</div>
<div class="product-date">2026-05-12 创建</div>
</div>
<div class="product-footer">
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 17l-5-5-9 9"/></svg>
素材 <b>96</b>
</span>
<span class="sep">·</span>
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
视频 <b>28</b>
</span>
</div>
</div>
<div class="product-card" data-cat="食品饮料" data-name="滋啦速食牛肉面 6 桶装" data-tags="加班 独居" data-added="3" data-assets="96" data-videos="24" data-date="2026-05-10" onclick="location.href='product-detail.html?t='+Date.now()">
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除商品" onclick="event.stopPropagation();" data-action="delete-product"><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 class="placeholder product-thumb"><span class="ph-frame">速食牛肉面 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">滋啦速食牛肉面 · 6 桶装</div>
<div class="product-cat">食品饮料</div>
<div class="product-date">2026-05-10 创建</div>
</div>
<div class="product-footer">
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 17l-5-5-9 9"/></svg>
素材 <b>96</b>
</span>
<span class="sep">·</span>
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
视频 <b>24</b>
</span>
</div>
</div>
<div class="product-card" data-cat="美妆个护" data-name="透真清透物理防晒霜" data-tags="SPF50 通勤" data-added="4" data-assets="76" data-videos="18" data-date="2026-05-08" onclick="location.href='product-detail.html?t='+Date.now()">
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除商品" onclick="event.stopPropagation();" data-action="delete-product"><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 class="placeholder product-thumb"><span class="ph-frame">防晒霜 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">透真清透物理防晒霜</div>
<div class="product-cat">美妆个护</div>
<div class="product-date">2026-05-08 创建</div>
</div>
<div class="product-footer">
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 17l-5-5-9 9"/></svg>
素材 <b>76</b>
</span>
<span class="sep">·</span>
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
视频 <b>18</b>
</span>
</div>
</div>
<div class="product-card" data-cat="食品饮料" data-name="三顿半同款冻干咖啡粉" data-tags="提神 早八" data-added="5" data-assets="68" data-videos="21" data-date="2026-05-05" onclick="location.href='product-detail.html?t='+Date.now()">
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除商品" onclick="event.stopPropagation();" data-action="delete-product"><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 class="placeholder product-thumb"><span class="ph-frame">咖啡冻干粉 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">三顿半同款冻干咖啡粉</div>
<div class="product-cat">食品饮料</div>
<div class="product-date">2026-05-05 创建</div>
</div>
<div class="product-footer">
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 17l-5-5-9 9"/></svg>
素材 <b>68</b>
</span>
<span class="sep">·</span>
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
视频 <b>21</b>
</span>
</div>
</div>
<div class="product-card" data-cat="家居家电" data-name="小熊 4L 可视空气炸锅" data-tags="小户型 健康" data-added="6" data-assets="54" data-videos="16" data-date="2026-05-03" onclick="location.href='product-detail.html?t='+Date.now()">
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除商品" onclick="event.stopPropagation();" data-action="delete-product"><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 class="placeholder product-thumb"><span class="ph-frame">空气炸锅 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">小熊 4L 可视空气炸锅</div>
<div class="product-cat">家居家电</div>
<div class="product-date">2026-05-03 创建</div>
</div>
<div class="product-footer">
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 17l-5-5-9 9"/></svg>
素材 <b>54</b>
</span>
<span class="sep">·</span>
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
视频 <b>16</b>
</span>
</div>
</div>
<div class="product-card" data-cat="运动户外" data-name="露露同款裸感瑜伽裤" data-tags="健身房 通勤" data-added="7" data-assets="42" data-videos="12" data-date="2026-04-30" onclick="location.href='product-detail.html?t='+Date.now()">
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除商品" onclick="event.stopPropagation();" data-action="delete-product"><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 class="placeholder product-thumb"><span class="ph-frame">瑜伽裤 · 1200×800</span></div>
<div class="product-body">
<div class="product-name">露露同款裸感瑜伽裤</div>
<div class="product-cat">运动户外</div>
<div class="product-date">2026-04-30 创建</div>
</div>
<div class="product-footer">
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 17l-5-5-9 9"/></svg>
素材 <b>42</b>
</span>
<span class="sep">·</span>
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
视频 <b>12</b>
</span>
</div>
</div>
</div>
<div class="empty-state" id="empty">
<div class="ic-empty">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
</div>
<h3>没有匹配的商品</h3>
<p>// 试试切换分类或修改搜索词</p>
</div>
</div>
<!-- 底部固定: 分页 -->
<div class="pagination" id="pagination" hidden>
<span class="total"><b id="page-total">0</b></span>
<button class="page-size" type="button" id="page-size-btn" title="切换每页条数">
<span id="page-size-label">12 条/页</span>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<span class="pages" id="page-list"></span>
<span class="jump">跳至 <input type="number" min="1" value="1" id="page-jump"></span>
</div>
</div><!-- /.products-main -->
</div><!-- /#page -->
<!-- ============================================================
新建商品 · 右侧 Drawer · 在商品库页面原地打开
============================================================ -->
<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>新建商品</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="form-card">
<div class="field">
<label class="field-label">商品名称<span class="req">*</span></label>
<input class="input" id="pf-name" placeholder="请输入商品名称(必填)" maxlength="100">
</div>
<div class="field-row">
<div>
<label class="field-label">品类<span class="req">*</span></label>
<select class="select" id="pf-cat">
<option>美妆个护</option>
<option>服饰内衣</option>
<option>食品饮料</option>
<option>家居家电</option>
<option>数码 3C</option>
<option>个护清洁</option>
<option>运动户外</option>
<option>母婴亲子</option>
</select>
</div>
<div>
<label class="field-label">目标人群<span class="opt">(选填)</span></label>
<input class="input" id="pf-target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
</div>
</div>
<div class="field">
<label class="field-label">商品主图<span class="req">*</span></label>
<input type="file" id="pf-file" accept="image/*" multiple hidden>
<div class="pf-upload-row">
<div class="pf-upload-zone" id="pf-zone">
<div class="uz-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
</div>
<div class="uz-t">点击上传或<strong>拖拽图片</strong>到此处</div>
<div class="uz-d">// 支持 JPG、PNG 格式,建议尺寸 800×800 以上,大小不超过 10MB</div>
</div>
<div class="pf-example">
<div class="ex-h">示例图</div>
<div class="ex-grid">
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4h10l1 4v12H6V8l1-4z"/><path d="M9 4v3M15 4v3M9 11h6M9 14h6"/></svg></div>
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="5" width="12" height="15" rx="2"/><path d="M9 9h6M9 12h6M9 15h4"/></svg></div>
<div class="ex-thumb"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3h8l1 5v12H7V8l1-5z"/><circle cx="12" cy="13" r="2.5"/></svg></div>
</div>
<div class="ex-d">优质的商品图有助于生成更好的素材效果</div>
</div>
</div>
<div class="pf-grid" id="pf-grid"></div>
</div>
<div class="field" style="margin-bottom: 0;">
<label class="field-label">核心卖点<span class="req">*</span></label>
<ul class="bullet-list" id="pf-bullets" data-bl>
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder="添加新卖点 · 回车确认"></li>
</ul>
</div>
</div>
</div>
<div class="drawer-f">
<button class="btn-guide" type="button" onclick="Shell.toast('使用指南', '点击查看完整填写指南')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 015 0c0 1.5-2.5 2-2.5 4M12 17h.01"/></svg>
使用指南
</button>
<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>
<!-- ===== 生成类型选择 modal ===== -->
<div class="modal-bg" id="gen-choice-bg">
<div class="modal" role="dialog" aria-labelledby="gen-choice-ti" style="max-width:560px">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m" style="background:var(--heat-12);color:var(--heat)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>
</div>
<div class="ti" id="gen-choice-ti"><span id="gen-choice-target"></span><span>// PICK ONE</span></div>
</div>
<div class="modal-b" style="padding-top:12px">
<div class="gen-choice-grid">
<button type="button" class="gen-choice-card" data-go="model-photo">
<div class="gc-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></svg>
</div>
<div class="gc-t">模特上身图</div>
<div class="gc-d">// 选模特 + 商品 → AI 生成穿搭/试用图</div>
</button>
<button type="button" class="gen-choice-card" data-go="platform-cover">
<div class="gc-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 9h18M9 5v14"/></svg>
</div>
<div class="gc-t">平台套图</div>
<div class="gc-d">// 多平台尺寸自动生成 (抖音/淘宝/小红书等)</div>
</button>
</div>
</div>
<div class="modal-f">
<button class="btn" type="button" id="gen-choice-cancel">取消</button>
</div>
</div>
</div>
<!-- ===== 删除确认 modal ===== -->
<div class="modal-bg" id="del-confirm-bg">
<div class="modal" role="dialog" aria-labelledby="del-confirm-ti">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m" style="background:var(--crimson-bg,#fdebea);color:var(--accent-crimson,#c43d3d)">
<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>
</div>
<div class="ti" id="del-confirm-ti">确认删除商品<span>// CONFIRM DELETE</span></div>
</div>
<div class="modal-b" id="del-confirm-body">即将删除 <span class="mono-acc" id="del-confirm-target"></span>,此操作无法撤销,商品下生成的素材记录也将一并清理。</div>
<div class="modal-f" id="del-confirm-foot">
<button class="btn" type="button" id="del-confirm-cancel">取消</button>
<button class="btn" type="button" id="del-confirm-ok" style="background:var(--accent-crimson,#c43d3d);color:var(--accent-white);border-color:var(--accent-crimson,#c43d3d)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
确认删除
</button>
</div>
</div>
</div>
<!-- ===== 批量编辑 浮动 action bar ===== -->
<div class="bulk-bar" id="bulk-bar">
<span class="ct">已选 <b id="bulk-count">0</b></span>
<button class="clear-sel" type="button" id="bulk-clear">清空</button>
<span class="sep"></span>
<button class="danger" type="button" id="bulk-del">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" 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>
<button type="button" id="bulk-exit">完成</button>
</div>
<script src="assets/shell.js"></script>
<script>
Shell.render({ active: 'products', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '商品库' }] });
// ============== Products: 商品分类多选 + 搜索 + 创建时间筛选 ==============
// state.cats: Set<string>,空集 = 全部
const PAGE_SIZES = [12, 24, 48, 96];
const state = { cats: new Set(), search: '', date: 'all', page: 1, pageSize: 12 };
// 抖音爆款品类 · TOP 8 (全部显示,即使没商品)
const CATEGORIES = ['美妆个护', '服饰内衣', '食品饮料', '家居家电', '数码 3C', '个护清洁', '运动户外', '母婴亲子'];
const DATE_LABEL = {
'all': '全部时间',
'7d': '最近 7 天',
'30d': '最近 30 天',
'90d': '最近 90 天',
};
const grid = document.getElementById('product-grid');
const cards = [...grid.querySelectorAll('.product-card')];
const TOTAL = cards.length;
const CAT_COUNT = Object.fromEntries(CATEGORIES.map(c =>
[c, cards.filter(card => card.dataset.cat === c).length]
));
// 同步计数: 大标题 SKU + 侧栏徽章
document.getElementById('sku-count').textContent = TOTAL;
const sidebarBadge = document.querySelector('aside.sidebar a[href="products.html"] .pill-mini');
if (sidebarBadge) sidebarBadge.textContent = TOTAL;
// ─── 卡片底部 stat 增强 · hover 切换文案 + 直跳生成入口 ───
cards.forEach(card => {
card.querySelectorAll('.product-footer .stat').forEach(stat => {
const text = stat.textContent.trim();
const m = text.match(/^(素材|视频)\s*(\d+)/);
if (!m) return;
const label = m[1];
const num = m[2];
const isAsset = label === '素材';
const cta = isAsset ? '+ 去生成素材' : '+ 去生成视频';
const svg = stat.querySelector('svg');
// 重组结构: <svg> <span.stat-default>素材 <b>N</b></span> <span.stat-hover>+ 去生成素材</span>
stat.innerHTML = '';
if (svg) stat.appendChild(svg);
const dft = document.createElement('span');
dft.className = 'stat-default';
dft.innerHTML = `${label} <b>${num}</b>`;
const hv = document.createElement('span');
hv.className = 'stat-hover';
hv.textContent = cta;
stat.appendChild(dft);
stat.appendChild(hv);
stat.dataset.type = isAsset ? 'asset' : 'video';
stat.title = isAsset ? '去生成 AI 素材' : '去生成视频项目';
stat.addEventListener('click', e => {
e.stopPropagation();
const name = card.dataset.name || '';
if (isAsset) {
// 弹出选择: 模特上身图 / 平台套图
openGenAssetChoice(name);
} else {
location.href = 'projects-new.html?t=' + Date.now() + '&product=' + encodeURIComponent(name);
}
});
});
});
// ─── 生成类型选择 modal (素材生成 → 选 模特上身图 / 平台套图) ───
const genChoiceBg = document.getElementById('gen-choice-bg');
const genChoiceTarget = document.getElementById('gen-choice-target');
const genChoiceCancel = document.getElementById('gen-choice-cancel');
let _genChoiceProduct = '';
function openGenAssetChoice(productName) {
_genChoiceProduct = productName || '';
genChoiceTarget.textContent = _genChoiceProduct || '该商品';
genChoiceBg.classList.add('show');
document.body.style.overflow = 'hidden';
}
function closeGenAssetChoice() {
genChoiceBg.classList.remove('show');
document.body.style.overflow = '';
}
genChoiceCancel.addEventListener('click', closeGenAssetChoice);
genChoiceBg.addEventListener('click', e => {
if (e.target === genChoiceBg) closeGenAssetChoice();
});
genChoiceBg.querySelectorAll('.gen-choice-card').forEach(btn => {
btn.addEventListener('click', () => {
const go = btn.dataset.go;
const target = (go === 'model-photo') ? 'model-photo.html' : 'platform-cover.html';
const url = target + '?t=' + Date.now() + '&product=' + encodeURIComponent(_genChoiceProduct);
closeGenAssetChoice();
location.href = url;
});
});
// ─── 删除确认 modal (PRD §6.3: 已被项目引用时禁止强删) ───
// 模拟: 某些商品被项目引用 (data-refs="项目A,项目B")
// 真实环境从后台查 product → projects 映射
const PRODUCT_REFS = {
'透真玻尿酸补水面膜': ['夏日水嫩计划', '七夕主题推广'],
'南卡 Lite Pro 蓝牙耳机': ['通勤好物']
};
function getRefs(card) {
const name = card.dataset.name || '';
return PRODUCT_REFS[name] || [];
}
const delBg = document.getElementById('del-confirm-bg');
const delBody = document.getElementById('del-confirm-body');
const delFoot = document.getElementById('del-confirm-foot');
const delCancel = document.getElementById('del-confirm-cancel');
const delOk = document.getElementById('del-confirm-ok');
let _delQueue = [];
function setFootDeletable() {
delFoot.innerHTML = '';
delFoot.appendChild(delCancel);
delFoot.appendChild(delOk);
}
function setFootBlocked() {
delFoot.innerHTML = '';
const okBtn = document.createElement('button');
okBtn.className = 'btn btn-primary';
okBtn.type = 'button';
okBtn.textContent = '我知道了';
okBtn.addEventListener('click', closeDelConfirm);
delFoot.appendChild(okBtn);
}
function openDelConfirm(targets) {
// 分组: 可删除 / 被引用 (PRD §6.3 软删除规则)
const blocked = targets.filter(c => getRefs(c).length > 0);
const deletable = targets.filter(c => getRefs(c).length === 0);
// 全部被引用 → 阻断式提示
if (deletable.length === 0 && blocked.length > 0) {
const c = blocked[0];
const refs = getRefs(c);
if (blocked.length === 1) {
delBody.innerHTML = `<span class="mono-acc">${c.dataset.name}</span> 当前被 <b>${refs.length}</b> 个项目使用,无法直接删除。请先在以下项目中解除引用:<br><br>` +
refs.map(r => `<span class="mono-acc" style="margin-right:6px">${r}</span>`).join('');
} else {
delBody.innerHTML = `所选 <b>${blocked.length}</b> 个商品均被项目引用,无法直接删除。请先解除引用后重试。`;
}
setFootBlocked();
delBg.classList.add('show');
_delQueue = [];
return;
}
_delQueue = deletable;
if (deletable.length === 1 && blocked.length === 0) {
delBody.innerHTML = '即将删除 <span class="mono-acc">' + deletable[0].dataset.name + '</span>,此操作无法撤销,商品下生成的素材记录也将一并清理。';
} else if (blocked.length > 0) {
delBody.innerHTML = '即将删除 <span class="mono-acc">' + deletable.length + ' 个商品</span>,其中 <b>' + blocked.length + '</b> 个被项目引用已跳过。可删除的将一并清理素材记录。';
} else {
delBody.innerHTML = '即将删除 <span class="mono-acc">' + deletable.length + ' 个商品</span>,此操作无法撤销,这些商品下生成的素材记录也将一并清理。';
}
setFootDeletable();
delBg.classList.add('show');
}
function closeDelConfirm() { delBg.classList.remove('show'); _delQueue = []; }
delCancel.addEventListener('click', closeDelConfirm);
delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });
delOk.addEventListener('click', () => {
const n = _delQueue.length;
_delQueue.forEach(card => card.remove());
closeDelConfirm();
const remaining = document.querySelectorAll('.product-card').length;
document.getElementById('sku-count').textContent = remaining;
const meta = document.querySelector('#result-meta .count');
if (meta) meta.textContent = remaining;
const sidebar = document.querySelector('aside.sidebar a[href="products.html"] .pill-mini');
if (sidebar) sidebar.textContent = remaining;
Shell.toast('已删除', n === 1 ? '商品已移除' : '已删除 ' + n + ' 个商品');
updateBulkBar();
});
// ─── 单卡片删除 (新统一 card-del-btn) ───
document.querySelectorAll('.product-card .card-del-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const card = btn.closest('.product-card');
if (!card) return;
openDelConfirm([card]);
});
});
// ─── 视图切换 网格 / 列表 ───
const productGrid = document.getElementById('product-grid');
document.querySelectorAll('#view-tog button').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('#view-tog button').forEach(x => x.classList.remove('active'));
b.classList.add('active');
if (b.dataset.view === 'list') productGrid.classList.add('list-view');
else productGrid.classList.remove('list-view');
});
});
// ─── 批量编辑模式 ───
const editToggleBtn = document.getElementById('edit-toggle-btn');
const bulkBar = document.getElementById('bulk-bar');
const bulkCount = document.getElementById('bulk-count');
const bulkClear = document.getElementById('bulk-clear');
const bulkDel = document.getElementById('bulk-del');
const bulkExit = document.getElementById('bulk-exit');
const editLabel = editToggleBtn.querySelector('.btn-edit-label');
function getSelectedCards() {
return [...document.querySelectorAll('.product-card.selected')];
}
function updateBulkBar() {
const sel = getSelectedCards();
bulkCount.textContent = sel.length;
bulkDel.disabled = sel.length === 0;
bulkDel.style.opacity = sel.length === 0 ? '.4' : '1';
bulkDel.style.cursor = sel.length === 0 ? 'not-allowed' : 'pointer';
}
function enterEditMode() {
document.body.classList.add('edit-mode');
editToggleBtn.classList.add('active');
editLabel.textContent = '完成';
updateBulkBar();
}
function exitEditMode() {
document.body.classList.remove('edit-mode');
editToggleBtn.classList.remove('active');
editLabel.textContent = '编辑商品';
document.querySelectorAll('.product-card.selected').forEach(c => c.classList.remove('selected'));
}
editToggleBtn.addEventListener('click', () => {
if (document.body.classList.contains('edit-mode')) exitEditMode();
else enterEditMode();
});
bulkExit.addEventListener('click', exitEditMode);
bulkClear.addEventListener('click', () => {
document.querySelectorAll('.product-card.selected').forEach(c => c.classList.remove('selected'));
updateBulkBar();
});
bulkDel.addEventListener('click', () => {
const sel = getSelectedCards();
if (!sel.length) return;
openDelConfirm(sel);
});
// 编辑模式下,卡片点击切换 selected (不再跳详情)
document.querySelectorAll('.product-card').forEach(card => {
card.addEventListener('click', e => {
if (!document.body.classList.contains('edit-mode')) return; // 非编辑模式走原 onclick (跳详情)
// capture 阶段必须用 stopImmediatePropagation 才能阻止 element.onclick (inline onclick="")
e.stopImmediatePropagation();
e.preventDefault();
card.classList.toggle('selected');
updateBulkBar();
}, true); // capture: 早于 inline onclick
});
const checkSvg = '<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg>';
// ====== 商品分类 chip · 多选 ======
const catWrap = document.querySelector('.chip-wrap[data-key="cat"]');
const catMenu = catWrap.querySelector('.chip-menu');
function renderCatMenu() {
const allSelected = state.cats.size === 0;
const allRow = `<div class="mi mi-all${allSelected ? ' selected' : ''}" data-value="__all__">${checkSvg}<span>全部商品</span><span class="cat-count">${TOTAL}</span></div>`;
const items = CATEGORIES
.filter(c => CAT_COUNT[c] > 0)
.map(c => {
const sel = state.cats.has(c);
const n = CAT_COUNT[c];
return `<div class="mi${sel ? ' selected' : ''}" data-value="${c}">${checkSvg}<span>${c}</span><span class="cat-count">${n}</span></div>`;
}).join('');
catMenu.innerHTML = allRow + items;
catMenu.querySelectorAll('.mi').forEach(mi => {
mi.addEventListener('click', e => {
e.stopPropagation();
const v = mi.dataset.value;
if (v === '__all__') {
state.cats.clear();
} else {
if (state.cats.has(v)) state.cats.delete(v);
else state.cats.add(v);
}
renderCatMenu(); // 不关菜单,只刷新 selected 态
syncCatChip();
state.page = 1;
applyFilter();
});
});
}
function syncCatChip() {
const label = catWrap.querySelector('.chip-label');
const chip = catWrap.querySelector('.chip');
// 先清掉旧的 chip-count
chip.querySelectorAll('.chip-count').forEach(n => n.remove());
if (state.cats.size === 0) {
label.textContent = '商品分类';
chip.classList.remove('active');
} else if (state.cats.size === 1) {
label.textContent = [...state.cats][0];
chip.classList.add('active');
} else {
label.textContent = '商品分类';
const count = document.createElement('span');
count.className = 'chip-count';
count.textContent = state.cats.size;
// 把计数徽标插在 caret 之前
chip.insertBefore(count, chip.querySelector('.caret'));
chip.classList.add('active');
}
}
catWrap.querySelector('.chip').addEventListener('click', e => {
e.stopPropagation();
const isOpen = catWrap.classList.contains('open');
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
if (!isOpen) catWrap.classList.add('open');
});
// menu 内点击不冒泡到 document(防止误关)
catMenu.addEventListener('click', e => e.stopPropagation());
renderCatMenu();
// ====== 创建时间 chip ======
const dateWrap = document.querySelector('.chip-wrap[data-key="date"]');
dateWrap.querySelector('.chip-menu').innerHTML = Object.entries(DATE_LABEL).map(([v, l]) =>
`<div class="mi${v === 'all' ? ' selected' : ''}" data-value="${v}">${checkSvg}<span>${l}</span></div>`
).join('');
function syncDateChip() {
const label = dateWrap.querySelector('.chip-label');
const chip = dateWrap.querySelector('.chip');
if (state.date === 'all') {
label.textContent = '创建时间';
chip.classList.remove('active');
} else {
label.textContent = DATE_LABEL[state.date];
chip.classList.add('active');
}
dateWrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === state.date));
}
dateWrap.querySelector('.chip').addEventListener('click', e => {
e.stopPropagation();
const isOpen = dateWrap.classList.contains('open');
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
if (!isOpen) dateWrap.classList.add('open');
});
dateWrap.querySelectorAll('.mi').forEach(mi => {
mi.addEventListener('click', e => {
e.stopPropagation();
state.date = mi.dataset.value;
syncDateChip();
dateWrap.classList.remove('open');
state.page = 1;
applyFilter();
});
});
document.addEventListener('click', () => {
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
});
// ====== 筛选应用 ======
const NOW = new Date('2026-05-19'); // 当前日期参考 (Shell.today)
function withinDate(dateStr) {
if (state.date === 'all') return true;
const days = { '7d': 7, '30d': 30, '90d': 90 }[state.date];
if (!days) return true;
const d = new Date(dateStr);
const diffMs = NOW - d;
return diffMs <= days * 24 * 60 * 60 * 1000;
}
function applyFilter() {
let visible = 0;
const visibleCards = [];
cards.forEach(c => {
const matchCat = state.cats.size === 0 || state.cats.has(c.dataset.cat);
const q = state.search.toLowerCase();
const matchSearch = !q
|| (c.dataset.name || '').toLowerCase().includes(q)
|| (c.dataset.tags || '').toLowerCase().includes(q)
|| (c.dataset.cat || '').toLowerCase().includes(q);
const matchDate = withinDate(c.dataset.date);
const show = matchCat && matchSearch && matchDate;
c.style.display = show ? '' : 'none';
if (show) { visible++; visibleCards.push(c); }
});
// 默认按 added desc(最新在前)
visibleCards.sort((a, b) => +b.dataset.added - +a.dataset.added);
visibleCards.forEach(c => grid.appendChild(c));
// 分页 · 排序后裁页, 把非当前页的卡片隐藏
const totalPages = Math.max(1, Math.ceil(visible / state.pageSize));
if (state.page > totalPages) state.page = totalPages;
if (state.page < 1) state.page = 1;
const start = (state.page - 1) * state.pageSize;
const end = start + state.pageSize;
visibleCards.forEach((c, i) => {
if (i < start || i >= end) c.style.display = 'none';
});
const empty = document.getElementById('empty');
if (visible === 0) {
empty.classList.add('show');
grid.style.display = 'none';
} else {
empty.classList.remove('show');
grid.style.display = '';
}
document.getElementById('result-meta').innerHTML = `// 显示 <span class="count">${visible}</span> / ${TOTAL} 个商品`;
document.getElementById('clear-filters').hidden = !(state.cats.size > 0 || state.search || state.date !== 'all');
renderPagination(visible, totalPages);
}
// ============== 分页器渲染 ==============
function pageNumberList(cur, total) {
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
const pages = new Set([1, total, cur, cur - 1, cur + 1]);
if (cur <= 4) [2, 3, 4, 5].forEach(p => pages.add(p));
if (cur >= total - 3) [total - 4, total - 3, total - 2, total - 1].forEach(p => pages.add(p));
const sorted = [...pages].filter(p => p >= 1 && p <= total).sort((a, b) => a - b);
const out = [];
for (let i = 0; i < sorted.length; i++) {
if (i > 0 && sorted[i] - sorted[i - 1] > 1) out.push('…');
out.push(sorted[i]);
}
return out;
}
function renderPagination(totalVisible, totalPages) {
const root = document.getElementById('pagination');
if (!root) return;
// 空结果或只有一页且 ≤ pageSize → 不显示
if (totalVisible === 0 || (totalPages <= 1 && totalVisible <= state.pageSize)) {
root.hidden = true;
return;
}
root.hidden = false;
document.getElementById('page-total').textContent = totalVisible;
document.getElementById('page-size-label').textContent = `${state.pageSize} 条/页`;
document.getElementById('page-jump').value = state.page;
document.getElementById('page-jump').max = totalPages;
const list = document.getElementById('page-list');
const items = pageNumberList(state.page, totalPages);
let html = `<button type="button" data-page="prev" ${state.page <= 1 ? 'disabled' : ''} aria-label="上一页">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M10 12L6 8l4-4"/></svg>
</button>`;
items.forEach(p => {
if (p === '…') html += `<span class="ellipsis">…</span>`;
else html += `<button type="button" data-page="${p}" ${p === state.page ? 'class="active"' : ''}>${p}</button>`;
});
html += `<button type="button" data-page="next" ${state.page >= totalPages ? 'disabled' : ''} aria-label="下一页">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4l4 4-4 4"/></svg>
</button>`;
list.innerHTML = html;
}
// 搜索
document.getElementById('search-input').addEventListener('input', e => {
state.search = e.target.value.trim();
state.page = 1;
applyFilter();
});
// 清空筛选
document.getElementById('clear-filters').addEventListener('click', () => {
state.cats.clear();
state.search = '';
state.date = 'all';
document.getElementById('search-input').value = '';
renderCatMenu();
syncCatChip();
syncDateChip();
state.page = 1;
applyFilter();
Shell.toast('已清空筛选');
});
// ============== 分页器控件 ==============
// 翻页按钮(事件委托)
document.getElementById('page-list').addEventListener('click', e => {
const btn = e.target.closest('button[data-page]');
if (!btn || btn.disabled) return;
const v = btn.dataset.page;
const totalPages = +document.getElementById('page-jump').max || 1;
if (v === 'prev') state.page = Math.max(1, state.page - 1);
else if (v === 'next') state.page = Math.min(totalPages, state.page + 1);
else state.page = +v;
applyFilter();
// 翻页后把网格滚回顶, 让首张卡立刻可见
const wrap = document.querySelector('.product-grid-wrap');
if (wrap) wrap.scrollTo({ top: 0, behavior: 'smooth' });
});
// 每页条数(循环切换 12 → 24 → 48 → 96 → 12)
document.getElementById('page-size-btn').addEventListener('click', () => {
const i = PAGE_SIZES.indexOf(state.pageSize);
state.pageSize = PAGE_SIZES[(i + 1) % PAGE_SIZES.length];
state.page = 1;
applyFilter();
});
// 跳转
const _jumpEl = document.getElementById('page-jump');
function _doJump() {
let v = parseInt(_jumpEl.value, 10);
const max = +_jumpEl.max || 1;
if (!Number.isFinite(v)) v = 1;
v = Math.max(1, Math.min(max, v));
state.page = v;
applyFilter();
}
_jumpEl.addEventListener('change', _doJump);
_jumpEl.addEventListener('blur', _doJump);
_jumpEl.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); _doJump(); _jumpEl.blur(); }
});
applyFilter();
// ============================================================
// 新建商品 · Drawer 控制 + 多图上传 + 卖点 bullet-list
// ============================================================
const drawerBg = document.getElementById('pc-drawer-bg');
const drawerEl = document.getElementById('pc-drawer');
const openBtn = document.getElementById('open-new-product');
const closeBtn = document.getElementById('pc-drawer-close');
const cancelBtn = document.getElementById('pc-cancel-btn');
const saveBtn = document.getElementById('pc-save-btn');
function openNewProductDrawer() {
if (drawerEl.classList.contains('show')) return;
drawerBg.classList.add('show');
drawerEl.classList.add('show');
drawerEl.setAttribute('aria-hidden', 'false');
if (typeof Shell !== 'undefined' && Shell.lockScroll) Shell.lockScroll();
setTimeout(() => document.getElementById('pf-name')?.focus(), 280);
}
function closeNewProductDrawer() {
if (!drawerEl.classList.contains('show')) return;
drawerBg.classList.remove('show');
drawerEl.classList.remove('show');
drawerEl.setAttribute('aria-hidden', 'true');
if (typeof Shell !== 'undefined' && Shell.unlockScroll) Shell.unlockScroll();
}
openBtn.addEventListener('click', openNewProductDrawer);
closeBtn.addEventListener('click', closeNewProductDrawer);
cancelBtn.addEventListener('click', closeNewProductDrawer);
drawerBg.addEventListener('click', closeNewProductDrawer);
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && drawerEl.classList.contains('show')) closeNewProductDrawer();
});
// 来自其它页面的"新建商品"链接 → 跳到 products.html 后自动开
if (sessionStorage.getItem('auto-open-new-product') === '1') {
sessionStorage.removeItem('auto-open-new-product');
setTimeout(openNewProductDrawer, 100);
}
// ─── 多图上传 ───
const PF_MAX = 5;
const pfFiles = []; // {id, dataUrl, name}
const pfFile = document.getElementById('pf-file');
const pfZone = document.getElementById('pf-zone');
const pfGrid = document.getElementById('pf-grid');
const pfUid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
const pfEsc = s => (s || '').replace(/[<>&"]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;' })[c]);
function pfRender() {
pfGrid.innerHTML = pfFiles.map(u => `
<div class="pf-thumb" data-id="${u.id}">
<img src="${u.dataUrl}" alt="${pfEsc(u.name)}">
<button class="pf-x" type="button" title="删除">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
</button>
</div>
`).join('');
pfGrid.querySelectorAll('.pf-thumb .pf-x').forEach(b => {
b.onclick = e => {
e.stopPropagation();
const id = b.closest('.pf-thumb').dataset.id;
const i = pfFiles.findIndex(f => f.id === id);
if (i >= 0) { pfFiles.splice(i, 1); pfRender(); }
};
});
}
function pfAdd(fileList) {
const room = PF_MAX - pfFiles.length;
if (room <= 0) { Shell.toast('已达上限', `${PF_MAX} / ${PF_MAX}`); return; }
const incoming = [...fileList].filter(f => f.type.startsWith('image/')).slice(0, room);
let done = 0;
incoming.forEach(f => {
const r = new FileReader();
r.onload = e => {
pfFiles.push({ id: pfUid(), dataUrl: e.target.result, name: f.name });
if (++done === incoming.length) {
pfRender();
Shell.toast('已上传', `+ ${done} 张 · 共 ${pfFiles.length} / ${PF_MAX}`);
}
};
r.readAsDataURL(f);
});
}
pfFile.addEventListener('change', e => { pfAdd(e.target.files); e.target.value = ''; });
pfZone.addEventListener('click', () => { if (pfFiles.length < PF_MAX) pfFile.click(); });
pfZone.addEventListener('dragover', e => { e.preventDefault(); pfZone.style.borderColor = 'var(--heat)'; });
pfZone.addEventListener('dragleave', () => { pfZone.style.borderColor = ''; });
pfZone.addEventListener('drop', e => {
e.preventDefault(); pfZone.style.borderColor = '';
if (e.dataTransfer?.files?.length) pfAdd(e.dataTransfer.files);
});
// ─── 核心卖点 bullet-list ───
const blList = document.getElementById('pf-bullets');
const blInput = blList.querySelector('.bl-add .bl-input');
const blXSvg = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
function blRenumber() {
[...blList.querySelectorAll('.bl-item')].forEach((li, i) => {
li.querySelector('.num').textContent = i + 1;
});
}
function blBindX(x) {
x.addEventListener('click', () => {
const li = x.closest('li');
li.style.transition = 'opacity .15s, transform .15s';
li.style.opacity = 0;
li.style.transform = 'translateX(-8px)';
setTimeout(() => { li.remove(); blRenumber(); }, 150);
});
}
function blAdd(text) {
const t = (text || '').trim();
if (!t) return;
const li = document.createElement('li');
li.className = 'bl-item';
li.innerHTML = `<span class="num">0</span><span class="bl-text">${pfEsc(t)}</span><span class="bl-x" title="删除">${blXSvg}</span>`;
blList.querySelector('.bl-add').before(li);
blBindX(li.querySelector('.bl-x'));
blRenumber();
}
blInput.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
blAdd(blInput.value);
blInput.value = '';
}
});
// 创建商品 (真正插入 grid)
saveBtn.addEventListener('click', () => {
const name = document.getElementById('pf-name').value.trim();
const cat = document.getElementById('pf-cat').value;
if (!name) {
Shell.toast('请填写商品名称');
document.getElementById('pf-name').focus();
return;
}
if (pfFiles.length === 0) {
Shell.toast('请上传商品主图', '至少 1 张');
return;
}
// 计数器收集卖点 bullets
const bullets = [...document.querySelectorAll('#pf-bullets .bl-item input')]
.map(i => i.value.trim()).filter(Boolean);
// 创建 card 并插入 grid 最前
const today = new Date().toISOString().slice(0, 10);
const card = document.createElement('div');
card.className = 'product-card';
card.dataset.cat = cat;
card.dataset.name = name;
card.dataset.tags = '';
card.dataset.added = '0';
card.dataset.assets = '0';
card.dataset.videos = '0';
card.dataset.date = today;
card.setAttribute('onclick', "location.href='product-detail.html?t='+Date.now()");
card.innerHTML = `
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
<button class="card-del-btn" type="button" title="删除商品" onclick="event.stopPropagation();" data-action="delete-product"><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 class="placeholder product-thumb"><span class="ph-frame">${name} · 新建</span></div>
<div class="product-body">
<div class="product-name">${name}</div>
<div class="product-cat">${cat}</div>
<div class="product-date">${today} 创建</div>
</div>
<div class="product-footer">
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 17l-5-5-9 9"/></svg>
素材 <b>0</b>
</span>
<span class="sep">·</span>
<span class="stat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
视频 <b>0</b>
</span>
</div>
`;
const grid = document.getElementById('product-grid');
grid.insertBefore(card, grid.firstChild);
// 给新卡片注入交互:stat hover/click + del-btn 删除确认
card.querySelectorAll('.product-footer .stat').forEach(stat => {
const text = stat.textContent.trim();
const m = text.match(/^(素材|视频)\s*(\d+)/);
if (!m) return;
const isAsset = m[1] === '素材';
const svg = stat.querySelector('svg');
stat.innerHTML = '';
if (svg) stat.appendChild(svg);
const dft = document.createElement('span');
dft.className = 'stat-default';
dft.innerHTML = `${m[1]} <b>${m[2]}</b>`;
const hv = document.createElement('span');
hv.className = 'stat-hover';
hv.textContent = isAsset ? '+ 去生成素材' : '+ 去生成视频';
stat.appendChild(dft); stat.appendChild(hv);
stat.dataset.type = isAsset ? 'asset' : 'video';
stat.title = isAsset ? '去生成 AI 素材' : '去生成视频项目';
stat.addEventListener('click', e => {
e.stopPropagation();
const n = card.dataset.name || '';
if (isAsset) openGenAssetChoice(n);
else location.href = 'projects-new.html?t=' + Date.now() + '&product=' + encodeURIComponent(n);
});
});
const delBtn = card.querySelector('.card-del-btn');
if (delBtn) {
delBtn.addEventListener('click', e => {
e.stopPropagation();
openDelConfirm([card]);
});
}
// 同步 SKU 计数(标题 + 列表 meta + 侧栏 badge)
const remaining = document.querySelectorAll('.product-card').length;
const skuCount = document.getElementById('sku-count');
if (skuCount) skuCount.textContent = remaining;
const meta = document.querySelector('#result-meta .count');
if (meta) meta.textContent = remaining;
const sidebar = document.querySelector('aside.sidebar a[href="products.html"] .pill-mini');
if (sidebar) sidebar.textContent = remaining;
Shell.toast('商品已创建', `+ ${name} · 跳转至详情页`);
closeNewProductDrawer();
// 重置表单(为下次新建准备)
document.getElementById('pf-name').value = '';
document.getElementById('pf-cat').value = cat;
if (typeof pfFiles !== 'undefined') pfFiles.length = 0;
const pfGrid = document.getElementById('pf-grid');
if (pfGrid) pfGrid.innerHTML = '';
const blList = document.getElementById('pf-bullets');
if (blList) blList.querySelectorAll('.bl-item').forEach(li => li.remove());
// 商品库新建商品后跳转到该商品的详情页(其他页保持原行为)
setTimeout(() => {
location.href = 'product-detail.html?t=' + Date.now() + '&product=' + encodeURIComponent(name);
}, 400);
});
</script>
</body>
</html>