/* ============================================================ Airshelf · 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 ShellIcon = (name, opts) => window.IconKit ? window.IconKit.svg(name, opts) : ''; const NAV = [ { id: 'dashboard', label: '工作台', href: 'index.html', icon: ShellIcon('home') }, { id: 'products', label: '商品库', href: 'products.html', badge: '7', icon: ShellIcon('package') }, { id: 'projects', label: '视频项目', href: 'projects.html', badge: '8', icon: ShellIcon('clapperboard') }, { id: 'asset-factory', label: '图片生成', href: 'asset-factory.html', icon: ShellIcon('sparkles') }, { id: 'library', label: '资产库', href: 'library.html', icon: ShellIcon('images') }, { id: 'team', label: '团队', href: 'team.html', icon: ShellIcon('users') }, { id: 'account', label: '消费', href: 'account.html', icon: ShellIcon('creditCard') }, { id: 'settings', label: '设置', href: 'settings.html', icon: ShellIcon('settings') } ]; window.Shell = { render({ active = '', crumbs = [], balance = '¥327.40', topActions = '' } = {}) { const navHtml = NAV.map(n => ` ${n.icon} ${n.label} ${n.badge ? `${n.badge}` : ''} `).join(''); const sidebar = ` `; const crumbHtml = crumbs.length ? `
${crumbs.map((c, i) => { const last = i === crumbs.length - 1; const sep = i > 0 ? '/' : ''; if (last) return `${sep}${c.label}`; return `${sep}${c.label}`; }).join('')}
` : ''; const topbar = `
${crumbHtml}
${ShellIcon('creditCard')} 余额 ${balance}
${topActions}
`; const decorations = `
`; const toastHtml = `
${ShellIcon('check')}
操作成功[ 200 OK ]
`; // ─── 全局 Lightbox · 任意页面可用 Shell._openLightbox(src, name) ── const lightboxHtml = `
`; // [DEPRECATED · 弹窗已废弃,创建商品直接进 product-create.html] const _deprecatedModalHtml = `
`; // 装订线 SVG 准星 · V2.1 签名元素(圆弧内凹的"+") const cornerSvg = ``; const cornerMarks = ` ${cornerSvg} ${cornerSvg} ${cornerSvg} ${cornerSvg} `; const app = document.createElement('div'); app.className = 'app'; app.innerHTML = sidebar + `
${decorations}${topbar}
${cornerMarks}
`; 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 = ''; 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 = `0${t.replace(/[<>&]/g, c => ({ '<':'<','>':'>','&':'&' })[c])}${xSvg}`; 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 = ''; const esc = (s) => s.replace(/[<>&"]/g, c => ({ '<':'<','>':'>','&':'&','"':'"' })[c]); const renderGrid = () => { const filledHtml = uploads.map(u => `
${esc(u.name)}
`).join(''); const emptyCount = MAX_UPLOADS - uploads.length; const emptyHtml = Array.from({ length: emptyCount }, () => `
无预览
`).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 ? `[ ${mono} ]` : ''); 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; // 把 modal-bg 挪到 body 直接子节点,绕开 .content 的层叠上下文(z-index:1)困住模态、让 topbar 漏出来的问题 if (el.parentNode !== document.body) document.body.appendChild(el); el.classList.add('show'); this.lockScroll(); if (!this._modalEsc) { this._modalEsc = (e) => { if (e.key !== 'Escape') return; const open = document.querySelector('.modal-bg.show'); if (open) this.closeModal(open.id); }; document.addEventListener('keydown', this._modalEsc); } }, 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(); } };