chore: 全量推送 · 累积页面改动 + Next.js 工程骨架 + v1/ 归档 + 文档
页面 (电商AI平台/) - account / team / settings / index / products / projects: 累积迭代 - restraint.css: 设计 token 补充 - login.html / register.html: 新增登录注册页 - _ARCHIVE.md: 归档说明 Next.js 工程骨架 - app/ + components/: 新一代 SPA 雏形 (page / layout / sidebar / topbar / GridBg / Icon) - package.json / package-lock.json / next.config.mjs / tsconfig.json / postcss.config.mjs / next-env.d.ts 历史归档 / 文档 - v1/: 原 V1 静态稿镜像 (含 mockup-A/B/C) - PRD.md / deployment-guide.md / _check.html - ui参考/ / screenshots/ 杂项 - .gitignore 调整 - 删除根 README.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+77
-95
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<title>商品库 · 流·Studio</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/restraint.css?v=202605211643">
|
||||
<link rel="stylesheet" href="assets/restraint.css?v=202605211800">
|
||||
<style>
|
||||
/* ─── 全局 viewport 高度链 (让右侧 toolbar/分页吸顶吸底) ─── */
|
||||
/* 整页滚动 · 头部 H1+actions sticky 固定 · 其他随页面滚 */
|
||||
@@ -1016,6 +1016,60 @@
|
||||
<script>
|
||||
Shell.render({ active: 'products', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '商品库' }] });
|
||||
|
||||
// ============== 注入用户新建的商品(来自工作台 / 商品库 drawer,写入 localStorage)==============
|
||||
// 必须在 const cards / TOTAL / CAT_COUNT 之前执行,让后续逻辑把它们当普通商品处理
|
||||
(function injectExtraProducts() {
|
||||
let pending;
|
||||
try {
|
||||
pending = JSON.parse(localStorage.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.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder product-thumb">
|
||||
<span class="tri-missing-badge" title="该商品尚未生成商品三视图,进入详情可在工作流第一步补齐"><span class="lbl-mono">缺三视图</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.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 17l-5-5-9 9"/></svg>
|
||||
素材 <b>${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.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
|
||||
视频 <b>${p.videos || 0}</b>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
grid.insertBefore(card, grid.firstChild);
|
||||
});
|
||||
})();
|
||||
|
||||
// ============== Products: 商品分类多选 + 搜索 + 创建时间筛选 ==============
|
||||
// state.cats: Set<string>,空集 = 全部
|
||||
const PAGE_SIZES = [12, 24, 48, 96];
|
||||
@@ -1675,9 +1729,8 @@ blInput.addEventListener('keydown', e => {
|
||||
}
|
||||
});
|
||||
|
||||
// 创建商品 (真正插入 grid)
|
||||
// 创建商品 · 持久化到 localStorage + 立即跳详情(中间不闪商品库)
|
||||
saveBtn.addEventListener('click', () => {
|
||||
// 防呆:每一步明确反馈,清晰看出在哪一步出问题
|
||||
const name = document.getElementById('pf-name').value.trim();
|
||||
const cat = document.getElementById('pf-cat').value;
|
||||
if (!name) {
|
||||
@@ -1689,110 +1742,39 @@ saveBtn.addEventListener('click', () => {
|
||||
Shell.toast('请上传商品主图', '至少 1 张 · 必填');
|
||||
return;
|
||||
}
|
||||
// 收集核心卖点:存在 .bl-item .bl-text 里(已确认的)+ .bl-add input(用户打了字但未回车)
|
||||
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 (pendingText) confirmedBullets.push(pendingText);
|
||||
if (confirmedBullets.length === 0) {
|
||||
Shell.toast('请填写核心卖点', '至少 1 条 · 回车确认');
|
||||
pendingInput?.focus();
|
||||
return;
|
||||
}
|
||||
const bullets = confirmedBullets;
|
||||
// 创建 card 并插入 grid 最前
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const card = document.createElement('div');
|
||||
card.className = 'product-card';
|
||||
card.dataset.cat = cat;
|
||||
card.dataset.name = name;
|
||||
card.dataset.tags = '';
|
||||
card.dataset.added = '0';
|
||||
card.dataset.assets = '0';
|
||||
card.dataset.videos = '0';
|
||||
card.dataset.date = today;
|
||||
card.setAttribute('onclick', "location.href='product-detail.html?t='+Date.now()");
|
||||
card.innerHTML = `
|
||||
<span class="card-check" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg></span>
|
||||
<button class="card-del-btn" type="button" title="删除商品" onclick="event.stopPropagation();" data-action="delete-product"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1.5 14a2 2 0 01-2 1.8H8.5a2 2 0 01-2-1.8L5 6"/></svg></button>
|
||||
<div class="placeholder product-thumb"><span class="ph-frame">${name} · 新建</span></div>
|
||||
<div class="product-body">
|
||||
<div class="product-name">${name}</div>
|
||||
<div class="product-cat">${cat}</div>
|
||||
<div class="product-date">${today} 创建</div>
|
||||
</div>
|
||||
<div class="product-footer">
|
||||
<span class="stat">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 17l-5-5-9 9"/></svg>
|
||||
素材 <b>0</b>
|
||||
</span>
|
||||
<span class="sep">·</span>
|
||||
<span class="stat">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
|
||||
视频 <b>0</b>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
const grid = document.getElementById('product-grid');
|
||||
grid.insertBefore(card, grid.firstChild);
|
||||
// 给新卡片注入交互:stat hover/click + del-btn 删除确认
|
||||
card.querySelectorAll('.product-footer .stat').forEach(stat => {
|
||||
const text = stat.textContent.trim();
|
||||
const m = text.match(/^(素材|视频)\s*(\d+)/);
|
||||
if (!m) return;
|
||||
const isAsset = m[1] === '素材';
|
||||
const svg = stat.querySelector('svg');
|
||||
stat.innerHTML = '';
|
||||
if (svg) stat.appendChild(svg);
|
||||
const dft = document.createElement('span');
|
||||
dft.className = 'stat-default';
|
||||
dft.innerHTML = `${m[1]} <b>${m[2]}</b>`;
|
||||
const hv = document.createElement('span');
|
||||
hv.className = 'stat-hover';
|
||||
hv.textContent = isAsset ? '+ 去生成素材' : '+ 去生成视频';
|
||||
stat.appendChild(dft); stat.appendChild(hv);
|
||||
stat.dataset.type = isAsset ? 'asset' : 'video';
|
||||
stat.title = isAsset ? '去生成 AI 素材' : '去生成视频项目';
|
||||
stat.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const n = card.dataset.name || '';
|
||||
if (isAsset) openGenAssetChoice(n);
|
||||
else location.href = 'projects-new.html?t=' + Date.now() + '&product=' + encodeURIComponent(n);
|
||||
// 持久化到 localStorage('fs-extra-products'),让 products.html 下次加载时
|
||||
// 自动从 storage 读出并 prepend 到 grid
|
||||
try {
|
||||
const KEY = 'fs-extra-products';
|
||||
const list = JSON.parse(localStorage.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(),
|
||||
});
|
||||
});
|
||||
const delBtn = card.querySelector('.card-del-btn');
|
||||
if (delBtn) {
|
||||
delBtn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
openDelConfirm([card]);
|
||||
});
|
||||
}
|
||||
// 同步 SKU 计数(标题 + 列表 meta + 侧栏 badge)
|
||||
const remaining = document.querySelectorAll('.product-card').length;
|
||||
const skuCount = document.getElementById('sku-count');
|
||||
if (skuCount) skuCount.textContent = remaining;
|
||||
const meta = document.querySelector('#result-meta .count');
|
||||
if (meta) meta.textContent = remaining;
|
||||
const sidebar = document.querySelector('aside.sidebar a[href="products.html"] .pill-mini');
|
||||
if (sidebar) sidebar.textContent = remaining;
|
||||
localStorage.setItem(KEY, JSON.stringify(list));
|
||||
} catch (e) { /* storage 不可用降级到只跳转 */ }
|
||||
|
||||
Shell.toast('商品已创建 · 即将跳转详情', `+ ${name} · ${bullets.length} 条卖点`);
|
||||
closeNewProductDrawer();
|
||||
// 重置表单(为下次新建准备)
|
||||
document.getElementById('pf-name').value = '';
|
||||
document.getElementById('pf-cat').value = cat;
|
||||
if (typeof pfFiles !== 'undefined') pfFiles.length = 0;
|
||||
const pfGrid = document.getElementById('pf-grid');
|
||||
if (pfGrid) pfGrid.innerHTML = '';
|
||||
const blList = document.getElementById('pf-bullets');
|
||||
if (blList) blList.querySelectorAll('.bl-item').forEach(li => li.remove());
|
||||
const pendingI = document.querySelector('#pf-bullets .bl-add .bl-input');
|
||||
if (pendingI) pendingI.value = '';
|
||||
// 1200ms 后跳转(让 toast 看清),商品库新建商品后跳转到该商品的详情页
|
||||
setTimeout(() => {
|
||||
location.href = 'product-detail.html?t=' + Date.now() + '&product=' + encodeURIComponent(name);
|
||||
}, 1200);
|
||||
// 不 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>
|
||||
|
||||
Reference in New Issue
Block a user