feat(workbench): 三工序图片区视觉对齐 + 任务中心聚合 + 工具台头部筛选
- model-photo / platform-cover · 头部 toolbar 落地: 时间 / 模特(平台) chip 下拉 + 折叠搜索 - model-photo / platform-cover · 图片卡片样式同步图片创作 (.io-cell): bg / hover / .gen 脉冲 / .err 红框 - model-photo / platform-cover · 单图 hover overlay: 再次生成 + 下载 + 更多(加入资产库/删除) - model-photo / platform-cover · 批次底栏: 再次生成图标统一 + 更多 menu(全部加入资产库/删除该批) - model-photo · 修 TDZ bug: renderModelMini 调用挪到 MODELS 声明后, 解决整页崩溃 - model-photo · 去掉冗余 pv-summary, 商品自动选最近编辑, task 写入 name 字段 - image-optimize · 单图右上加再次生成图标, 加入 fs-image-tasks-image 与任务中心打通 - image-optimize · 输入区拆 3 行: + 在顶 / textarea 满宽 / 发送在底栏右; 参考图缩略与加号同 64×64 - asset-factory · 任务中心加时间 chip + image 类型 + 跳转表; 删冗余类型列 - pipeline · stage2 商品卡换商品库风格 + AI 生成三视图主 CTA + .tri-missing-badge[hidden] CSS 修复
This commit is contained in:
+415
-42
@@ -21,7 +21,7 @@
|
||||
.product-pick-row .product-pick { flex: 0 0 240px; min-width: 240px; }
|
||||
.product-pick-row .product-pick.lib { flex: 0 0 152px; min-width: 152px; min-height: 96px; }
|
||||
/* 底部开始 CTA */
|
||||
.wiz-start-bar { display: flex; justify-content: center; padding: 20px 0 8px; }
|
||||
.wiz-start-bar { display: flex; justify-content: flex-end; padding: 20px 0 8px; }
|
||||
.wiz-start-bar .btn-start { height: 44px; padding: 0 36px; background: var(--heat); color: var(--accent-white); border: 1px solid var(--heat); border-radius: 999px; font-size: 14px; font-weight: 600; cursor: pointer; box-shadow: var(--shadow-cta); display: inline-flex; align-items: center; gap: 8px; font-family: inherit; transition: box-shadow var(--t-base), opacity var(--t-base); }
|
||||
.wiz-start-bar .btn-start:hover:not(.disabled) { box-shadow: var(--shadow-cta-hover); }
|
||||
.wiz-start-bar .btn-start.disabled { opacity: .4; cursor: not-allowed; }
|
||||
@@ -121,6 +121,234 @@
|
||||
.pick-section-h { display: flex; align-items: baseline; gap: 8px; margin: 14px 0 8px; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .08em; text-transform: uppercase; }
|
||||
.pick-section-h .count { background: var(--background-lighter); border: 1px solid var(--border-faint); padding: 1px 6px; color: var(--black-alpha-48); font-size: 10px; }
|
||||
|
||||
/* ============================================================
|
||||
第 1 步 · 商品选择器(沿用 products.html 商品库的卡片与 toolbar 视觉)
|
||||
─ 命名空间 .pp- 前缀,避免与 products.html 冲突
|
||||
============================================================ */
|
||||
.pp-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.pp-toolbar .search-inline {
|
||||
flex: 1; min-width: 220px; max-width: 340px;
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
height: 34px; padding: 0 12px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
transition: border-color var(--t-base);
|
||||
}
|
||||
.pp-toolbar .search-inline:focus-within { border-color: var(--heat-40); }
|
||||
.pp-toolbar .search-inline svg { width: 14px; height: 14px; color: var(--black-alpha-48); flex-shrink: 0; }
|
||||
.pp-toolbar .search-inline input { flex: 1; min-width: 0; height: 100%; border: 0; outline: 0; background: transparent; font-size: 13px; color: var(--accent-black); font-family: inherit; }
|
||||
.pp-toolbar .pp-chip-wrap { position: relative; }
|
||||
.pp-toolbar .pp-chip {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
height: 34px; padding: 0 12px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
font-size: 13px; font-family: inherit;
|
||||
color: var(--black-alpha-72);
|
||||
cursor: pointer;
|
||||
transition: border-color var(--t-base), color var(--t-base);
|
||||
}
|
||||
.pp-toolbar .pp-chip:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
|
||||
.pp-toolbar .pp-chip.active { background: var(--heat-12); color: var(--heat); border-color: var(--heat-20); }
|
||||
.pp-toolbar .pp-chip svg { width: 11px; height: 11px; opacity: .6; }
|
||||
.pp-toolbar .pp-menu {
|
||||
position: absolute; top: calc(100% + 4px); left: 0;
|
||||
min-width: 160px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,.06);
|
||||
padding: 4px;
|
||||
display: none;
|
||||
z-index: 20;
|
||||
}
|
||||
.pp-toolbar .pp-chip-wrap.open .pp-menu { display: block; }
|
||||
.pp-toolbar .pp-menu .mi {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 7px 10px;
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 12.5px;
|
||||
color: var(--accent-black);
|
||||
cursor: pointer;
|
||||
}
|
||||
.pp-toolbar .pp-menu .mi:hover { background: var(--background-lighter); }
|
||||
.pp-toolbar .pp-menu .mi.selected { color: var(--heat); font-weight: 600; }
|
||||
.pp-toolbar .pp-menu .mi-check { width: 12px; height: 12px; opacity: 0; }
|
||||
.pp-toolbar .pp-menu .mi.selected .mi-check { opacity: 1; }
|
||||
.pp-toolbar .pp-clear {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
height: 30px; padding: 0 10px;
|
||||
background: transparent; border: 0; border-radius: var(--r-sm);
|
||||
color: var(--black-alpha-56); font-size: 12.5px; font-family: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pp-toolbar .pp-clear:hover { color: var(--accent-crimson); background: var(--crimson-bg, #fdebea); }
|
||||
.pp-toolbar .pp-clear svg { width: 11px; height: 11px; }
|
||||
.pp-toolbar .spacer { flex: 1; }
|
||||
.pp-view-tog { display: inline-flex; border: 1px solid var(--border-faint); border-radius: var(--r-md); overflow: hidden; }
|
||||
.pp-view-tog button {
|
||||
width: 34px; height: 34px;
|
||||
background: var(--surface);
|
||||
border: 0; border-right: 1px solid var(--border-faint);
|
||||
cursor: pointer;
|
||||
color: var(--black-alpha-48);
|
||||
display: grid; place-items: center;
|
||||
transition: background var(--t-base), color var(--t-base);
|
||||
}
|
||||
.pp-view-tog button:last-child { border-right: 0; }
|
||||
.pp-view-tog button:hover { background: var(--background-lighter); color: var(--accent-black); }
|
||||
.pp-view-tog button.active { background: var(--heat-12); color: var(--heat); }
|
||||
.pp-view-tog button svg { width: 14px; height: 14px; }
|
||||
|
||||
.pp-result-meta {
|
||||
font-family: var(--font-mono); font-size: 11.5px;
|
||||
color: var(--black-alpha-48); letter-spacing: .02em;
|
||||
margin: 4px 0 12px;
|
||||
}
|
||||
|
||||
/* 网格 — 沿用 products.html 商品库卡片样式(复制必要部分)
|
||||
固定 4 列 → 每页 8 tile(createCard + 7 商品)正好两行 */
|
||||
.pp-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
|
||||
@media (max-width: 1100px) { .pp-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
@media (max-width: 800px) { .pp-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
.pp-grid .product-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
cursor: pointer; position: relative; overflow: hidden;
|
||||
display: flex; flex-direction: column;
|
||||
transition: background .15s, border-color .15s, transform .15s;
|
||||
}
|
||||
.pp-grid .product-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
|
||||
.pp-grid .product-card.selected { border-color: var(--heat); background: var(--heat-12); }
|
||||
.pp-grid .product-card.selected::after {
|
||||
content: ''; position: absolute; top: 0; right: 0;
|
||||
width: 0; height: 0;
|
||||
border-top: 28px solid var(--heat);
|
||||
border-left: 28px solid transparent;
|
||||
z-index: 2;
|
||||
}
|
||||
.pp-grid .product-card.selected::before {
|
||||
content: ''; position: absolute; top: 4px; right: 4px;
|
||||
width: 10px; height: 10px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23ffffff' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 8 7 12 13 4'/%3E%3C/svg%3E") no-repeat center / contain;
|
||||
z-index: 3;
|
||||
}
|
||||
.pp-grid .product-thumb { aspect-ratio: 1.4 / 1; }
|
||||
.pp-grid .product-body { padding: 14px 14px 12px; flex: 1; }
|
||||
.pp-grid .product-name {
|
||||
font-size: 14px; font-weight: 600; color: var(--accent-black);
|
||||
line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.pp-grid .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;
|
||||
}
|
||||
.pp-grid .product-date {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px; color: var(--black-alpha-48);
|
||||
margin-top: 10px; letter-spacing: .02em;
|
||||
}
|
||||
.pp-grid .product-card.selected .product-cat { background: var(--surface); color: var(--heat); }
|
||||
|
||||
/* 创建新商品 空卡 */
|
||||
.pp-grid .pp-create-card {
|
||||
border: 1.5px dashed var(--black-alpha-24);
|
||||
border-radius: var(--r-md);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 10px; min-height: 220px;
|
||||
color: var(--black-alpha-48);
|
||||
transition: border-color var(--t-base), color var(--t-base), background var(--t-base);
|
||||
}
|
||||
.pp-grid .pp-create-card:hover { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
|
||||
.pp-grid .pp-create-card .pc-plus {
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 50%;
|
||||
background: var(--heat); color: var(--accent-white);
|
||||
display: grid; place-items: center;
|
||||
transition: filter var(--t-base);
|
||||
}
|
||||
.pp-grid .pp-create-card:hover .pc-plus { filter: brightness(1.06); }
|
||||
.pp-grid .pp-create-card .pc-plus svg { width: 18px; height: 18px; }
|
||||
.pp-grid .pp-create-card .pc-t { font-size: 13px; font-weight: 600; }
|
||||
.pp-grid .pp-create-card .pc-d { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .02em; }
|
||||
|
||||
/* 列表视图 */
|
||||
.pp-grid.list-view { display: flex; flex-direction: column; gap: 6px; }
|
||||
.pp-grid.list-view .product-card {
|
||||
flex-direction: row; align-items: center;
|
||||
}
|
||||
.pp-grid.list-view .product-thumb { width: 96px; aspect-ratio: 1.4 / 1; flex-shrink: 0; }
|
||||
.pp-grid.list-view .product-body { flex: 1; padding: 10px 14px; }
|
||||
.pp-grid.list-view .pp-create-card { flex-direction: row; min-height: 56px; gap: 12px; }
|
||||
.pp-grid.list-view .pp-create-card .pc-plus { width: 32px; height: 32px; }
|
||||
|
||||
/* 空筛选结果 */
|
||||
.pp-empty {
|
||||
grid-column: 1 / -1;
|
||||
padding: 48px 24px; text-align: center;
|
||||
border: 1px dashed var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--background-lighter);
|
||||
color: var(--black-alpha-48);
|
||||
font-size: 12.5px; font-family: var(--font-mono); letter-spacing: .02em;
|
||||
}
|
||||
.pp-empty .reset { display: inline-block; margin-top: 8px; color: var(--heat); cursor: pointer; text-decoration: underline; }
|
||||
|
||||
/* 分页 */
|
||||
.pp-pager {
|
||||
display: flex; align-items: center; gap: 16px;
|
||||
margin-top: 18px; padding-top: 14px;
|
||||
border-top: 1px solid var(--border-faint);
|
||||
font-size: 12.5px; color: var(--black-alpha-56);
|
||||
}
|
||||
.pp-pager .total { font-family: var(--font-mono); letter-spacing: .02em; }
|
||||
.pp-pager .pages { display: inline-flex; gap: 4px; margin-left: auto; }
|
||||
.pp-pager .pages button {
|
||||
min-width: 28px; height: 28px; 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: border-color var(--t-base), background var(--t-base), color var(--t-base);
|
||||
}
|
||||
.pp-pager .pages button:hover:not(.active):not(:disabled) { border-color: var(--black-alpha-32); color: var(--accent-black); }
|
||||
.pp-pager .pages button.active { background: var(--heat); color: var(--accent-white); border-color: var(--heat); font-weight: 600; }
|
||||
.pp-pager .pages button:disabled { opacity: .4; cursor: not-allowed; }
|
||||
.pp-pager .pages .ellipsis {
|
||||
min-width: 22px; height: 28px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
color: var(--black-alpha-48); font-family: var(--font-mono);
|
||||
}
|
||||
.pp-pager .page-size {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
height: 28px; 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);
|
||||
}
|
||||
.pp-pager .page-size:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
|
||||
.pp-pager .page-size svg { width: 10px; height: 10px; opacity: .6; }
|
||||
|
||||
/* 底部提示 */
|
||||
.pp-bottom-tip {
|
||||
margin-top: 14px;
|
||||
padding: 10px 14px;
|
||||
background: var(--background-lighter);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
font-size: 12.5px; color: var(--black-alpha-56);
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.pp-bottom-tip svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--black-alpha-48); }
|
||||
.pp-bottom-tip a { color: var(--heat); cursor: pointer; text-decoration: none; }
|
||||
.pp-bottom-tip a:hover { text-decoration: underline; }
|
||||
|
||||
/* ── Step 1 · product picker grid ── */
|
||||
.product-pick-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
||||
@media (max-width: 1100px) { .product-pick-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@@ -391,13 +619,13 @@
|
||||
/* ---------- data ---------- */
|
||||
|
||||
const PRODUCTS = [
|
||||
{ id: 'mask', name: '透真玻尿酸补水面膜', cat: '美妆个护', price: 39.9, imgs: 3, points: ['透明质酸 + B5', '30g 大容量精华', '0 香精 0 酒精'], tags: ['熬夜党', '敏感肌'] },
|
||||
{ id: 'earphone', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', price: 199, imgs: 5, points: ['主动降噪', '32 小时续航', 'IP55 防水'], tags: ['通勤', '运动'] },
|
||||
{ id: 'noodle', name: '滋啦速食牛肉面 · 6 桶装', cat: '食品饮料', price: 49.9, imgs: 4, points: ['3 分钟出餐', '真材实料牛肉', '0 防腐剂'], tags: ['加班', '独居'] },
|
||||
{ id: 'sun', name: '透真清透物理防晒霜', cat: '美妆个护', price: 69, imgs: 4, points: ['SPF50 PA+++', '纯物理防晒', '不泛白不假面'], tags: ['SPF50', '通勤'] },
|
||||
{ id: 'coffee', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', price: 89, imgs: 6, points: ['冷热水秒溶', '意式深烘', '24 颗轻便装'], tags: ['提神', '早八'] },
|
||||
{ id: 'fryer', name: '小熊 4L 可视空气炸锅', cat: '家居家电', price: 159, imgs: 5, points: ['可视化窗口', '4L 大容量', '低脂少油'], tags: ['小户型', '健康'] },
|
||||
{ id: 'yoga', name: '露露同款裸感瑜伽裤', cat: '运动户外', price: 119, imgs: 8, points: ['裸感面料', '高弹回弹', '随心动随心穿'], tags: ['健身房', '通勤'] },
|
||||
{ id: 'mask', name: '透真玻尿酸补水面膜', cat: '美妆个护', price: 39.9, imgs: 3, points: ['透明质酸 + B5', '30g 大容量精华', '0 香精 0 酒精'], tags: ['熬夜党', '敏感肌'], date: '2026-05-15' },
|
||||
{ id: 'earphone', name: '南卡 Lite Pro 蓝牙耳机', cat: '数码 3C', price: 199, imgs: 5, points: ['主动降噪', '32 小时续航', 'IP55 防水'], tags: ['通勤', '运动'], date: '2026-05-12' },
|
||||
{ id: 'noodle', name: '滋啦速食牛肉面 · 6 桶装', cat: '食品饮料', price: 49.9, imgs: 4, points: ['3 分钟出餐', '真材实料牛肉', '0 防腐剂'], tags: ['加班', '独居'], date: '2026-05-10' },
|
||||
{ id: 'sun', name: '透真清透物理防晒霜', cat: '美妆个护', price: 69, imgs: 4, points: ['SPF50 PA+++', '纯物理防晒', '不泛白不假面'], tags: ['SPF50', '通勤'], date: '2026-05-08' },
|
||||
{ id: 'coffee', name: '三顿半同款冻干咖啡粉', cat: '食品饮料', price: 89, imgs: 6, points: ['冷热水秒溶', '意式深烘', '24 颗轻便装'], tags: ['提神', '早八'], date: '2026-05-05' },
|
||||
{ id: 'fryer', name: '小熊 4L 可视空气炸锅', cat: '家居家电', price: 159, imgs: 5, points: ['可视化窗口', '4L 大容量', '低脂少油'], tags: ['小户型', '健康'], date: '2026-05-03' },
|
||||
{ id: 'yoga', name: '露露同款裸感瑜伽裤', cat: '运动户外', price: 119, imgs: 8, points: ['裸感面料', '高弹回弹', '随心动随心穿'], tags: ['健身房', '通勤'], date: '2026-04-30' },
|
||||
];
|
||||
|
||||
const RECENT_IDS = ['mask', 'sun', 'coffee', 'earphone'];
|
||||
@@ -434,6 +662,37 @@
|
||||
{ id: 'genz', name: '学生党', sub: 'Z 世代 18-24', metric: '平价快消', defaults: { duration: '0-10', style: 'compare' } },
|
||||
];
|
||||
|
||||
/* ---------- 合并其它页面创建的商品 ---------- */
|
||||
// 商品库 / 工作台 / 新建项目 都共用同一 drawer(assets/new-product-drawer.js),
|
||||
// 该 drawer 在 save() 时把新商品写入 sessionStorage['fs-extra-products']。
|
||||
// 这里在 PRODUCTS hardcoded 数据后,把 storage 中尚不在 PRODUCTS 的商品 unshift
|
||||
// 到列表头部 → 用户跨页面新建的商品在 step 1 也能立即看到。
|
||||
(function mergeExtraProducts() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('fs-extra-products');
|
||||
if (!raw) return;
|
||||
const list = JSON.parse(raw);
|
||||
if (!Array.isArray(list) || !list.length) return;
|
||||
const existingIds = new Set(PRODUCTS.map(p => p.id));
|
||||
// 按 createdAt 倒序(最新在前),逐个 unshift
|
||||
list.slice().sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)).forEach(x => {
|
||||
if (existingIds.has(x.id)) return;
|
||||
existingIds.add(x.id);
|
||||
PRODUCTS.unshift({
|
||||
id: x.id,
|
||||
name: x.name || '未命名商品',
|
||||
cat: x.cat || '未分类',
|
||||
price: null, // 表单未采集价格
|
||||
imgs: Math.max(1, x.assets || 0), // 用素材数兜底,至少 1
|
||||
points: Array.isArray(x.bullets) ? x.bullets : [],
|
||||
tags: x.target ? String(x.target).split(/[ ,、、]+/).filter(Boolean).slice(0, 3) : [],
|
||||
date: x.date || (x.createdAt ? new Date(x.createdAt).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10)),
|
||||
createdAt: x.createdAt || Date.now(),
|
||||
});
|
||||
});
|
||||
} catch (e) { /* storage 不可用就静默放弃 */ }
|
||||
})();
|
||||
|
||||
const USER_EMAIL = 'li@shop.com';
|
||||
const ACCOUNT_BALANCE = 327.40;
|
||||
|
||||
@@ -444,6 +703,10 @@
|
||||
productId: null,
|
||||
pickSearch: '',
|
||||
pickCat: '全部',
|
||||
pickSort: 'recent', // recent | name | added
|
||||
pickView: 'grid', // grid | list
|
||||
pickPage: 1,
|
||||
pickPageSize: 7, // 固定 4 列 × 2 行 = 8 tile,首页含 createCard 占 1 位 → 7 商品
|
||||
sourceId: null,
|
||||
themeText: '',
|
||||
manualScript: '',
|
||||
@@ -523,7 +786,7 @@
|
||||
state.projectName = p.name.split(' ')[0] + ' · 痛点种草 · v1';
|
||||
}
|
||||
state.points = {};
|
||||
p.points.forEach((pt, i) => { state.points[pt] = i < 2; });
|
||||
p.points.forEach(pt => { state.points[pt] = false; });
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -598,6 +861,7 @@
|
||||
onSave: function (p) {
|
||||
// p = { id, name, cat, target, points: string[], images: [...], imgs: N }
|
||||
// 适配 wizard 数据结构
|
||||
const now = Date.now();
|
||||
const product = {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
@@ -606,6 +870,8 @@
|
||||
imgs: p.imgs,
|
||||
points: p.points,
|
||||
tags: p.target ? p.target.split(/[ ,、、]+/).filter(Boolean).slice(0, 3) : [],
|
||||
date: new Date(now).toISOString().slice(0, 10),
|
||||
createdAt: now,
|
||||
};
|
||||
// 置顶插入,让用户立刻看到
|
||||
PRODUCTS.unshift(product);
|
||||
@@ -683,8 +949,13 @@
|
||||
window._wiz = {
|
||||
selectProduct, selectSource, goNext, goPrev, jumpTo, applyPreset, startGenerate, openNewProduct,
|
||||
openProdLib, closeProdLib,
|
||||
setSearch: v => { state.pickSearch = v; renderStep1Only(); },
|
||||
setCat: v => { state.pickCat = v; renderStep1Only(); },
|
||||
setSearch: v => { state.pickSearch = v; state.pickPage = 1; renderStep1Only(); },
|
||||
setCat: v => { state.pickCat = v; state.pickPage = 1; renderStep1Only(); },
|
||||
setSort: v => { state.pickSort = v; state.pickPage = 1; renderStep1Only(); },
|
||||
setView: v => { state.pickView = v; renderStep1Only(); },
|
||||
setPage: v => { state.pickPage = Math.max(1, v); renderStep1Only(); },
|
||||
setPageSize: v => { state.pickPageSize = v; state.pickPage = 1; renderStep1Only(); },
|
||||
clearPickFilters: () => { state.pickSearch=''; state.pickCat='全部'; state.pickPage=1; renderStep1Only(); },
|
||||
setTheme: v => { state.themeText = v; updateFootOnly(); updatePreviewLive(); },
|
||||
setScript: v => { state.manualScript = v; updateFootOnly(); },
|
||||
setName: v => { state.projectName = v; updatePreviewLive(); updateFootOnly(); updateRailOnly(); },
|
||||
@@ -749,24 +1020,108 @@
|
||||
$('#rail').innerHTML = html;
|
||||
}
|
||||
|
||||
function productPickHTML(p) {
|
||||
function productCardHTML(p) {
|
||||
const selected = state.productId === p.id;
|
||||
return `<div class="product-pick${selected ? ' selected' : ''}" onclick="_wiz.selectProduct('${p.id}')">
|
||||
<div class="placeholder thumb"><span class="ph-frame">9:16</span></div>
|
||||
<div class="body">
|
||||
<div class="name">${esc(p.name)}</div>
|
||||
<div class="meta">${esc(p.cat)}${p.price != null ? ' · <b>¥' + p.price + '</b>' : ''} · ${p.imgs} 张图</div>
|
||||
<div class="tags">${p.tags.map(t => `<span class="tag-s">${esc(t)}</span>`).join('')}</div>
|
||||
return `<div class="product-card${selected ? ' selected' : ''}" onclick="_wiz.selectProduct('${p.id}')">
|
||||
<div class="placeholder product-thumb"><span class="ph-frame">${esc(p.name)} · 1200×800</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>`;
|
||||
}
|
||||
|
||||
function pickerFilteredProducts() {
|
||||
const q = (state.pickSearch || '').trim().toLowerCase();
|
||||
let list = PRODUCTS.filter(p => {
|
||||
if (state.pickCat !== '全部' && p.cat !== state.pickCat) return false;
|
||||
if (q) {
|
||||
const blob = (p.name + ' ' + p.cat + ' ' + (p.tags || []).join(' ')).toLowerCase();
|
||||
if (!blob.includes(q)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (state.pickSort === 'name') {
|
||||
list = list.slice().sort((a, b) => a.name.localeCompare(b.name, 'zh'));
|
||||
} else if (state.pickSort === 'added') {
|
||||
list = list.slice();
|
||||
} else {
|
||||
// recent 排序优先级:
|
||||
// 1) 用户新建的商品(有 createdAt) — 按 createdAt 倒序,最新在最前(紧挨 createCard 按钮)
|
||||
// 2) RECENT_IDS 命中的商品 — 按命中顺序
|
||||
// 3) 其余 — 保持原始顺序
|
||||
const recent = new Map(RECENT_IDS.map((id, i) => [id, i]));
|
||||
function weight(p) {
|
||||
if (p.createdAt) return -p.createdAt; // 负值 → 最大的负数(最近创建)排最前
|
||||
if (recent.has(p.id)) return recent.get(p.id);
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
list = list.slice().sort((a, b) => weight(a) - weight(b));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
function pickerPagerHTML(total) {
|
||||
const pageSize = state.pickPageSize;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const cur = Math.min(state.pickPage, totalPages);
|
||||
function pageBtn(n) {
|
||||
const act = n === cur ? ' active' : '';
|
||||
return `<button type="button" class="${act.trim()}" onclick="_wiz.setPage(${n})">${n}</button>`;
|
||||
}
|
||||
const items = [];
|
||||
items.push(`<button type="button"${cur === 1 ? ' disabled' : ''} onclick="_wiz.setPage(${cur - 1})">‹</button>`);
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) items.push(pageBtn(i));
|
||||
} else {
|
||||
items.push(pageBtn(1));
|
||||
if (cur > 3) items.push('<span class="ellipsis">…</span>');
|
||||
const lo = Math.max(2, cur - 1), hi = Math.min(totalPages - 1, cur + 1);
|
||||
for (let i = lo; i <= hi; i++) items.push(pageBtn(i));
|
||||
if (cur < totalPages - 2) items.push('<span class="ellipsis">…</span>');
|
||||
items.push(pageBtn(totalPages));
|
||||
}
|
||||
items.push(`<button type="button"${cur === totalPages ? ' disabled' : ''} onclick="_wiz.setPage(${cur + 1})">›</button>`);
|
||||
return `<div class="pp-pager">
|
||||
<span class="total">共 ${total} 条</span>
|
||||
<div class="pages">${items.join('')}</div>
|
||||
<span class="page-size">每页 ${pageSize} 条</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderStep1() {
|
||||
// 已选 + 最近使用,合并去重
|
||||
const ids = new Set();
|
||||
if (state.productId) ids.add(state.productId);
|
||||
RECENT_IDS.forEach(id => ids.add(id));
|
||||
const recent = [...ids].map(id => PRODUCTS.find(p => p.id === id)).filter(Boolean);
|
||||
const list = pickerFilteredProducts();
|
||||
const total = list.length;
|
||||
const pageSize = state.pickPageSize;
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const cur = Math.min(state.pickPage, totalPages);
|
||||
const pageList = list.slice((cur - 1) * pageSize, cur * pageSize);
|
||||
|
||||
const cats = ['全部', ...new Set(PRODUCTS.map(p => p.cat))];
|
||||
const catChipActive = state.pickCat !== '全部';
|
||||
const sortLabels = { recent: '最近使用', name: '商品名称', added: '添加顺序' };
|
||||
const hasFilter = !!state.pickSearch || state.pickCat !== '全部';
|
||||
|
||||
const catMenu = cats.map(c => `<div class="mi${state.pickCat === c ? ' selected' : ''}" onclick="_wiz.setCat('${esc(c)}')">
|
||||
<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6"><polyline points="3 8 7 12 13 4"/></svg>
|
||||
<span>${esc(c)}</span>
|
||||
</div>`).join('');
|
||||
|
||||
const sortMenu = Object.keys(sortLabels).map(k => `<div class="mi${state.pickSort === k ? ' selected' : ''}" onclick="_wiz.setSort('${k}')">
|
||||
<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.6"><polyline points="3 8 7 12 13 4"/></svg>
|
||||
<span>${sortLabels[k]}</span>
|
||||
</div>`).join('');
|
||||
|
||||
const cards = pageList.map(productCardHTML).join('');
|
||||
const createCard = `<div class="pp-create-card" onclick="_wiz.openNewProduct()">
|
||||
<div class="pc-plus"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg></div>
|
||||
<div class="pc-t">创建新商品</div>
|
||||
<div class="pc-d">// 在此添加一个新商品</div>
|
||||
</div>`;
|
||||
const gridContent = total === 0
|
||||
? (createCard + `<div class="pp-empty">// NO MATCH<br>没有符合筛选条件的商品 <span class="reset" onclick="_wiz.clearPickFilters()">[ 清空筛选 ]</span></div>`)
|
||||
: (createCard + cards);
|
||||
|
||||
return `<div class="wiz-pane active" data-step="1">
|
||||
<div class="wiz-step-h">
|
||||
@@ -774,23 +1129,33 @@
|
||||
<p>从商品库选一个 SKU。它的主图与卖点会被 LLM 作为脚本/资产生成的素材。</p>
|
||||
</div>
|
||||
|
||||
<div class="pick-actions">
|
||||
<button class="cap-pill" type="button" onclick="_wiz.openNewProduct()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
添加商品
|
||||
</button>
|
||||
<button class="cap-pill" type="button" onclick="_wiz.openProdLib()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M9 4v16"/></svg>
|
||||
去商品库 <span style="font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .02em; margin-left: 2px;">// 共 ${PRODUCTS.length} 个</span>
|
||||
</button>
|
||||
<div class="pp-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 type="text" placeholder="搜索商品名称、标签" value="${esc(state.pickSearch)}" oninput="_wiz.setSearch(this.value)">
|
||||
</div>
|
||||
<div class="pp-chip-wrap" data-key="cat">
|
||||
<button class="pp-chip${catChipActive ? ' active' : ''}" type="button" onclick="event.stopPropagation(); this.parentElement.classList.toggle('open'); document.querySelectorAll('.pp-chip-wrap.open').forEach(w=>{if(w!==this.parentElement)w.classList.remove('open')})">
|
||||
<span>${state.pickCat === '全部' ? '全部分类' : esc(state.pickCat)}</span>
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="pp-menu">${catMenu}</div>
|
||||
</div>
|
||||
${hasFilter ? `<button class="pp-clear" type="button" onclick="_wiz.clearPickFilters()">
|
||||
<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="pick-section-h">
|
||||
<span>最近使用</span>
|
||||
<span class="count">${recent.length}</span>
|
||||
</div>
|
||||
<div class="product-pick-row">
|
||||
${recent.map(productPickHTML).join('')}
|
||||
<div class="pp-result-meta">// 显示 ${pageList.length} / ${total} 个商品${hasFilter ? ' (已筛选)' : ''}</div>
|
||||
|
||||
<div class="pp-grid${state.pickView === 'list' ? ' list-view' : ''}">${gridContent}</div>
|
||||
|
||||
${total > pageSize ? pickerPagerHTML(total) : ''}
|
||||
|
||||
<div class="pp-bottom-tip">
|
||||
<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="M12 8v5M12 16h.01"/></svg>
|
||||
<span>找不到想要的商品?可<a onclick="_wiz.openNewProduct()">创建新商品</a>,或前往 <a href="products.html">商品库 · 管理商品</a></span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -1170,12 +1535,13 @@
|
||||
tmp.innerHTML = renderStep1();
|
||||
active.replaceWith(tmp.firstElementChild);
|
||||
}
|
||||
// refocus search input
|
||||
const inp = body.querySelector('.search-input input');
|
||||
if (inp && document.activeElement !== inp) {
|
||||
// refocus search input (旧 .search-input → 新 .pp-toolbar .search-inline)
|
||||
const inp = body.querySelector('.pp-toolbar .search-inline input')
|
||||
|| body.querySelector('.search-input input');
|
||||
if (inp && state.pickSearch && document.activeElement !== inp) {
|
||||
inp.focus();
|
||||
const v = inp.value;
|
||||
inp.setSelectionRange(v.length, v.length);
|
||||
try { inp.setSelectionRange(v.length, v.length); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1223,6 +1589,13 @@
|
||||
openNewProduct();
|
||||
});
|
||||
|
||||
// 全局点击 → 关闭 picker chip 菜单
|
||||
document.addEventListener('click', e => {
|
||||
if (!e.target.closest('.pp-chip-wrap')) {
|
||||
document.querySelectorAll('.pp-chip-wrap.open').forEach(w => w.classList.remove('open'));
|
||||
}
|
||||
});
|
||||
|
||||
// initial render
|
||||
render();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user