feat(v2): 添加 V2.1 设计稿目录 · 团队/设置页 · pipeline 多项 mock 优化
This commit is contained in:
@@ -0,0 +1,578 @@
|
||||
/* ============================================================
|
||||
新建商品 · 共享 Drawer 模块
|
||||
----------------------------------------------------------
|
||||
在任意页面只需 <script src="assets/new-product-drawer.js"> 引入,
|
||||
然后调用 NewProductDrawer.open({ onSave: fn }) 即可在当前页之上
|
||||
弹出右侧 Drawer。点击遮罩 / X / 取消 / ESC 关闭后,用户停在原页面。
|
||||
|
||||
提供:
|
||||
window.NewProductDrawer.open(opts?)
|
||||
window.NewProductDrawer.close()
|
||||
opts 字段:
|
||||
onSave(product) — 保存时回调,product = { id, name, cat, target,
|
||||
points: string[], images: { id, dataUrl, name }[] }
|
||||
============================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
if (window.NewProductDrawer) return; // idempotent
|
||||
|
||||
const DRAWER_ID = 'npd-drawer';
|
||||
const DRAWER_BG_ID = 'npd-drawer-bg';
|
||||
|
||||
/* ---------- 注入样式(独立 namespace 以免与 products.html 冲突) ---------- */
|
||||
|
||||
const CSS = `
|
||||
/* drawer base (相同尺寸/动画) */
|
||||
#${DRAWER_BG_ID} {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(21, 20, 15, .32);
|
||||
display: none; z-index: 1100;
|
||||
}
|
||||
#${DRAWER_BG_ID}.show { display: block; }
|
||||
#${DRAWER_ID} {
|
||||
position: fixed; right: 0; top: 0; bottom: 0;
|
||||
width: 820px; max-width: 100vw;
|
||||
background: var(--surface);
|
||||
border-left: 1px solid var(--border-faint);
|
||||
z-index: 1101;
|
||||
transform: translateX(100%);
|
||||
transition: transform .25s cubic-bezier(.32, .72, 0, 1);
|
||||
display: flex; flex-direction: column;
|
||||
box-shadow: -4px 0 24px rgba(21, 20, 15, .04);
|
||||
}
|
||||
#${DRAWER_ID}.show { transform: translateX(0); }
|
||||
#${DRAWER_ID} .drawer-h {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--border-faint);
|
||||
display: flex; align-items: center;
|
||||
}
|
||||
#${DRAWER_ID} .drawer-h h3 { font-size: 16px; font-weight: 600; color: var(--accent-black); }
|
||||
#${DRAWER_ID} .drawer-h .x {
|
||||
margin-left: auto; width: 32px; height: 32px;
|
||||
border-radius: var(--r-md);
|
||||
display: grid; place-items: center;
|
||||
color: var(--black-alpha-56); cursor: pointer;
|
||||
background: transparent; border: 0;
|
||||
transition: background var(--t-base);
|
||||
}
|
||||
#${DRAWER_ID} .drawer-h .x:hover { background: var(--black-alpha-4); color: var(--accent-black); }
|
||||
#${DRAWER_ID} .drawer-b { padding: 24px 28px; overflow-y: auto; flex: 1; overscroll-behavior: contain; }
|
||||
#${DRAWER_ID} .drawer-f {
|
||||
padding: 14px 24px;
|
||||
border-top: 1px solid var(--border-faint);
|
||||
display: flex; gap: 10px; align-items: center;
|
||||
background: var(--surface);
|
||||
}
|
||||
#${DRAWER_ID} .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);
|
||||
}
|
||||
#${DRAWER_ID} .drawer-f .btn-guide:hover { color: var(--accent-black); background: var(--black-alpha-4); }
|
||||
#${DRAWER_ID} .drawer-f .btn-guide svg { width: 14px; height: 14px; }
|
||||
|
||||
/* form-card */
|
||||
#${DRAWER_ID} .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);
|
||||
}
|
||||
#${DRAWER_ID} .field { margin-bottom: 16px; }
|
||||
#${DRAWER_ID} .field:last-child { margin-bottom: 0; }
|
||||
#${DRAWER_ID} .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; }
|
||||
#${DRAWER_ID} .field-label {
|
||||
display: block; font-size: 13px; font-weight: 500;
|
||||
color: var(--accent-black); margin-bottom: 6px;
|
||||
}
|
||||
#${DRAWER_ID} .field-label .req { color: var(--heat); margin-left: 2px; }
|
||||
#${DRAWER_ID} .field-label .opt {
|
||||
color: var(--black-alpha-48); font-weight: 400; font-size: 12px; margin-left: 6px;
|
||||
}
|
||||
#${DRAWER_ID} .input,
|
||||
#${DRAWER_ID} .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);
|
||||
}
|
||||
#${DRAWER_ID} .input:focus,
|
||||
#${DRAWER_ID} .select:focus {
|
||||
border-color: var(--heat-40);
|
||||
box-shadow: inset 0 0 0 1px var(--heat-40);
|
||||
}
|
||||
|
||||
/* upload */
|
||||
#${DRAWER_ID} .pf-upload-row {
|
||||
display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
|
||||
gap: 16px; align-items: stretch;
|
||||
}
|
||||
#${DRAWER_ID} .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;
|
||||
}
|
||||
#${DRAWER_ID} .pf-upload-zone:hover { border-color: var(--heat); background: var(--heat-8); }
|
||||
#${DRAWER_ID} .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;
|
||||
}
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-ic svg { width: 20px; height: 20px; }
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-t { font-size: 14px; color: var(--accent-black); font-weight: 500; }
|
||||
#${DRAWER_ID} .pf-upload-zone .uz-t strong { color: var(--heat); font-weight: 600; }
|
||||
#${DRAWER_ID} .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;
|
||||
}
|
||||
#${DRAWER_ID} .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;
|
||||
}
|
||||
#${DRAWER_ID} .pf-example .ex-h { font-size: 13px; font-weight: 600; color: var(--accent-black); }
|
||||
#${DRAWER_ID} .pf-example .ex-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||
#${DRAWER_ID} .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);
|
||||
}
|
||||
#${DRAWER_ID} .pf-example .ex-grid .ex-thumb svg { width: 22px; height: 22px; }
|
||||
#${DRAWER_ID} .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;
|
||||
}
|
||||
#${DRAWER_ID} .pf-example .ex-d { font-size: 12px; color: var(--black-alpha-56); line-height: 1.5; }
|
||||
#${DRAWER_ID} .pf-grid {
|
||||
display: grid; grid-template-columns: repeat(5, 1fr);
|
||||
gap: 8px; margin-top: 12px;
|
||||
}
|
||||
#${DRAWER_ID} .pf-grid:empty { display: none; }
|
||||
#${DRAWER_ID} .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;
|
||||
}
|
||||
#${DRAWER_ID} .pf-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||
#${DRAWER_ID} .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);
|
||||
}
|
||||
#${DRAWER_ID} .pf-thumb:hover .pf-x { opacity: 1; }
|
||||
#${DRAWER_ID} .pf-thumb .pf-x svg { width: 11px; height: 11px; }
|
||||
|
||||
/* bullet-list */
|
||||
#${DRAWER_ID} .bullet-list { list-style: none; padding: 0; margin: 0; }
|
||||
#${DRAWER_ID} .bullet-list .bl-item,
|
||||
#${DRAWER_ID} .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;
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-add { background: transparent; border-style: dashed; }
|
||||
#${DRAWER_ID} .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;
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-text { flex: 1; color: var(--accent-black); }
|
||||
#${DRAWER_ID} .bullet-list .bl-input {
|
||||
flex: 1; background: transparent; border: 0; outline: none;
|
||||
font-size: 13.5px; color: var(--accent-black); font-family: inherit;
|
||||
}
|
||||
#${DRAWER_ID} .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);
|
||||
}
|
||||
#${DRAWER_ID} .bullet-list .bl-x:hover { color: var(--accent-crimson); background: var(--crimson-bg); }
|
||||
#${DRAWER_ID} .bullet-list .bl-x svg { width: 11px; height: 11px; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
#${DRAWER_ID} .pf-upload-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
`;
|
||||
|
||||
const HTML = `
|
||||
<div id="${DRAWER_BG_ID}"></div>
|
||||
<aside id="${DRAWER_ID}" role="dialog" aria-label="新建商品" aria-hidden="true">
|
||||
<div class="drawer-h">
|
||||
<h3>新建商品</h3>
|
||||
<button class="x" type="button" data-act="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="form-h">基础信息</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">商品名称<span class="req">*</span></label>
|
||||
<input class="input" data-f="name" placeholder="请输入商品名称(必填)" maxlength="100">
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div>
|
||||
<label class="field-label">品类<span class="req">*</span></label>
|
||||
<select class="select" data-f="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" data-f="target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">商品主图<span class="req">*</span></label>
|
||||
<input type="file" data-f="file" accept="image/*" multiple hidden>
|
||||
<div class="pf-upload-row">
|
||||
<div class="pf-upload-zone" data-act="upload-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" data-f="grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="field" style="margin-bottom: 0;">
|
||||
<label class="field-label">核心卖点<span class="req">*</span></label>
|
||||
<ul class="bullet-list" data-f="bullets">
|
||||
<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" data-act="guide">
|
||||
<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" data-act="cancel">取消</button>
|
||||
<button class="btn btn-primary" type="button" data-act="save">
|
||||
<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>
|
||||
`;
|
||||
|
||||
/* ---------- DOM refs (populated by ensureInjected) ---------- */
|
||||
let injected = false;
|
||||
let bg, drawer, $f, $grid, $bullets, $blInput;
|
||||
let currentOpts = {};
|
||||
const PF_MAX = 5;
|
||||
let pfFiles = []; // { id, dataUrl, name }
|
||||
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 esc(s) { return String(s == null ? '' : s).replace(/[<>&"]/g, c => ({ '<':'<','>':'>','&':'&','"':'"' })[c]); }
|
||||
function uid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 5); }
|
||||
function toast(msg, sub) {
|
||||
if (typeof Shell !== 'undefined' && Shell && Shell.toast) Shell.toast(msg, sub);
|
||||
}
|
||||
|
||||
function ensureInjected() {
|
||||
if (injected) return;
|
||||
// style
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.textContent = CSS;
|
||||
document.head.appendChild(styleEl);
|
||||
// html
|
||||
const wrap = document.createElement('div');
|
||||
wrap.innerHTML = HTML;
|
||||
while (wrap.firstChild) document.body.appendChild(wrap.firstChild);
|
||||
|
||||
bg = document.getElementById(DRAWER_BG_ID);
|
||||
drawer = document.getElementById(DRAWER_ID);
|
||||
$f = {
|
||||
name: drawer.querySelector('[data-f="name"]'),
|
||||
cat: drawer.querySelector('[data-f="cat"]'),
|
||||
target: drawer.querySelector('[data-f="target"]'),
|
||||
file: drawer.querySelector('[data-f="file"]'),
|
||||
};
|
||||
$grid = drawer.querySelector('[data-f="grid"]');
|
||||
$bullets = drawer.querySelector('[data-f="bullets"]');
|
||||
$blInput = $bullets.querySelector('.bl-add .bl-input');
|
||||
|
||||
bindEvents();
|
||||
injected = true;
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
// 关闭交互
|
||||
bg.addEventListener('click', close);
|
||||
drawer.addEventListener('click', e => {
|
||||
const a = e.target.closest('[data-act]');
|
||||
if (!a) return;
|
||||
const act = a.dataset.act;
|
||||
if (act === 'close') return close();
|
||||
if (act === 'cancel') return close();
|
||||
if (act === 'guide') return toast('使用指南', '点击查看完整填写指南');
|
||||
if (act === 'save') return save();
|
||||
if (act === 'upload-zone') return openFilePicker();
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && drawer.classList.contains('show')) close();
|
||||
});
|
||||
|
||||
// 上传
|
||||
$f.file.addEventListener('change', e => { addFiles(e.target.files); e.target.value = ''; });
|
||||
const zone = drawer.querySelector('[data-act="upload-zone"]');
|
||||
zone.addEventListener('dragover', e => { e.preventDefault(); zone.style.borderColor = 'var(--heat)'; });
|
||||
zone.addEventListener('dragleave', () => { zone.style.borderColor = ''; });
|
||||
zone.addEventListener('drop', e => {
|
||||
e.preventDefault(); zone.style.borderColor = '';
|
||||
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) addFiles(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
// 卖点 bullet-list
|
||||
$blInput.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); blAdd($blInput.value); $blInput.value = ''; }
|
||||
});
|
||||
}
|
||||
|
||||
function openFilePicker() { if (pfFiles.length < PF_MAX) $f.file.click(); }
|
||||
|
||||
function pfRender() {
|
||||
$grid.innerHTML = pfFiles.map(u => `
|
||||
<div class="pf-thumb" data-id="${u.id}">
|
||||
<img src="${u.dataUrl}" alt="${esc(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('');
|
||||
$grid.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 addFiles(fileList) {
|
||||
const room = PF_MAX - pfFiles.length;
|
||||
if (room <= 0) { 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: uid(), dataUrl: e.target.result, name: f.name });
|
||||
if (++done === incoming.length) {
|
||||
pfRender();
|
||||
toast('已上传', '+ ' + done + ' 张 · 共 ' + pfFiles.length + ' / ' + PF_MAX);
|
||||
}
|
||||
};
|
||||
r.readAsDataURL(f);
|
||||
});
|
||||
}
|
||||
|
||||
function blRenumber() {
|
||||
[...$bullets.querySelectorAll('.bl-item')].forEach((li, i) => {
|
||||
li.querySelector('.num').textContent = i + 1;
|
||||
});
|
||||
}
|
||||
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">' + esc(t) + '</span><span class="bl-x" title="删除">' + blXSvg + '</span>';
|
||||
$bullets.querySelector('.bl-add').before(li);
|
||||
li.querySelector('.bl-x').addEventListener('click', () => {
|
||||
li.style.transition = 'opacity .15s, transform .15s';
|
||||
li.style.opacity = 0;
|
||||
li.style.transform = 'translateX(-8px)';
|
||||
setTimeout(() => { li.remove(); blRenumber(); }, 150);
|
||||
});
|
||||
blRenumber();
|
||||
}
|
||||
function getBullets() {
|
||||
return [...$bullets.querySelectorAll('.bl-item .bl-text')].map(t => t.textContent.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/* ---------- API ---------- */
|
||||
|
||||
function resetForm() {
|
||||
$f.name.value = '';
|
||||
$f.cat.value = $f.cat.options[0].value;
|
||||
$f.target.value = '';
|
||||
pfFiles = [];
|
||||
pfRender();
|
||||
[...$bullets.querySelectorAll('.bl-item')].forEach(li => li.remove());
|
||||
$blInput.value = '';
|
||||
}
|
||||
|
||||
function lockBody() {
|
||||
// 优先用 Shell 的引用计数实现(避免多 overlay 互相解锁)
|
||||
if (typeof Shell !== 'undefined' && Shell && typeof Shell.lockScroll === 'function') {
|
||||
Shell.lockScroll();
|
||||
return;
|
||||
}
|
||||
// 兜底: Shell 未加载时本地锁
|
||||
const docEl = document.documentElement;
|
||||
const sbw = window.innerWidth - docEl.clientWidth;
|
||||
drawer._lockSnap = {
|
||||
bodyOverflow: document.body.style.overflow,
|
||||
bodyPaddingRight: document.body.style.paddingRight,
|
||||
htmlOverflow: docEl.style.overflow,
|
||||
};
|
||||
document.body.style.overflow = 'hidden';
|
||||
docEl.style.overflow = 'hidden';
|
||||
if (sbw > 0) document.body.style.paddingRight = sbw + 'px';
|
||||
}
|
||||
function unlockBody() {
|
||||
if (typeof Shell !== 'undefined' && Shell && typeof Shell.unlockScroll === 'function') {
|
||||
Shell.unlockScroll();
|
||||
return;
|
||||
}
|
||||
const s = drawer._lockSnap;
|
||||
if (s) {
|
||||
document.body.style.overflow = s.bodyOverflow;
|
||||
document.body.style.paddingRight = s.bodyPaddingRight;
|
||||
document.documentElement.style.overflow = s.htmlOverflow;
|
||||
drawer._lockSnap = null;
|
||||
}
|
||||
}
|
||||
|
||||
function open(opts) {
|
||||
ensureInjected();
|
||||
if (drawer.classList.contains('show')) return; // 已开则不重复锁
|
||||
currentOpts = opts || {};
|
||||
resetForm();
|
||||
bg.classList.add('show');
|
||||
drawer.classList.add('show');
|
||||
drawer.setAttribute('aria-hidden', 'false');
|
||||
lockBody();
|
||||
setTimeout(() => $f.name.focus(), 280);
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!injected) return;
|
||||
if (!drawer.classList.contains('show')) return; // 已关则不重复解锁
|
||||
bg.classList.remove('show');
|
||||
drawer.classList.remove('show');
|
||||
drawer.setAttribute('aria-hidden', 'true');
|
||||
unlockBody();
|
||||
if (typeof currentOpts.onClose === 'function') currentOpts.onClose();
|
||||
}
|
||||
|
||||
function save() {
|
||||
const name = ($f.name.value || '').trim();
|
||||
const cat = $f.cat.value;
|
||||
const target = ($f.target.value || '').trim();
|
||||
const points = getBullets();
|
||||
const images = pfFiles.slice();
|
||||
|
||||
if (!name) {
|
||||
toast('请填写商品名称');
|
||||
$f.name.focus();
|
||||
return;
|
||||
}
|
||||
if (images.length === 0) {
|
||||
toast('请上传商品主图', '至少 1 张');
|
||||
return;
|
||||
}
|
||||
if (points.length === 0) {
|
||||
toast('请添加核心卖点', '至少 1 条');
|
||||
$blInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const product = {
|
||||
id: 'np-' + uid(),
|
||||
name, cat, target,
|
||||
points,
|
||||
images,
|
||||
imgs: images.length,
|
||||
};
|
||||
toast('商品已创建', '+ ' + name);
|
||||
if (typeof currentOpts.onSave === 'function') currentOpts.onSave(product);
|
||||
close();
|
||||
}
|
||||
|
||||
window.NewProductDrawer = { open, close };
|
||||
|
||||
/* ---------- sessionStorage 自动打开钩子 ---------- */
|
||||
// 任何页面只要在跳转前 sessionStorage.setItem('npd-auto-open','1') 即可,
|
||||
// 落地页加载完模块后,会自动 open() 一次并清掉 flag。
|
||||
// 用于:product-create.html 重定向后让落地页弹出 drawer,而不是用户重新点击。
|
||||
function checkAutoOpen() {
|
||||
try {
|
||||
if (sessionStorage.getItem('npd-auto-open') === '1') {
|
||||
sessionStorage.removeItem('npd-auto-open');
|
||||
// 延后一拍,确保宿主页面自己的 init 已经跑完
|
||||
setTimeout(() => open(), 50);
|
||||
}
|
||||
} catch (e) { /* sessionStorage 不可用就静默放弃 */ }
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', checkAutoOpen);
|
||||
} else {
|
||||
checkAutoOpen();
|
||||
}
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,602 @@
|
||||
/* ============================================================
|
||||
流·Studio · Shell renderer V2.1
|
||||
渲染 sidebar / topbar / 网格背景装饰 / Toast / Modal helpers
|
||||
每个页面调用 Shell.render({ active, crumbs, balance, topActions })
|
||||
|
||||
V2.1 变化:
|
||||
- sidebar 搜索 ⌘K → "Ctrl K" Inter Bold 平铺(无 kbd 边框)
|
||||
============================================================ */
|
||||
|
||||
const NAV = [
|
||||
{
|
||||
id: 'dashboard', label: '工作台', href: 'index.html',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 12 12 3l9 9"/><path d="M5 10v10h14V10"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'products', label: '商品库', href: 'products.html', badge: '7',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4M3 17l9 4 9-4"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'projects', label: '视频项目', href: 'projects.html', badge: '8',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="18" height="16"/><path d="M7 4v16M16 4v16M3 9h18M3 15h18"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'asset-factory', label: '图片生成', href: 'asset-factory.html',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.7 4.6L18 9l-4.3 1.4L12 15l-1.7-4.6L6 9l4.3-1.4L12 3z"/><path d="M19 16l.85 2.3L22 19l-2.15.7L19 22l-.85-2.3L16 19l2.15-.7L19 16z"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'library', label: '资产库', href: 'library.html',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="4" width="6" height="16"/><rect x="11" y="4" width="4" height="16"/><rect x="17" y="6" width="4" height="14"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'team', label: '团队', href: 'team.html', badge: '5',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="9" cy="8" r="3"/><circle cx="17" cy="9" r="2.5"/><path d="M3 19c0-3 2.7-5 6-5s6 2 6 5M14 19c.5-2.4 2.4-4 5-4 .8 0 1.5.2 2 .5"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'account', label: '账户', href: 'account.html',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 14h2"/></svg>'
|
||||
},
|
||||
{
|
||||
id: 'settings', label: '设置', href: 'settings.html',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8 2 2 0 0 1-2.8 2.8 1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5 2 2 0 0 1-4 0 1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3 2 2 0 0 1-2.8-2.8 1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1 2 2 0 0 1 0-4 1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8 2 2 0 0 1 2.8-2.8 1.7 1.7 0 0 0 1.8.3 1.7 1.7 0 0 0 1-1.5 2 2 0 0 1 4 0 1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3 2 2 0 0 1 2.8 2.8 1.7 1.7 0 0 0-.3 1.8 1.7 1.7 0 0 0 1.5 1 2 2 0 0 1 0 4 1.7 1.7 0 0 0-1.5 1.1Z"/></svg>'
|
||||
}
|
||||
];
|
||||
|
||||
window.Shell = {
|
||||
render({ active = '', crumbs = [], balance = '¥327.40', topActions = '' } = {}) {
|
||||
const navHtml = NAV.map(n => `
|
||||
<a href="${n.href}" class="${active === n.id ? 'active' : ''}">
|
||||
${n.icon}
|
||||
<span>${n.label}</span>
|
||||
${n.badge ? `<span class="pill-mini">${n.badge}</span>` : ''}
|
||||
</a>
|
||||
`).join('');
|
||||
|
||||
const sidebar = `
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<div class="flame"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2c1 3 4 5 4 9a4 4 0 0 1-4 4 4 4 0 0 1-4-4 5 5 0 0 1 1.5-3.5C10.5 6 11.5 4 12 2zm-1 13c0 2 1 3 1 5 1-1 3-2 3-5 0-1.5-1-2-2-3-1 1-2 2-2 3z"/></svg></div>
|
||||
<div><div class="name">流·Studio</div></div>
|
||||
</div>
|
||||
<div class="search-box" onclick="document.getElementById('global-search').focus()">
|
||||
<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 id="global-search" placeholder="搜索"/>
|
||||
<span class="kbd">Ctrl K</span>
|
||||
</div>
|
||||
<div class="nav-section">主要</div>
|
||||
<nav>${navHtml}</nav>
|
||||
<div class="aside-foot">
|
||||
<div class="user" onclick="Shell.toast('账户菜单', 'li@shop.com')">
|
||||
<div class="av">李</div>
|
||||
<div class="em">小李的店</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
`;
|
||||
|
||||
const crumbHtml = crumbs.length ? `
|
||||
<div class="crumbs">
|
||||
${crumbs.map((c, i) => {
|
||||
const last = i === crumbs.length - 1;
|
||||
const sep = i > 0 ? '<span class="sep">/</span>' : '';
|
||||
if (last) return `${sep}<span class="here">${c.label}</span>`;
|
||||
return `${sep}<a href="${c.href || '#'}">${c.label}</a>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const topbar = `
|
||||
<header class="topbar">
|
||||
${crumbHtml}
|
||||
<div class="right">
|
||||
<span class="balance-chip" onclick="location.href='account.html'">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 7v10M9 10h4a1.5 1.5 0 0 1 0 3h-3a1.5 1.5 0 0 0 0 3h4"/></svg>
|
||||
余额 <strong>${balance}</strong>
|
||||
</span>
|
||||
<button class="queue-chip" onclick="Shell.toast('任务队列', '3 个进行中')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
|
||||
任务队列
|
||||
<span class="count">3</span>
|
||||
</button>
|
||||
<button class="icon-btn" onclick="Shell.toast('通知中心', '12 条未读')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10 21a2 2 0 0 0 4 0"/></svg>
|
||||
<span class="count-noti">12</span>
|
||||
</button>
|
||||
<div class="topbar-avatar" onclick="Shell.toast('账户菜单', '李 · li@shop.com')" title="账户">
|
||||
<span>李</span>
|
||||
</div>
|
||||
${topActions}
|
||||
</div>
|
||||
</header>
|
||||
`;
|
||||
|
||||
const decorations = `
|
||||
<div class="grid-bg"></div>
|
||||
<span class="sq-mark" style="top:238px;left:478px"></span>
|
||||
<span class="sq-mark" style="top:478px;left:1198px"></span>
|
||||
<span class="sq-mark" style="bottom:300px;left:238px"></span>
|
||||
<span class="sq-mark" style="top:718px;right:240px"></span>
|
||||
`;
|
||||
|
||||
const toastHtml = `
|
||||
<div class="toast" id="__toast">
|
||||
<div class="ic-t"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg></div>
|
||||
<div class="txt" id="__toast-txt">操作成功<span class="mono">[ 200 OK ]</span></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// ─── 全局 Lightbox · 任意页面可用 Shell._openLightbox(src, name) ──
|
||||
const lightboxHtml = `
|
||||
<div class="np-lightbox" id="np-lightbox" onclick="Shell._closeLightbox()">
|
||||
<button class="lb-x" type="button" onclick="event.stopPropagation();Shell._closeLightbox()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
<img id="np-lightbox-img" alt="">
|
||||
<span class="lb-name" id="np-lightbox-name"></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// [DEPRECATED · 弹窗已废弃,创建商品直接进 product-create.html]
|
||||
const _deprecatedModalHtml = `
|
||||
<div class="modal-bg" id="new-product-bg" onclick="if(event.target===this)Shell.closeModal('new-product-bg')">
|
||||
<div class="modal new-product-modal">
|
||||
<span class="corner-tr"></span>
|
||||
<span class="corner-bl"></span>
|
||||
|
||||
<div class="np-header">
|
||||
<div class="np-title-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 9-4 9 4-9 4-9-4z"/><path d="m3 12 9 4 9-4M3 17l9 4 9-4"/></svg>
|
||||
</div>
|
||||
<h2>新建商品</h2>
|
||||
<span class="np-mode-pill">[ UPLOAD MODE ]</span>
|
||||
<button class="np-x" type="button" onclick="Shell.closeModal('new-product-bg')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="np-body">
|
||||
<div class="np-body-grid">
|
||||
|
||||
<!-- 左栏: 图片 -->
|
||||
<div class="np-left">
|
||||
<!-- AI CTA: 独立全宽区域,放在 商品图册 上方 -->
|
||||
<a class="np-ai-cta primary" id="np-ai-cta" title="不上传也能用 AI 生成图">
|
||||
<span class="ai-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l1.5 4.5L18 8l-4.5 1.5L12 14l-1.5-4.5L6 8l4.5-1.5L12 2zM19 14l.9 2.7 2.6.8-2.6.8L19 21l-.9-2.7-2.6-.8 2.6-.8L19 14zM5 14l.7 2.1L7.8 17l-2.1.7L5 20l-.7-2.3-2.1-.7 2.1-.7L5 14z"/></svg>
|
||||
</span>
|
||||
<span class="ai-label" id="np-ai-label">没有图? 让 AI 帮我生成</span>
|
||||
<span class="ai-arrow">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div class="field" style="margin-bottom: 12px;">
|
||||
<label class="field-label">商品图册<span class="req">*</span></label>
|
||||
<input type="file" id="np-file" accept="image/*" multiple hidden>
|
||||
|
||||
<!-- 图片案例 (静态示例) -->
|
||||
<div class="np-section-h">
|
||||
<span class="check-ic"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 8 7 12 13 4"/></svg></span>
|
||||
图片案例
|
||||
</div>
|
||||
<div class="np-section-sub">上传您的商品的多角度白底图和使用图</div>
|
||||
<div class="np-examples">
|
||||
<div class="ex"><span>白底主图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
|
||||
<div class="ex"><span>多角度</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
|
||||
<div class="ex"><span>细节图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
|
||||
<div class="ex"><span>使用图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
|
||||
<div class="ex"><span>包装图</span><span class="ex-badge"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 21h4V9H2v12zM23 10c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg></span></div>
|
||||
</div>
|
||||
|
||||
<!-- 我的上传 (动态 5 槽) -->
|
||||
<div class="np-section-h">
|
||||
我的上传
|
||||
<span class="counter">( <span class="num" id="np-upload-count">0</span> / 5 )</span>
|
||||
</div>
|
||||
<div class="upload-grid" id="np-grid"></div>
|
||||
|
||||
<!-- Dropzone (放在 grid 之后, 满 5 张时禁用) -->
|
||||
<div class="upload-zone" id="np-zone" style="margin-top: 10px;">
|
||||
<span 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>
|
||||
</span>
|
||||
<span id="np-zone-text">点击或拖拽上传图片</span>
|
||||
<span class="uz-hint">// 支持多选 · 最多 5 张 · JPG / PNG / WEBP</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右栏: 文案 -->
|
||||
<div class="np-right">
|
||||
<div class="field">
|
||||
<label class="field-label">商品名称<span class="req">*</span></label>
|
||||
<input class="input" id="np-name" placeholder="例: 透真玻尿酸补水面膜">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">品类</label>
|
||||
<select class="select" id="np-cat">
|
||||
<option>美妆个护</option>
|
||||
<option>服饰内衣</option>
|
||||
<option>食品饮料</option>
|
||||
<option>家居家电</option>
|
||||
<option>数码 3C</option>
|
||||
<option>个护清洁</option>
|
||||
<option>运动户外</option>
|
||||
<option>母婴亲子</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">核心卖点<span class="req">*</span></label>
|
||||
<ul class="bullet-list" id="np-bullets" data-bl>
|
||||
<li class="bl-add"><span class="num">+</span><input class="bl-input" placeholder="添加新卖点 · 回车确认"></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:0;">
|
||||
<label class="field-label">目标人群</label>
|
||||
<input class="input" id="np-target" placeholder="例: 22-32 岁女性、敏感肌、办公室通勤">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="np-footer">
|
||||
<span class="np-meta">// 上传模式 · <span class="accent">不消耗 token</span> · 一次性创建</span>
|
||||
<button class="btn" type="button" onclick="Shell.closeModal('new-product-bg')">取消</button>
|
||||
<button class="btn btn-primary" type="button" id="np-create" disabled style="opacity:.5;cursor:not-allowed;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
|
||||
创建商品
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lightbox · 缩略图全屏预览 -->
|
||||
<div class="np-lightbox" id="np-lightbox" onclick="Shell._closeLightbox()">
|
||||
<button class="lb-x" type="button" onclick="event.stopPropagation();Shell._closeLightbox()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
<img id="np-lightbox-img" alt="">
|
||||
<span class="lb-name" id="np-lightbox-name"></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 装订线 SVG 准星 · V2.1 签名元素(圆弧内凹的"+")
|
||||
const cornerSvg = `<path d="M10.5 4C10.5 7.31371 7.81371 10 4.5 10H0.5V11H4.5C7.81371 11 10.5 13.6863 10.5 17V21H11.5V17C11.5 13.6863 14.1863 11 17.5 11H21.5V10H17.5C14.1863 10 11.5 7.31371 11.5 4V0H10.5V4Z" fill="currentColor"/>`;
|
||||
const cornerMarks = `
|
||||
<span class="corner-mark tl"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
|
||||
<span class="corner-mark tr"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
|
||||
<span class="corner-mark bl"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
|
||||
<span class="corner-mark br"><svg viewBox="0 0 22 21" fill="none">${cornerSvg}</svg></span>
|
||||
`;
|
||||
|
||||
const app = document.createElement('div');
|
||||
app.className = 'app';
|
||||
app.innerHTML = sidebar + `<main>${decorations}${topbar}<div class="content" id="page-content">${cornerMarks}</div></main>`;
|
||||
|
||||
const src = document.getElementById('page');
|
||||
document.body.prepend(app);
|
||||
if (src) {
|
||||
// 把页面 body 内容追加到 .content,保留 4 个 corner-mark SVG
|
||||
document.getElementById('page-content').insertAdjacentHTML('beforeend', src.innerHTML);
|
||||
src.remove();
|
||||
}
|
||||
document.body.insertAdjacentHTML('beforeend', toastHtml);
|
||||
document.body.insertAdjacentHTML('beforeend', lightboxHtml);
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
document.getElementById('global-search')?.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// [DEPRECATED] 新建商品弹窗 · 已废弃,创建商品改为直跳 product-create.html
|
||||
_bindNewProductModal_DEPRECATED() {
|
||||
const modal = document.getElementById('new-product-bg');
|
||||
if (!modal) return;
|
||||
|
||||
// 上传图册的内存状态 (供 collect 和 AI CTA 用)
|
||||
const uploads = []; // { id, dataUrl, name, type, size }
|
||||
|
||||
// 收集表单状态(供 AI CTA 和创建按钮用)
|
||||
const collect = () => {
|
||||
const get = (id) => document.getElementById(id)?.value?.trim() || '';
|
||||
return {
|
||||
name: get('np-name'),
|
||||
cat: get('np-cat'),
|
||||
target: get('np-target'),
|
||||
bullets: [...document.querySelectorAll('#np-bullets .bl-item .bl-text')].map(el => el.textContent),
|
||||
uploadedCount: uploads.length,
|
||||
uploads: uploads.map(u => ({ name: u.name, type: u.type })),
|
||||
};
|
||||
};
|
||||
|
||||
const MAX_UPLOADS = 5;
|
||||
|
||||
// 创建按钮: 商品名 + 至少 1 张图
|
||||
const nameInput = document.getElementById('np-name');
|
||||
const createBtn = document.getElementById('np-create');
|
||||
const updateCreateBtn = () => {
|
||||
const nameOk = (nameInput.value || '').trim().length > 0;
|
||||
const uploadOk = uploads.length >= 1;
|
||||
const ok = nameOk && uploadOk;
|
||||
createBtn.disabled = !ok;
|
||||
createBtn.style.opacity = ok ? '' : '.5';
|
||||
createBtn.style.cursor = ok ? '' : 'not-allowed';
|
||||
// 提示用户缺什么
|
||||
if (!nameOk) createBtn.title = '请填写商品名称';
|
||||
else if (!uploadOk) createBtn.title = '请至少上传 1 张商品图';
|
||||
else createBtn.title = '';
|
||||
};
|
||||
nameInput.addEventListener('input', updateCreateBtn);
|
||||
createBtn.addEventListener('click', () => {
|
||||
if (createBtn.disabled) return;
|
||||
const s = collect();
|
||||
Shell.toast('商品已创建', `+ ${s.name}`);
|
||||
Shell.closeModal('new-product-bg');
|
||||
});
|
||||
|
||||
// 核心卖点: bullet-list
|
||||
const list = document.getElementById('np-bullets');
|
||||
const addInput = list.querySelector('.bl-add .bl-input');
|
||||
const xSvg = '<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>';
|
||||
const renumber = () => {
|
||||
[...list.querySelectorAll('.bl-item')].forEach((li, i) => {
|
||||
li.querySelector('.num').textContent = i + 1;
|
||||
});
|
||||
};
|
||||
const bindX = (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(); renumber(); }, 150);
|
||||
});
|
||||
};
|
||||
const addBullet = (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">${t.replace(/[<>&]/g, c => ({ '<':'<','>':'>','&':'&' })[c])}</span><span class="bl-x" title="删除">${xSvg}</span>`;
|
||||
list.querySelector('.bl-add').before(li);
|
||||
bindX(li.querySelector('.bl-x'));
|
||||
renumber();
|
||||
};
|
||||
addInput?.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addBullet(addInput.value);
|
||||
addInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 上传组件: 批量上传(MAX 5) + 5 固定槽 + 预览/删除 + AI CTA 联动
|
||||
const zone = document.getElementById('np-zone');
|
||||
const zoneText = document.getElementById('np-zone-text');
|
||||
const grid = document.getElementById('np-grid');
|
||||
const fileInput = document.getElementById('np-file');
|
||||
const aiCtaEl = document.getElementById('np-ai-cta');
|
||||
const aiLabel = document.getElementById('np-ai-label');
|
||||
const uploadCount = document.getElementById('np-upload-count');
|
||||
|
||||
const syncAiCta = () => {
|
||||
if (uploads.length === 0) {
|
||||
aiCtaEl.classList.add('primary');
|
||||
aiLabel.textContent = '没有图? 让 AI 帮我生成';
|
||||
} else {
|
||||
aiCtaEl.classList.remove('primary');
|
||||
aiLabel.textContent = '用 AI 加工 / 生成模特上身图';
|
||||
}
|
||||
};
|
||||
|
||||
const syncZone = () => {
|
||||
const full = uploads.length >= MAX_UPLOADS;
|
||||
zone.classList.toggle('full', full);
|
||||
zoneText.textContent = full ? `已达上限 (${MAX_UPLOADS} / ${MAX_UPLOADS})` : '点击或拖拽上传图片';
|
||||
};
|
||||
|
||||
const syncCounter = () => {
|
||||
uploadCount.textContent = uploads.length;
|
||||
};
|
||||
|
||||
const thumbXSvg = '<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>';
|
||||
const esc = (s) => s.replace(/[<>&"]/g, c => ({ '<':'<','>':'>','&':'&','"':'"' })[c]);
|
||||
|
||||
const renderGrid = () => {
|
||||
const filledHtml = uploads.map(u => `
|
||||
<div class="up-thumb" data-id="${u.id}">
|
||||
<img src="${u.dataUrl}" alt="${esc(u.name)}">
|
||||
<button class="slot-x" type="button" title="删除">${thumbXSvg}</button>
|
||||
</div>
|
||||
`).join('');
|
||||
const emptyCount = MAX_UPLOADS - uploads.length;
|
||||
const emptyHtml = Array.from({ length: emptyCount }, () => `<div class="up-slot-empty" data-action="add">无预览</div>`).join('');
|
||||
grid.innerHTML = filledHtml + emptyHtml;
|
||||
|
||||
// 已填充槽: 点击预览 + X 删除
|
||||
grid.querySelectorAll('.up-thumb').forEach(thumb => {
|
||||
const id = thumb.dataset.id;
|
||||
thumb.querySelector('.slot-x').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const idx = uploads.findIndex(u => u.id === id);
|
||||
if (idx >= 0) {
|
||||
const removed = uploads.splice(idx, 1)[0];
|
||||
Shell.toast('已删除', removed.name);
|
||||
refreshAll();
|
||||
}
|
||||
});
|
||||
thumb.addEventListener('click', () => {
|
||||
const u = uploads.find(x => x.id === id);
|
||||
if (u) Shell._openLightbox(u.dataUrl, u.name);
|
||||
});
|
||||
});
|
||||
// 空槽: 点击触发上传
|
||||
grid.querySelectorAll('[data-action="add"]').forEach(slot => {
|
||||
slot.addEventListener('click', () => fileInput.click());
|
||||
});
|
||||
};
|
||||
|
||||
const refreshAll = () => {
|
||||
renderGrid();
|
||||
syncAiCta();
|
||||
syncZone();
|
||||
syncCounter();
|
||||
updateCreateBtn();
|
||||
};
|
||||
|
||||
const addFiles = (fileList) => {
|
||||
const remaining = MAX_UPLOADS - uploads.length;
|
||||
if (remaining <= 0) {
|
||||
Shell.toast('已达上传上限', `${MAX_UPLOADS} / ${MAX_UPLOADS} 张`);
|
||||
return;
|
||||
}
|
||||
const incoming = [...fileList].filter(f => f.type.startsWith('image/'));
|
||||
if (!incoming.length) return;
|
||||
const accepted = incoming.slice(0, remaining);
|
||||
const overflow = incoming.length - accepted.length;
|
||||
let added = 0;
|
||||
accepted.forEach(f => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
uploads.push({
|
||||
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
||||
dataUrl: e.target.result,
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
size: f.size,
|
||||
});
|
||||
added++;
|
||||
if (added === accepted.length) {
|
||||
refreshAll();
|
||||
const msg = overflow > 0
|
||||
? `+ ${added} 张 · 超出 ${overflow} 张已忽略`
|
||||
: `+ ${added} 张 · 共 ${uploads.length} / ${MAX_UPLOADS}`;
|
||||
Shell.toast('图片已上传', msg);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(f);
|
||||
});
|
||||
};
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
addFiles(e.target.files);
|
||||
e.target.value = '';
|
||||
});
|
||||
zone.addEventListener('click', () => {
|
||||
if (uploads.length < MAX_UPLOADS) fileInput.click();
|
||||
});
|
||||
zone.addEventListener('dragover', e => {
|
||||
e.preventDefault();
|
||||
if (uploads.length < MAX_UPLOADS) zone.style.borderColor = 'var(--heat)';
|
||||
});
|
||||
zone.addEventListener('dragleave', () => { zone.style.borderColor = ''; });
|
||||
zone.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
zone.style.borderColor = '';
|
||||
if (e.dataTransfer?.files?.length) addFiles(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
refreshAll();
|
||||
|
||||
// AI CTA: 把已填表单 + 上传图册存到 sessionStorage,跳到 AI 工作台
|
||||
const aiCta = document.getElementById('np-ai-cta');
|
||||
aiCta.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const s = collect();
|
||||
sessionStorage.setItem('pending-product', JSON.stringify(s));
|
||||
Shell.toast('正在跳转 AI 工作台', `带入 ${s.uploadedCount} 张图`);
|
||||
setTimeout(() => { location.href = 'product-create.html'; }, 350);
|
||||
});
|
||||
|
||||
updateCreateBtn();
|
||||
},
|
||||
|
||||
_openLightbox(src, name) {
|
||||
const lb = document.getElementById('np-lightbox');
|
||||
const img = document.getElementById('np-lightbox-img');
|
||||
const nm = document.getElementById('np-lightbox-name');
|
||||
if (!lb || !img || lb.classList.contains('show')) return;
|
||||
img.src = src;
|
||||
if (nm) nm.textContent = name || '';
|
||||
lb.classList.add('show');
|
||||
this.lockScroll();
|
||||
},
|
||||
|
||||
_closeLightbox() {
|
||||
const lb = document.getElementById('np-lightbox');
|
||||
if (!lb || !lb.classList.contains('show')) return;
|
||||
lb.classList.remove('show');
|
||||
this.unlockScroll();
|
||||
},
|
||||
|
||||
toast(text, mono) {
|
||||
const t = document.getElementById('__toast');
|
||||
const txt = document.getElementById('__toast-txt');
|
||||
if (!t || !txt) return;
|
||||
txt.innerHTML = text + (mono ? `<span class="mono">[ ${mono} ]</span>` : '');
|
||||
t.classList.add('show');
|
||||
clearTimeout(this._tt);
|
||||
this._tt = setTimeout(() => t.classList.remove('show'), 2400);
|
||||
},
|
||||
|
||||
/* ─── Body scroll lock (引用计数 · 多 overlay 叠加安全) ─── */
|
||||
_scrollLockCount: 0,
|
||||
_scrollLockSnapshot: null,
|
||||
lockScroll() {
|
||||
if (++this._scrollLockCount > 1) return;
|
||||
const docEl = document.documentElement;
|
||||
const sbw = window.innerWidth - docEl.clientWidth;
|
||||
this._scrollLockSnapshot = {
|
||||
bodyOverflow: document.body.style.overflow,
|
||||
bodyPaddingRight: document.body.style.paddingRight,
|
||||
htmlOverflow: docEl.style.overflow,
|
||||
};
|
||||
document.body.style.overflow = 'hidden';
|
||||
docEl.style.overflow = 'hidden';
|
||||
if (sbw > 0) document.body.style.paddingRight = sbw + 'px';
|
||||
},
|
||||
unlockScroll() {
|
||||
if (--this._scrollLockCount > 0) return;
|
||||
this._scrollLockCount = 0;
|
||||
const s = this._scrollLockSnapshot;
|
||||
if (s) {
|
||||
document.body.style.overflow = s.bodyOverflow;
|
||||
document.body.style.paddingRight = s.bodyPaddingRight;
|
||||
document.documentElement.style.overflow = s.htmlOverflow;
|
||||
this._scrollLockSnapshot = null;
|
||||
}
|
||||
},
|
||||
|
||||
openModal(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || el.classList.contains('show')) return;
|
||||
el.classList.add('show');
|
||||
this.lockScroll();
|
||||
},
|
||||
closeModal(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || !el.classList.contains('show')) return;
|
||||
el.classList.remove('show');
|
||||
this.unlockScroll();
|
||||
},
|
||||
openDrawer(id) {
|
||||
const el = document.getElementById(id);
|
||||
const bg = document.getElementById(id + '-bg');
|
||||
if (!el || el.classList.contains('show')) return;
|
||||
el.classList.add('show');
|
||||
if (bg) bg.classList.add('show');
|
||||
this.lockScroll();
|
||||
},
|
||||
closeDrawer(id) {
|
||||
const el = document.getElementById(id);
|
||||
const bg = document.getElementById(id + '-bg');
|
||||
if (!el || !el.classList.contains('show')) return;
|
||||
el.classList.remove('show');
|
||||
if (bg) bg.classList.remove('show');
|
||||
this.unlockScroll();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user