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:
+254
-3
@@ -176,6 +176,42 @@
|
||||
.bulk-bar button svg { width: 12px; height: 12px; }
|
||||
.bulk-bar .clear-sel { color: rgba(255,255,255,.6); font-size: 12px; cursor: pointer; background: none; border: 0; padding: 4px 6px; }
|
||||
.bulk-bar .clear-sel:hover { color: var(--accent-white); }
|
||||
/* 移动到 · 弹层菜单 (向上弹) */
|
||||
.bulk-bar .move-wrap { position: relative; display: inline-flex; }
|
||||
.bulk-bar .move-menu {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
right: 0;
|
||||
min-width: 160px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-faint);
|
||||
border-radius: var(--r-md);
|
||||
padding: 6px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.18);
|
||||
display: none;
|
||||
z-index: 2;
|
||||
}
|
||||
.bulk-bar .move-menu.show { display: block; }
|
||||
.bulk-bar .move-menu .mv-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
width: 100%; height: 32px; padding: 0 10px;
|
||||
background: transparent; border: 0;
|
||||
color: var(--accent-black);
|
||||
font-size: 13px; font-family: inherit;
|
||||
cursor: pointer; border-radius: var(--r-sm);
|
||||
text-align: left;
|
||||
}
|
||||
.bulk-bar .move-menu .mv-item:hover { background: var(--heat-12); color: var(--heat); }
|
||||
.bulk-bar .move-menu .mv-item svg { width: 12px; height: 12px; opacity: .7; }
|
||||
/* tab 作为拖拽目标 hover 态 */
|
||||
.tabs .tab.drag-over {
|
||||
background: var(--heat-12);
|
||||
color: var(--heat);
|
||||
border-radius: var(--r-sm);
|
||||
box-shadow: inset 0 0 0 1px var(--heat-40);
|
||||
}
|
||||
body.edit-mode .asset-card { cursor: grab; }
|
||||
body.edit-mode .asset-card.dragging { opacity: .4; }
|
||||
.asset-thumb { aspect-ratio: 1; }
|
||||
.asset-card.video .asset-thumb { aspect-ratio: 9/16; max-height: 280px; }
|
||||
.asset-body { padding: 12px 14px; }
|
||||
@@ -286,6 +322,7 @@
|
||||
<div class="tab" data-tab="products">商品图 <span class="count">0</span></div>
|
||||
<div class="tab" data-tab="finals">成片 <span class="count">0</span></div>
|
||||
<div class="tab" data-tab="uploads">我的上传 <span class="count">0</span></div>
|
||||
<div class="tab" data-tab="unclassified">未分类 <span class="count">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
@@ -399,7 +436,18 @@
|
||||
<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-asset"><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 asset-thumb">
|
||||
<span class="tri-missing-badge" title="手动上传的人物未生成三视图,前往图片生成可补齐"><span class="lbl-mono">缺三视图</span></span>
|
||||
<span class="tri-missing-badge" tabindex="0" role="button" aria-label="缺三视图,查看说明">
|
||||
<span class="ico" aria-hidden="true"></span>
|
||||
<span class="lbl-mono">缺三视图</span>
|
||||
<span class="tri-missing-pop" role="tooltip">
|
||||
<span class="pop-h">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
|
||||
MISSING TRI-VIEW
|
||||
</span>
|
||||
<span class="pop-body">手动上传的人物未生成 <b>正 / 侧 / 背</b> 三视图。直接进入图片或视频生成,人脸/服饰一致性可能下降。</span>
|
||||
<span class="pop-tip">建议:前往 <b>图片生成</b> 先补齐三视图,再发起后续生成。</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="ph-frame">妈妈 · 居家</span>
|
||||
</div>
|
||||
<div class="asset-body">
|
||||
@@ -456,7 +504,18 @@
|
||||
<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-asset"><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 asset-thumb">
|
||||
<span class="tri-missing-badge" title="手动上传的人物未生成三视图,前往图片生成可补齐"><span class="lbl-mono">缺三视图</span></span>
|
||||
<span class="tri-missing-badge" tabindex="0" role="button" aria-label="缺三视图,查看说明">
|
||||
<span class="ico" aria-hidden="true"></span>
|
||||
<span class="lbl-mono">缺三视图</span>
|
||||
<span class="tri-missing-pop" role="tooltip">
|
||||
<span class="pop-h">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01"/><path d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
|
||||
MISSING TRI-VIEW
|
||||
</span>
|
||||
<span class="pop-body">手动上传的人物未生成 <b>正 / 侧 / 背</b> 三视图。直接进入图片或视频生成,人脸/服饰一致性可能下降。</span>
|
||||
<span class="pop-tip">建议:前往 <b>图片生成</b> 先补齐三视图,再发起后续生成。</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="ph-frame">李爷爷 · 居家</span>
|
||||
</div>
|
||||
<div class="asset-body">
|
||||
@@ -692,6 +751,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ 未分类(由图片优化"加入资产库"持久化进来) ============ -->
|
||||
<div class="asset-grid" data-tab="unclassified" id="grid-unclassified" hidden></div>
|
||||
|
||||
<div class="empty-state" id="empty">
|
||||
<div class="ic-empty">
|
||||
<svg width="20" height="20" 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>
|
||||
@@ -866,6 +928,14 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" 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="move-wrap">
|
||||
<button type="button" id="bulk-move">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/><path d="M3 12h12"/></svg>
|
||||
移动到
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" style="margin-left:2px"><path d="M4 10l4-4 4 4"/></svg>
|
||||
</button>
|
||||
<div class="move-menu" id="bulk-move-menu"></div>
|
||||
</div>
|
||||
<button type="button" id="bulk-exit">完成</button>
|
||||
</div>
|
||||
|
||||
@@ -897,7 +967,47 @@ Shell.render({ active: 'library', crumbs: [{ label: '工作台', href: 'index.ht
|
||||
})();
|
||||
|
||||
// ============== State ==============
|
||||
const TAB_KEYS = ['people', 'scenes', 'products', 'finals', 'uploads'];
|
||||
const TAB_KEYS = ['people', 'scenes', 'products', 'finals', 'uploads', 'unclassified'];
|
||||
|
||||
/* ============== 加载图片优化"加入资产库"持久化数据 ==============
|
||||
image-optimize.html 把图保存到 localStorage['fs-library-unclassified']
|
||||
这里读出后注入到 #grid-unclassified ============== */
|
||||
(function loadUnclassified() {
|
||||
let list;
|
||||
try { list = JSON.parse(localStorage.getItem('fs-library-unclassified') || '[]'); } catch (e) { list = []; }
|
||||
if (!Array.isArray(list) || !list.length) return;
|
||||
const grid = document.getElementById('grid-unclassified');
|
||||
if (!grid) return;
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
||||
function fmtDate(ts) {
|
||||
if (!ts) return '';
|
||||
const d = new Date(ts), z = n => (n < 10 ? '0' + n : '' + n);
|
||||
return d.getFullYear() + z(d.getMonth() + 1) + z(d.getDate());
|
||||
}
|
||||
list.forEach(it => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'asset-card';
|
||||
card.dataset.name = it.name || '未命名';
|
||||
card.dataset.kind = '未分类';
|
||||
card.dataset.source = it.source || '图片优化';
|
||||
card.dataset.used = '0';
|
||||
card.dataset.added = fmtDate(it.addedAt);
|
||||
card.dataset.libId = it.id || '';
|
||||
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-asset"><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 asset-thumb"><span class="ph-frame">${esc(it.name || '未命名')}</span></div>
|
||||
<div class="asset-body">
|
||||
<div class="asset-name">${esc(it.name || '未命名')}</div>
|
||||
<div class="asset-meta">未分类 · ${esc(it.source || '图片优化')} · ${esc(it.ratio || '')}</div>
|
||||
</div>
|
||||
`;
|
||||
card.addEventListener('click', () => {
|
||||
if (typeof Shell !== 'undefined' && Shell.toast) Shell.toast('查看资产', it.name || '未分类素材');
|
||||
});
|
||||
grid.appendChild(card);
|
||||
});
|
||||
})();
|
||||
const PAGE_SIZES = [12, 24, 48, 96];
|
||||
const state = {
|
||||
tab: 'people',
|
||||
@@ -1648,7 +1758,19 @@ delCancel.addEventListener('click', closeDelConfirm);
|
||||
delBg.addEventListener('click', e => { if (e.target === delBg) closeDelConfirm(); });
|
||||
delOk.addEventListener('click', () => {
|
||||
const n = _delQueue.length;
|
||||
// 收集被删除中、source 是"未分类"的 libId,同步从 localStorage 移除
|
||||
const removedLibIds = _delQueue
|
||||
.filter(c => c.dataset.libId)
|
||||
.map(c => c.dataset.libId);
|
||||
_delQueue.forEach(card => card.remove());
|
||||
if (removedLibIds.length) {
|
||||
try {
|
||||
const LIB_KEY = 'fs-library-unclassified';
|
||||
const list = JSON.parse(localStorage.getItem(LIB_KEY) || '[]');
|
||||
const next = (Array.isArray(list) ? list : []).filter(x => !removedLibIds.includes(x.id));
|
||||
localStorage.setItem(LIB_KEY, JSON.stringify(next));
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
closeDelConfirm();
|
||||
Shell.toast('已删除', n === 1 ? '资产已移除' : '已删除 ' + n + ' 个资产');
|
||||
updateBulkBar();
|
||||
@@ -1684,6 +1806,7 @@ function enterEditMode() {
|
||||
document.body.classList.add('edit-mode');
|
||||
libManageBtn.classList.add('active');
|
||||
libManageLabel.textContent = '完成';
|
||||
applyDraggableToCards(true);
|
||||
updateBulkBar();
|
||||
}
|
||||
function exitEditMode() {
|
||||
@@ -1691,6 +1814,8 @@ function exitEditMode() {
|
||||
libManageBtn.classList.remove('active');
|
||||
libManageLabel.textContent = '管理资产';
|
||||
document.querySelectorAll('.asset-card.selected').forEach(c => c.classList.remove('selected'));
|
||||
applyDraggableToCards(false);
|
||||
if (bulkMoveMenu) bulkMoveMenu.classList.remove('show');
|
||||
}
|
||||
libManageBtn.addEventListener('click', () => {
|
||||
if (document.body.classList.contains('edit-mode')) exitEditMode();
|
||||
@@ -1707,6 +1832,132 @@ bulkDel.addEventListener('click', () => {
|
||||
openDelConfirm(sel);
|
||||
});
|
||||
|
||||
// ── 移动到 · 菜单 + 拖拽 ──
|
||||
const TAB_NAMES = {
|
||||
people: '人物', scenes: '场景', products: '商品图',
|
||||
finals: '成片', uploads: '我的上传', unclassified: '未分类'
|
||||
};
|
||||
const bulkMove = document.getElementById('bulk-move');
|
||||
const bulkMoveMenu = document.getElementById('bulk-move-menu');
|
||||
|
||||
function getCurrentTab() {
|
||||
const active = document.querySelector('#asset-tabs .tab.active');
|
||||
return active ? active.dataset.tab : TAB_KEYS[0];
|
||||
}
|
||||
function refreshAllTabCounts() {
|
||||
TAB_KEYS.forEach(t => {
|
||||
const tab = document.querySelector(`.tab[data-tab="${t}"] .count`);
|
||||
if (tab && cardsByTab[t]) tab.textContent = cardsByTab[t].length;
|
||||
});
|
||||
const total = TAB_KEYS.reduce((s, t) => s + (cardsByTab[t] ? cardsByTab[t].length : 0), 0);
|
||||
const sidebarBadge = document.querySelector('aside.sidebar a[href="library.html"] .pill-mini');
|
||||
if (sidebarBadge) sidebarBadge.textContent = total;
|
||||
const subMap = { people:'sub-people', scenes:'sub-scenes', products:'sub-products', finals:'sub-finals' };
|
||||
Object.keys(subMap).forEach(k => {
|
||||
const el = document.getElementById(subMap[k]);
|
||||
if (el && cardsByTab[k]) el.textContent = cardsByTab[k].length;
|
||||
});
|
||||
}
|
||||
function moveSelectedTo(targetTab) {
|
||||
const sel = getSelected();
|
||||
if (!sel.length) { Shell.toast('请先选中资产'); return; }
|
||||
const targetGrid = document.getElementById(`grid-${targetTab}`);
|
||||
if (!targetGrid) return;
|
||||
let movedCount = 0;
|
||||
sel.forEach(card => {
|
||||
// 找出 card 当前所在 tab (DOM-based,跨多 tab 的 selected 也能正确移动)
|
||||
const curGrid = card.closest('.asset-grid');
|
||||
const curTab = curGrid ? curGrid.dataset.tab : getCurrentTab();
|
||||
if (curTab === targetTab) return; // 同分类跳过
|
||||
// 更新内存
|
||||
if (cardsByTab[curTab]) {
|
||||
const idx = cardsByTab[curTab].indexOf(card);
|
||||
if (idx >= 0) cardsByTab[curTab].splice(idx, 1);
|
||||
}
|
||||
if (cardsByTab[targetTab]) cardsByTab[targetTab].push(card);
|
||||
// 更新 DOM
|
||||
card.classList.remove('selected');
|
||||
card.dataset.kind = TAB_NAMES[targetTab];
|
||||
targetGrid.appendChild(card);
|
||||
movedCount += 1;
|
||||
});
|
||||
refreshAllTabCounts();
|
||||
updateBulkBar();
|
||||
if (movedCount > 0) {
|
||||
Shell.toast('已移动', `${movedCount} 个资产 → 「${TAB_NAMES[targetTab]}」`);
|
||||
} else {
|
||||
Shell.toast('未移动', '所选资产已在该分类');
|
||||
}
|
||||
}
|
||||
function renderMoveMenu() {
|
||||
const cur = getCurrentTab();
|
||||
bulkMoveMenu.innerHTML = TAB_KEYS
|
||||
.filter(t => t !== cur)
|
||||
.map(t => `<button class="mv-item" type="button" data-target="${t}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
移到「${TAB_NAMES[t]}」
|
||||
</button>`).join('');
|
||||
bulkMoveMenu.querySelectorAll('.mv-item').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
moveSelectedTo(btn.dataset.target);
|
||||
bulkMoveMenu.classList.remove('show');
|
||||
});
|
||||
});
|
||||
}
|
||||
bulkMove.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
if (!getSelected().length) { Shell.toast('请先选中资产'); return; }
|
||||
renderMoveMenu();
|
||||
bulkMoveMenu.classList.toggle('show');
|
||||
});
|
||||
document.addEventListener('click', e => {
|
||||
if (!bulkMove.contains(e.target) && !bulkMoveMenu.contains(e.target)) {
|
||||
bulkMoveMenu.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// ── 拖拽到 tab 移动 (edit-mode 下生效) ──
|
||||
function applyDraggableToCards(on) {
|
||||
document.querySelectorAll('.asset-card').forEach(c => {
|
||||
if (on) c.setAttribute('draggable', 'true');
|
||||
else c.removeAttribute('draggable');
|
||||
});
|
||||
}
|
||||
document.addEventListener('dragstart', e => {
|
||||
const card = e.target.closest('.asset-card');
|
||||
if (!card || !document.body.classList.contains('edit-mode')) return;
|
||||
// 没选中就当前 card 也算 (允许直接拖单张)
|
||||
if (!card.classList.contains('selected')) {
|
||||
card.classList.add('selected');
|
||||
updateBulkBar();
|
||||
}
|
||||
card.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
try { e.dataTransfer.setData('text/plain', 'move-asset'); } catch (err) {}
|
||||
});
|
||||
document.addEventListener('dragend', e => {
|
||||
const card = e.target.closest('.asset-card');
|
||||
if (card) card.classList.remove('dragging');
|
||||
document.querySelectorAll('#asset-tabs .tab.drag-over').forEach(t => t.classList.remove('drag-over'));
|
||||
});
|
||||
document.querySelectorAll('#asset-tabs .tab').forEach(tab => {
|
||||
tab.addEventListener('dragover', e => {
|
||||
if (!document.body.classList.contains('edit-mode')) return;
|
||||
if (tab.dataset.tab === getCurrentTab()) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
tab.classList.add('drag-over');
|
||||
});
|
||||
tab.addEventListener('dragleave', () => tab.classList.remove('drag-over'));
|
||||
tab.addEventListener('drop', e => {
|
||||
if (!document.body.classList.contains('edit-mode')) return;
|
||||
e.preventDefault();
|
||||
tab.classList.remove('drag-over');
|
||||
if (tab.dataset.tab === getCurrentTab()) return;
|
||||
moveSelectedTo(tab.dataset.tab);
|
||||
});
|
||||
});
|
||||
|
||||
// 编辑模式下,卡片点击切换 selected (不再 toast / 打开)
|
||||
document.querySelectorAll('.asset-card').forEach(card => {
|
||||
card.addEventListener('click', e => {
|
||||
|
||||
Reference in New Issue
Block a user