All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
1808 lines
80 KiB
HTML
1808 lines
80 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>商品库 · Airshelf</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
|
||
<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: grid;
|
||
grid-template-columns: 1fr auto 1fr;
|
||
align-items: center;
|
||
column-gap: 8px;
|
||
padding: 10px 12px;
|
||
border-top: 1px solid var(--border-faint);
|
||
font-size: 11.5px;
|
||
color: #6f6f6f;
|
||
background: var(--background-base);
|
||
}
|
||
.product-footer .stat {
|
||
display: inline-flex; align-items: center; justify-content: center; gap: 5px;
|
||
padding: 3px 8px;
|
||
border-radius: var(--r-sm);
|
||
font-family: var(--font-mono);
|
||
letter-spacing: .02em;
|
||
white-space: nowrap; /* 防止"素材"等中文被挤压成竖排 */
|
||
border: 1px solid transparent;
|
||
transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
|
||
}
|
||
.product-footer .stat { justify-self: center; }
|
||
.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: 14px; height: 14px;
|
||
color: currentColor;
|
||
flex-shrink: 0;
|
||
stroke-width: 1.25;
|
||
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 > svg { 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: #b8b8b8;
|
||
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 btn-create" type="button" id="open-new-product">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22V12"/><path d="M16 17h6"/><path d="M19 14v6"/><path d="M21 10.5V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7l7 4a2 2 0 0 0 2 0l1.7-1"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="m7.5 4.3 9 5.1"/></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.5"><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.5"><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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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/icons.js?v=2026052608"></script>
|
||
<script src="assets/shell.js?v=2026052607"></script>
|
||
<script>
|
||
Shell.render({ active: 'products', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '商品库' }] });
|
||
|
||
// ============== 注入用户新建的商品(来自工作台 / 商品库 drawer,写入 localStorage)==============
|
||
// 必须在 const cards / TOTAL / CAT_COUNT 之前执行,让后续逻辑把它们当普通商品处理
|
||
(function injectExtraProducts() {
|
||
// 一次性清掉历史遗留的 localStorage 旧数据(用户上次会话误持久化的占位商品)
|
||
try { localStorage.removeItem('fs-extra-products'); } catch (e) {}
|
||
let pending;
|
||
try {
|
||
pending = JSON.parse(sessionStorage.getItem('fs-extra-products') || '[]');
|
||
} catch (e) { return; }
|
||
if (!Array.isArray(pending) || !pending.length) return;
|
||
const grid = document.getElementById('product-grid');
|
||
if (!grid) return;
|
||
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
|
||
// 按 createdAt 升序排,然后逐个 insertBefore firstChild → 最新的排最上
|
||
pending.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
|
||
pending.forEach(p => {
|
||
const card = document.createElement('div');
|
||
card.className = 'product-card';
|
||
card.dataset.cat = p.cat || '美妆个护';
|
||
card.dataset.name = p.name || '';
|
||
card.dataset.tags = p.tags || '';
|
||
card.dataset.added = '0';
|
||
card.dataset.assets = String(p.assets || 0);
|
||
card.dataset.videos = String(p.videos || 0);
|
||
card.dataset.date = p.date || new Date(p.createdAt || Date.now()).toISOString().slice(0, 10);
|
||
card.setAttribute('onclick', `location.href='product-detail.html?t='+Date.now()+'&product=${encodeURIComponent(p.name || '')}'`);
|
||
card.dataset.triview = '0';
|
||
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.5" 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="tri-missing-badge" tabindex="0" role="button" aria-label="缺三视图,查看说明">
|
||
<span class="ico" aria-hidden="true"></span>
|
||
<span class="lbl-mono">缺三视图</span>
|
||
<span class="tri-missing-pop" role="tooltip">
|
||
<span class="pop-h">
|
||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
|
||
MISSING TRI-VIEW
|
||
</span>
|
||
<span class="pop-body">该商品还未生成 <b>正 / 侧 / 背</b> 三视图。直接生成图片或视频,模型缺少多角度参考,角色一致性、姿态稳定性可能下降。</span>
|
||
<span class="pop-tip">建议:进入 <b>商品详情</b> 先补齐三视图,再发起后续生成。</span>
|
||
</span>
|
||
</span>
|
||
<span class="ph-frame">${esc(p.name)} · 新建</span>
|
||
</div>
|
||
<div class="product-body">
|
||
<div class="product-name">${esc(p.name)}</div>
|
||
<div class="product-cat">${esc(p.cat || '美妆个护')}</div>
|
||
<div class="product-date">${esc(p.date || '')} 创建</div>
|
||
</div>
|
||
<div class="product-footer">
|
||
<span class="stat">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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>${p.assets || 0}</b>
|
||
</span>
|
||
<span class="sep">·</span>
|
||
<span class="stat">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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>${p.videos || 0}</b>
|
||
</span>
|
||
</div>
|
||
`;
|
||
grid.insertBefore(card, grid.firstChild);
|
||
});
|
||
})();
|
||
|
||
// ============== 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');
|
||
const icon = window.IconKit
|
||
? window.IconKit.svg(isAsset ? 'images' : 'video', {
|
||
size: 14,
|
||
strokeWidth: 1.25,
|
||
className: 'product-stat-icon'
|
||
})
|
||
: (svg ? svg.outerHTML : '');
|
||
// 重组结构: <svg> <span.stat-default>素材 <b>N</b></span> <span.stat-hover>去生成素材</span>
|
||
stat.innerHTML = '';
|
||
if (icon) stat.insertAdjacentHTML('beforeend', icon);
|
||
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.replace(/^\s*\+\s*/, '');
|
||
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.5" 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.5" 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 => ({ '<':'<','>':'>','&':'&','"':'"' })[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="1.5" 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 = '';
|
||
}
|
||
});
|
||
|
||
// 创建商品 · 持久化到 localStorage + 立即跳详情(中间不闪商品库)
|
||
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;
|
||
}
|
||
const confirmedBullets = [...document.querySelectorAll('#pf-bullets .bl-item .bl-text')]
|
||
.map(el => el.textContent.trim()).filter(Boolean);
|
||
const pendingInput = document.querySelector('#pf-bullets .bl-add .bl-input');
|
||
const pendingText = (pendingInput?.value || '').trim();
|
||
if (pendingText) confirmedBullets.push(pendingText);
|
||
if (confirmedBullets.length === 0) {
|
||
Shell.toast('请填写核心卖点', '至少 1 条 · 回车确认');
|
||
pendingInput?.focus();
|
||
return;
|
||
}
|
||
const bullets = confirmedBullets;
|
||
// 持久化到 sessionStorage('fs-extra-products') · 仅在当前标签页生命周期内有效
|
||
// 关闭标签页/浏览器后自动清空,不会跨会话累积演示残留
|
||
try {
|
||
const KEY = 'fs-extra-products';
|
||
const list = JSON.parse(sessionStorage.getItem(KEY) || '[]');
|
||
list.push({
|
||
id: 'pp-' + Date.now(),
|
||
name, cat,
|
||
tags: '',
|
||
assets: 0,
|
||
videos: 0,
|
||
bullets,
|
||
date: new Date().toISOString().slice(0, 10),
|
||
createdAt: Date.now(),
|
||
});
|
||
sessionStorage.setItem(KEY, JSON.stringify(list));
|
||
} catch (e) { /* storage 不可用降级到只跳转 */ }
|
||
|
||
// 不 close drawer · 跳转期间 drawer 仍覆盖 host 页面 → 视觉上彻底消除"闪商品库"
|
||
// 浏览器导航开始后,整页会被新页面替换,drawer 自然消失
|
||
Shell.toast('商品已创建 · 跳转详情', `+ ${name} · ${bullets.length} 条卖点`);
|
||
location.href = 'product-detail.html?t=' + Date.now() + '&product=' + encodeURIComponent(name) + '&id=new';
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|