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:
iye
2026-05-21 21:16:46 +08:00
co-authored by Claude Opus 4.7
parent 8a783ca36f
commit f420af2069
82 changed files with 15367 additions and 418 deletions
+77 -95
View File
@@ -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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>