AirShelf/电商AI平台/library.html
iye 04335f3269
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 7s
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 修复
2026-05-22 19:35:36 +08:00

2228 lines
134 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<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=202605211800">
<style>
.asset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
.asset-grid.video-grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
/* 修复:.asset-grid 的 display:grid 会盖过 [hidden] 的默认 display:none, 导致切 tab 时其它 tab 的卡片仍可见 */
.asset-grid[hidden] { display: none; }
/* ─── 底部分页 (吸底) ─── */
.pagination {
position: sticky;
bottom: 0;
z-index: 5;
display: flex; align-items: center; gap: 16px;
padding: 14px 28px;
margin: 20px -28px 0;
border-top: 1px solid var(--border-faint);
background: var(--background-base);
box-shadow: 0 -8px 24px -16px rgba(0, 0, 0, .08);
font-size: 12.5px;
color: var(--black-alpha-56);
}
.pagination[hidden] { display: none; }
.pagination .total { font-family: var(--font-mono); letter-spacing: .02em; }
.pagination .page-size {
display: inline-flex; align-items: center; gap: 4px;
height: 30px; 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);
transition: border-color var(--t-base), color var(--t-base);
}
.pagination .page-size:hover { border-color: var(--black-alpha-32); color: var(--accent-black); }
.pagination .page-size svg { width: 10px; height: 10px; opacity: .6; }
.pagination .pages {
display: inline-flex; gap: 4px;
margin-left: auto;
}
.pagination .pages button {
min-width: 30px; height: 30px;
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: background var(--t-base), border-color var(--t-base), color var(--t-base);
}
.pagination .pages button:hover:not(.active):not(:disabled) { border-color: var(--black-alpha-32); color: var(--accent-black); }
.pagination .pages button.active {
background: var(--heat);
color: var(--accent-white);
border-color: var(--heat);
font-weight: 600;
}
.pagination .pages button:disabled { opacity: .4; cursor: not-allowed; }
.pagination .pages .ellipsis {
min-width: 22px; height: 30px;
display: inline-flex; align-items: center; justify-content: center;
color: var(--black-alpha-48);
font-family: var(--font-mono);
}
.pagination .jump {
display: inline-flex; align-items: center; gap: 6px;
color: var(--black-alpha-56);
}
.pagination .jump input {
width: 44px; height: 30px;
border: 1px solid var(--border-faint);
background: var(--surface);
border-radius: var(--r-sm);
text-align: center;
font-size: 12.5px;
color: var(--accent-black);
font-family: inherit;
outline: none;
transition: border-color var(--t-base);
}
.pagination .jump input:focus { border-color: var(--heat-40); }
@media (max-width: 1100px) {
.pagination { margin: 20px -24px 0; padding: 14px 24px; }
}
.asset-card { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); cursor: pointer; transition: background .15s; position: relative; }
.asset-card:hover { background: var(--background-lighter); border-color: var(--black-alpha-48); }
/* 下载按钮 · hover 卡片显示,与 card-del-btn 并列 · PRD §6.5 中间产物可下载 */
.asset-card .card-dl-btn {
position: absolute;
top: 8px; right: 48px;
width: 32px; height: 32px;
background: rgba(255,255,255,.95);
border: 1px solid var(--black-alpha-12);
border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-56);
cursor: pointer;
opacity: 0;
transition: opacity var(--t-base), background var(--t-base), color var(--t-base);
z-index: 4;
}
.asset-card .card-dl-btn svg { width: 14px; height: 14px; }
.asset-card:hover .card-dl-btn { opacity: 1; }
.asset-card .card-dl-btn:hover { background: var(--heat); color: var(--accent-white); border-color: var(--heat); }
body.edit-mode .asset-card .card-dl-btn { opacity: 0 !important; pointer-events: none !important; }
/* 编辑模式 checkbox */
.asset-card .card-check {
position: absolute; top: 10px; left: 10px;
width: 22px; height: 22px;
border-radius: 50%;
background: var(--surface);
border: 2px solid var(--black-alpha-32);
display: none;
place-items: center;
color: var(--accent-white);
z-index: 5;
pointer-events: none;
}
.asset-card .card-check svg { width: 11px; height: 11px; opacity: 0; }
body.edit-mode .asset-card { cursor: pointer; }
body.edit-mode .asset-card .card-check { display: grid; }
body.edit-mode .asset-card.selected .card-check {
background: var(--heat); border-color: var(--heat);
}
body.edit-mode .asset-card.selected .card-check svg { opacity: 1; }
body.edit-mode .asset-card.selected { border-color: var(--heat); box-shadow: 0 0 0 1px var(--heat) inset; }
body.edit-mode .asset-card .card-del-btn { opacity: 0 !important; pointer-events: none !important; }
/* edit-mode 下「管理资产」按钮变成「完成」 */
.btn.active {
background: var(--accent-black);
color: var(--accent-white);
border-color: var(--accent-black);
}
/* bulk-bar (浮动批量操作栏) */
.bulk-bar {
position: fixed;
bottom: 24px; left: 50%;
transform: translateX(-50%);
background: var(--accent-black);
color: var(--accent-white);
border-radius: var(--r-md);
padding: 10px 14px 10px 18px;
display: none;
align-items: center; gap: 16px;
box-shadow: 0 8px 24px rgba(0,0,0,.18);
z-index: 100;
font-size: 13px;
}
body.edit-mode .bulk-bar { display: inline-flex; }
.bulk-bar .ct { font-family: var(--font-mono); letter-spacing: .02em; }
.bulk-bar .ct b { color: var(--heat); font-weight: 700; padding: 0 3px; }
.bulk-bar .sep { width: 1px; height: 18px; background: rgba(255,255,255,.16); }
.bulk-bar button {
height: 30px; padding: 0 12px;
background: transparent;
border: 1px solid rgba(255,255,255,.24);
border-radius: var(--r-sm);
color: var(--accent-white);
font-size: 12.5px;
font-family: inherit;
cursor: pointer;
display: inline-flex; align-items: center; gap: 5px;
transition: background var(--t-base);
}
.bulk-bar button:hover { background: rgba(255,255,255,.08); }
.bulk-bar button.danger { background: var(--accent-crimson); border-color: var(--accent-crimson); }
.bulk-bar button.danger:hover { filter: brightness(1.06); }
.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; }
.asset-name { font-size: 13px; font-weight: 600; color: var(--accent-black); }
.asset-meta { font-size: 11px; color: var(--black-alpha-48); margin-top: 3px; font-family: var(--font-mono); letter-spacing: .02em; }
.asset-badge { position: absolute; top: 8px; left: 8px; font-family: var(--font-mono); font-size: 10px; letter-spacing: .04em; padding: 2px 6px; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-56); }
.asset-card .placeholder { position: relative; }
/* ─── Upload modal ─── */
.upload-modal {
max-width: 520px; width: 92%;
max-height: calc(100vh - 80px);
display: flex; flex-direction: column;
}
.upload-modal .modal-h {
position: relative;
flex-shrink: 0;
}
.upload-modal .modal-h .ti { flex: 1; min-width: 0; }
.upload-modal .modal-h .modal-x {
width: 32px; height: 32px; border-radius: var(--r-md);
display: grid; place-items: center;
color: var(--black-alpha-56); cursor: pointer;
background: transparent; border: 0; padding: 0;
flex-shrink: 0;
transition: background var(--t-base), color var(--t-base);
}
.upload-modal .modal-h .modal-x:hover { background: var(--black-alpha-4); color: var(--accent-crimson); }
.upload-modal .modal-h .modal-x svg { width: 14px; height: 14px; }
.upload-modal .modal-b {
flex: 1; min-height: 0;
overflow-y: auto;
padding: 20px 24px;
}
.upload-modal .modal-f { flex-shrink: 0; }
.upload-modal .field { margin-bottom: 16px; }
.upload-modal .field:last-child { margin-bottom: 0; }
/* 修复:.field 的 display:flex 会盖过 [hidden] 的默认 display:none */
.upload-modal .modal-b .field[hidden] { display: none; }
.upload-modal .upload-zone {
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 8px; padding: 24px; cursor: pointer;
border: 1px dashed var(--border-faint); border-radius: var(--r-md);
background: var(--background-lighter); color: var(--black-alpha-56);
font-size: 13px; transition: border-color .15s, background .15s, color .15s;
}
.upload-modal .upload-zone:hover { border-color: var(--heat); background: var(--heat-8); color: var(--heat); }
.upload-modal .upload-zone:hover .uz-ic { background: var(--heat); color: #fff; border-color: var(--heat); }
.upload-modal .upload-zone .uz-ic {
width: 40px; height: 40px; border-radius: var(--r-md);
border: 1px solid var(--border-faint); background: var(--surface);
display: grid; place-items: center; color: var(--black-alpha-56);
transition: background .15s, color .15s, border-color .15s;
}
.upload-modal .upload-zone .uz-ic svg { width: 18px; height: 18px; }
.upload-modal .upload-zone .uz-hint { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
.upload-modal .upload-preview {
position: relative;
width: calc((100% - 32px) / 5);
min-width: 80px; max-width: 110px;
aspect-ratio: 1; border-radius: var(--r-md);
overflow: hidden; border: 1px solid var(--border-faint); background: var(--background-lighter);
}
.upload-modal .upload-preview.video { aspect-ratio: 9/16; max-height: none; min-width: 80px; max-width: 110px; margin: 0; }
.upload-modal .upload-preview img,
.upload-modal .upload-preview video { width: 100%; height: 100%; object-fit: cover; display: block; }
.upload-modal .upload-preview .preview-x {
position: absolute; top: 8px; right: 8px;
width: 24px; height: 24px; border-radius: 999px;
background: rgba(21, 20, 15, .7); color: #fff;
display: grid; place-items: center; cursor: pointer; border: 0;
transition: background .15s, transform .15s;
}
.upload-modal .upload-preview .preview-x:hover { background: var(--accent-crimson); transform: scale(1.08); }
.upload-modal .upload-preview .preview-x svg { width: 12px; height: 12px; }
.upload-modal .modal-f { align-items: center; }
.upload-modal .modal-f .modal-meta {
flex: 1; font-family: var(--font-mono); font-size: 11.5px;
color: var(--black-alpha-48); letter-spacing: .02em;
}
.upload-modal .modal-f .modal-meta .accent { color: var(--heat); font-weight: 600; }
.upload-modal .btn-primary:disabled { opacity: .5; cursor: not-allowed; box-shadow: none; }
</style>
</head>
<body>
<div id="page">
<div class="page-head">
<div>
<h1>资产库</h1>
<div class="sub"><span class="mono">// 跨项目复用 · <span id="sub-people">0</span> 人 · <span id="sub-scenes">0</span> 景 · <span id="sub-products">0</span> 商 · <span id="sub-finals">0</span></span></div>
</div>
<div class="actions">
<button class="btn" type="button" id="lib-manage-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>
<span class="lib-manage-label">管理资产</span>
</button>
<button class="btn btn-primary" id="open-upload-btn" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
上传资产
</button>
</div>
</div>
<div class="tabs" id="asset-tabs">
<div class="tab active" data-tab="people">人物 <span class="count">0</span></div>
<div class="tab" data-tab="scenes">场景 <span class="count">0</span></div>
<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">
<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 class="input" id="search-input" placeholder="搜索资产名称、标签">
</div>
<!-- ── 人物 tab 专属 ── -->
<div class="chip-wrap" data-key="gender" data-tabs="people">
<button class="chip" type="button"><span class="chip-label">性别</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<div class="chip-wrap" data-key="age" data-tabs="people">
<button class="chip" type="button"><span class="chip-label">年龄段</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<div class="chip-wrap" data-key="role" data-tabs="people">
<button class="chip" type="button"><span class="chip-label">角色标签</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<!-- ── 场景 tab 专属 ── -->
<div class="chip-wrap" data-key="sceneType" data-tabs="scenes">
<button class="chip" type="button"><span class="chip-label">场景类型</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<!-- ── 商品图 tab 专属 ── -->
<div class="chip-wrap" data-key="product" data-tabs="products">
<button class="chip" type="button"><span class="chip-label">关联商品</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<!-- ── 成片 tab 专属 ── -->
<div class="chip-wrap" data-key="project" data-tabs="finals">
<button class="chip" type="button"><span class="chip-label">关联项目</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<div class="chip-wrap" data-key="duration" data-tabs="finals">
<button class="chip" type="button"><span class="chip-label">时长</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<!-- ── 我的上传 tab 专属 ── -->
<div class="chip-wrap" data-key="kind" data-tabs="uploads">
<button class="chip" type="button"><span class="chip-label">资产类型</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<!-- ── 共用(人物 / 场景 / 商品图 / 我的上传)── -->
<div class="chip-wrap" data-key="source" data-tabs="people scenes products uploads">
<button class="chip" type="button"><span class="chip-label">来源</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu"></div>
</div>
<button class="clear-filters" id="clear-filters" type="button" hidden>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4l8 8M12 4l-8 8"/></svg>
清空筛选
</button>
<span class="spacer"></span>
<!-- ── 排序(所有 tab 共用)── -->
<div class="chip-wrap" data-key="sort" data-tabs="all">
<button class="chip" type="button"><span class="chip-label">最近使用</span> <svg class="caret" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg></button>
<div class="chip-menu align-right"></div>
</div>
</div>
<div class="result-meta" id="result-meta">// 显示 <span class="count">0</span> / 0 个资产</div>
<!-- ============ 人物 (8) ============ -->
<div class="asset-grid" data-tab="people" id="grid-people">
<div class="asset-card" data-name="林夕" data-gender="女" data-age="青年" data-role="都市白领" data-source="AI 生成" data-used="4" data-added="20260513" onclick="Shell.toast('查看资产', '林夕')">
<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">林夕 · 都市白领</span></div>
<div class="asset-body">
<div class="asset-name">林夕</div>
<div class="asset-meta">女 · 青年 · 都市白领 · 用过 4 次</div>
</div>
</div>
<div class="asset-card" data-name="阿楠" data-gender="女" data-age="青年" data-role="都市白领" data-source="AI 生成" data-used="2" data-added="20260507" onclick="Shell.toast('查看资产', '阿楠')">
<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">阿楠 · 同事女</span></div>
<div class="asset-body">
<div class="asset-name">阿楠</div>
<div class="asset-meta">女 · 青年 · 都市白领 · 用过 2 次</div>
</div>
</div>
<div class="asset-card" data-name="小七" data-gender="女" data-age="青年" data-role="学生" data-source="AI 生成" data-used="3" data-added="20260512" onclick="Shell.toast('查看资产', '小七')">
<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">小七 · 学生女</span></div>
<div class="asset-body">
<div class="asset-name">小七</div>
<div class="asset-meta">女 · 青年 · 学生 · 用过 3 次</div>
</div>
</div>
<div class="asset-card" data-name="阿杰" data-gender="男" data-age="青年" data-role="都市白领" data-source="AI 生成" data-used="2" data-added="20260428" onclick="Shell.toast('查看资产', '阿杰')">
<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">阿杰 · 通勤男</span></div>
<div class="asset-body">
<div class="asset-name">阿杰</div>
<div class="asset-meta">男 · 青年 · 都市白领 · 用过 2 次</div>
</div>
</div>
<div class="asset-card" data-name="妈妈 · 王姐" data-gender="女" data-age="中年" data-role="居家" data-source="手动上传" data-triview="0" data-used="1" data-added="20260415" onclick="Shell.toast('查看资产', '王姐')">
<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" 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">
<div class="asset-name">妈妈 · 王姐</div>
<div class="asset-meta">女 · 中年 · 居家 · 用过 1 次</div>
</div>
</div>
<div class="asset-card" data-name="阿强" data-gender="男" data-age="青年" data-role="健身" data-source="AI 生成" data-used="2" data-added="20260508" onclick="Shell.toast('查看资产', '阿强')">
<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">阿强 · 健身男</span></div>
<div class="asset-body">
<div class="asset-name">阿强</div>
<div class="asset-meta">男 · 青年 · 健身 · 用过 2 次</div>
</div>
</div>
<div class="asset-card" data-name="小苏" data-gender="女" data-age="青年" data-role="文艺" data-source="AI 生成" data-used="1" data-added="20260420" onclick="Shell.toast('查看资产', '小苏')">
<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">小苏 · 文艺女</span></div>
<div class="asset-body">
<div class="asset-name">小苏</div>
<div class="asset-meta">女 · 青年 · 文艺 · 用过 1 次</div>
</div>
</div>
<div class="asset-card" data-name="闺蜜组合" data-gender="女" data-age="青年" data-role="都市白领" data-source="AI 生成" data-used="1" data-added="20260511" onclick="Shell.toast('查看资产', '闺蜜组合')">
<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">闺蜜组合 · 双人</span></div>
<div class="asset-body">
<div class="asset-name">闺蜜组合</div>
<div class="asset-meta">女 · 青年 · 都市白领 · 用过 1 次</div>
</div>
</div>
<div class="asset-card" data-name="豆豆" data-gender="女" data-age="幼年" data-role="居家" data-source="AI 生成" data-used="2" data-added="20260509" onclick="Shell.toast('查看资产', '豆豆')">
<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">豆豆 · 幼儿</span></div>
<div class="asset-body">
<div class="asset-name">豆豆</div>
<div class="asset-meta">女 · 幼年 · 居家 · 用过 2 次</div>
</div>
</div>
<div class="asset-card" data-name="小宇" data-gender="男" data-age="少年" data-role="学生" data-source="AI 生成" data-used="1" data-added="20260502" onclick="Shell.toast('查看资产', '小宇')">
<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">小宇 · 中学生</span></div>
<div class="asset-body">
<div class="asset-name">小宇</div>
<div class="asset-meta">男 · 少年 · 学生 · 用过 1 次</div>
</div>
</div>
<div class="asset-card" data-name="李爷爷" data-gender="男" data-age="老年" data-role="居家" data-source="手动上传" data-triview="0" data-used="1" data-added="20260418" onclick="Shell.toast('查看资产', '李爷爷')">
<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" 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">
<div class="asset-name">李爷爷</div>
<div class="asset-meta">男 · 老年 · 居家 · 用过 1 次</div>
</div>
</div>
</div>
<!-- ============ 场景 (12) ============ -->
<div class="asset-grid" data-tab="scenes" id="grid-scenes" hidden>
<div class="asset-card" data-name="卧室·暖光" data-scene-type="卧室" data-source="AI 生成" data-used="6" data-added="20260513" onclick="Shell.toast('查看资产', '卧室·暖光')">
<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">卧室 · 暖光</span></div>
<div class="asset-body"><div class="asset-name">卧室·暖光</div><div class="asset-meta">卧室 · AI 生成 · 用过 6 次</div></div>
</div>
<div class="asset-card" data-name="卧室·冷调" data-scene-type="卧室" data-source="AI 生成" data-used="3" data-added="20260507" onclick="Shell.toast('查看资产', '卧室·冷调')">
<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">卧室 · 冷调</span></div>
<div class="asset-body"><div class="asset-name">卧室·冷调</div><div class="asset-meta">卧室 · AI 生成 · 用过 3 次</div></div>
</div>
<div class="asset-card" data-name="浴室·梳妆台" data-scene-type="浴室" data-source="AI 生成" data-used="4" data-added="20260510" onclick="Shell.toast('查看资产', '浴室·梳妆台')">
<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">浴室 · 梳妆台</span></div>
<div class="asset-body"><div class="asset-name">浴室·梳妆台</div><div class="asset-meta">浴室 · AI 生成 · 用过 4 次</div></div>
</div>
<div class="asset-card" data-name="客厅·北欧" data-scene-type="客厅" data-source="AI 生成" data-used="5" data-added="20260512" onclick="Shell.toast('查看资产', '客厅·北欧')">
<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">客厅 · 北欧</span></div>
<div class="asset-body"><div class="asset-name">客厅·北欧</div><div class="asset-meta">客厅 · AI 生成 · 用过 5 次</div></div>
</div>
<div class="asset-card" data-name="客厅·中古" data-scene-type="客厅" data-source="手动上传" data-used="1" data-added="20260418" onclick="Shell.toast('查看资产', '客厅·中古')">
<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">客厅 · 中古</span></div>
<div class="asset-body"><div class="asset-name">客厅·中古</div><div class="asset-meta">客厅 · 手动上传 · 用过 1 次</div></div>
</div>
<div class="asset-card" data-name="厨房·中岛" data-scene-type="厨房" data-source="AI 生成" data-used="3" data-added="20260509" onclick="Shell.toast('查看资产', '厨房·中岛')">
<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">厨房 · 中岛</span></div>
<div class="asset-body"><div class="asset-name">厨房·中岛</div><div class="asset-meta">厨房 · AI 生成 · 用过 3 次</div></div>
</div>
<div class="asset-card" data-name="办公室·开放" data-scene-type="办公室" data-source="AI 生成" data-used="2" data-added="20260506" onclick="Shell.toast('查看资产', '办公室·开放')">
<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">办公室 · 开放</span></div>
<div class="asset-body"><div class="asset-name">办公室·开放</div><div class="asset-meta">办公室 · AI 生成 · 用过 2 次</div></div>
</div>
<div class="asset-card" data-name="办公室·会议室" data-scene-type="办公室" data-source="AI 生成" data-used="1" data-added="20260425" onclick="Shell.toast('查看资产', '办公室·会议室')">
<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">办公室 · 会议室</span></div>
<div class="asset-body"><div class="asset-name">办公室·会议室</div><div class="asset-meta">办公室 · AI 生成 · 用过 1 次</div></div>
</div>
<div class="asset-card" data-name="咖啡店·窗边" data-scene-type="咖啡店" data-source="AI 生成" data-used="4" data-added="20260511" onclick="Shell.toast('查看资产', '咖啡店·窗边')">
<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">咖啡店 · 窗边</span></div>
<div class="asset-body"><div class="asset-name">咖啡店·窗边</div><div class="asset-meta">咖啡店 · AI 生成 · 用过 4 次</div></div>
</div>
<div class="asset-card" data-name="街景·夜" data-scene-type="街景" data-source="AI 生成" data-used="2" data-added="20260430" onclick="Shell.toast('查看资产', '街景·夜')">
<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">街景 · 夜</span></div>
<div class="asset-body"><div class="asset-name">街景·夜</div><div class="asset-meta">街景 · AI 生成 · 用过 2 次</div></div>
</div>
<div class="asset-card" data-name="健身房·器械" data-scene-type="健身房" data-source="AI 生成" data-used="3" data-added="20260508" onclick="Shell.toast('查看资产', '健身房·器械')">
<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">健身房 · 器械</span></div>
<div class="asset-body"><div class="asset-name">健身房·器械</div><div class="asset-meta">健身房 · AI 生成 · 用过 3 次</div></div>
</div>
<div class="asset-card" data-name="街景·日" data-scene-type="街景" data-source="手动上传" data-used="1" data-added="20260422" onclick="Shell.toast('查看资产', '街景·日')">
<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">街景 · 日</span></div>
<div class="asset-body"><div class="asset-name">街景·日</div><div class="asset-meta">街景 · 手动上传 · 用过 1 次</div></div>
</div>
</div>
<!-- ============ 商品图 (12) ============ -->
<div class="asset-grid" data-tab="products" id="grid-products" hidden>
<div class="asset-card" data-name="补水面膜 · 主图" data-product="透真补水面膜" data-source="商品库引用" data-used="5" data-added="20260513" onclick="Shell.toast('查看资产', '补水面膜 主图')">
<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">补水面膜 · 主图</span></div>
<div class="asset-body"><div class="asset-name">补水面膜 · 主图</div><div class="asset-meta">透真补水面膜 · 库引用 · 用过 5 次</div></div>
</div>
<div class="asset-card" data-name="补水面膜 · AI 优化" data-product="透真补水面膜" data-source="AI 优化" data-used="3" data-added="20260513" onclick="Shell.toast('查看资产', '补水面膜 AI 优化')">
<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">补水面膜 · AI 优化</span></div>
<div class="asset-body"><div class="asset-name">补水面膜 · AI 优化</div><div class="asset-meta">透真补水面膜 · AI 优化 · 用过 3 次</div></div>
</div>
<div class="asset-card" data-name="蓝牙耳机 · 主图" data-product="南卡 Lite Pro" data-source="商品库引用" data-used="4" data-added="20260507" onclick="Shell.toast('查看资产', '蓝牙耳机 主图')">
<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">蓝牙耳机 · 主图</span></div>
<div class="asset-body"><div class="asset-name">蓝牙耳机 · 主图</div><div class="asset-meta">南卡 Lite Pro · 库引用 · 用过 4 次</div></div>
</div>
<div class="asset-card" data-name="蓝牙耳机 · 场景图" data-product="南卡 Lite Pro" data-source="手动上传" data-used="1" data-added="20260507" onclick="Shell.toast('查看资产', '蓝牙耳机 场景')">
<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">蓝牙耳机 · 场景</span></div>
<div class="asset-body"><div class="asset-name">蓝牙耳机 · 场景图</div><div class="asset-meta">南卡 Lite Pro · 手动上传 · 用过 1 次</div></div>
</div>
<div class="asset-card" data-name="速食面 · 主图" data-product="滋啦速食" data-source="商品库引用" data-used="3" data-added="20260512" onclick="Shell.toast('查看资产', '速食面 主图')">
<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">速食面 · 主图</span></div>
<div class="asset-body"><div class="asset-name">速食面 · 主图</div><div class="asset-meta">滋啦速食 · 库引用 · 用过 3 次</div></div>
</div>
<div class="asset-card" data-name="速食面 · 加汤" data-product="滋啦速食" data-source="AI 优化" data-used="2" data-added="20260512" onclick="Shell.toast('查看资产', '速食面 加汤')">
<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">速食面 · 加汤</span></div>
<div class="asset-body"><div class="asset-name">速食面 · 加汤</div><div class="asset-meta">滋啦速食 · AI 优化 · 用过 2 次</div></div>
</div>
<div class="asset-card" data-name="防晒霜 · 主图" data-product="透真防晒霜" data-source="商品库引用" data-used="4" data-added="20260510" onclick="Shell.toast('查看资产', '防晒霜 主图')">
<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">防晒霜 · 主图</span></div>
<div class="asset-body"><div class="asset-name">防晒霜 · 主图</div><div class="asset-meta">透真防晒霜 · 库引用 · 用过 4 次</div></div>
</div>
<div class="asset-card" data-name="防晒霜 · AI 优化" data-product="透真防晒霜" data-source="AI 优化" data-used="3" data-added="20260510" onclick="Shell.toast('查看资产', '防晒霜 优化')">
<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">防晒霜 · AI 优化</span></div>
<div class="asset-body"><div class="asset-name">防晒霜 · AI 优化</div><div class="asset-meta">透真防晒霜 · AI 优化 · 用过 3 次</div></div>
</div>
<div class="asset-card" data-name="咖啡冻干 · 主图" data-product="三顿半同款" data-source="商品库引用" data-used="3" data-added="20260509" onclick="Shell.toast('查看资产', '咖啡冻干 主图')">
<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">咖啡冻干 · 主图</span></div>
<div class="asset-body"><div class="asset-name">咖啡冻干 · 主图</div><div class="asset-meta">三顿半同款 · 库引用 · 用过 3 次</div></div>
</div>
<div class="asset-card" data-name="咖啡冻干 · 24 颗" data-product="三顿半同款" data-source="商品库引用" data-used="2" data-added="20260509" onclick="Shell.toast('查看资产', '咖啡冻干 24 颗')">
<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">咖啡冻干 · 24 颗</span></div>
<div class="asset-body"><div class="asset-name">咖啡冻干 · 24 颗</div><div class="asset-meta">三顿半同款 · 库引用 · 用过 2 次</div></div>
</div>
<div class="asset-card" data-name="空气炸锅 · 主图" data-product="小熊 4L 空气炸锅" data-source="商品库引用" data-used="2" data-added="20260504" onclick="Shell.toast('查看资产', '空气炸锅 主图')">
<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">空气炸锅 · 主图</span></div>
<div class="asset-body"><div class="asset-name">空气炸锅 · 主图</div><div class="asset-meta">小熊 4L · 库引用 · 用过 2 次</div></div>
</div>
<div class="asset-card" data-name="瑜伽裤 · 模特图" data-product="露露同款瑜伽裤" data-source="手动上传" data-used="3" data-added="20260506" onclick="Shell.toast('查看资产', '瑜伽裤 模特')">
<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">瑜伽裤 · 模特</span></div>
<div class="asset-body"><div class="asset-name">瑜伽裤 · 模特图</div><div class="asset-meta">露露同款瑜伽裤 · 手动上传 · 用过 3 次</div></div>
</div>
</div>
<!-- ============ 成片 (8) ============ -->
<div class="asset-grid video-grid" data-tab="finals" id="grid-finals" hidden>
<div class="asset-card video" data-name="蓝牙耳机 · 开箱测评" data-project="蓝牙耳机 · 开箱测评" data-duration="60s" data-used="3" data-added="20260507" onclick="Shell.toast('打开成片', '蓝牙耳机 · 开箱测评')">
<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">9:16 · 60s</span></div>
<div class="asset-body"><div class="asset-name">蓝牙耳机 · 开箱测评</div><div class="asset-meta">南卡 Lite Pro · 60s · 5 月 7 日</div></div>
</div>
<div class="asset-card video" data-name="瑜伽裤 · 通勤穿搭" data-project="瑜伽裤 · 通勤穿搭" data-duration="45s" data-used="2" data-added="20260506" onclick="Shell.toast('打开成片', '瑜伽裤 · 通勤穿搭')">
<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">9:16 · 45s</span></div>
<div class="asset-body"><div class="asset-name">瑜伽裤 · 通勤穿搭</div><div class="asset-meta">露露同款 · 45s · 5 月 6 日</div></div>
</div>
<div class="asset-card video" data-name="空气炸锅 · 小户型" data-project="空气炸锅 · 小户型" data-duration="30s" data-used="2" data-added="20260504" onclick="Shell.toast('打开成片', '空气炸锅 · 小户型')">
<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">9:16 · 30s</span></div>
<div class="asset-body"><div class="asset-name">空气炸锅 · 小户型</div><div class="asset-meta">小熊 4L · 30s · 5 月 4 日</div></div>
</div>
<div class="asset-card video" data-name="补水面膜 · 痛点种草 v1" data-project="补水面膜 · 痛点种草 v1" data-duration="60s" data-used="2" data-added="20260428" onclick="Shell.toast('打开成片', '补水面膜 v1')">
<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">9:16 · 60s</span></div>
<div class="asset-body"><div class="asset-name">补水面膜 · 痛点种草 v1</div><div class="asset-meta">透真补水面膜 · 60s · 4 月 28 日</div></div>
</div>
<div class="asset-card video" data-name="防晒霜 · 通勤对比" data-project="防晒霜 · 通勤对比" data-duration="60s" data-used="1" data-added="20260425" onclick="Shell.toast('打开成片', '防晒霜 · 通勤对比')">
<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">9:16 · 60s</span></div>
<div class="asset-body"><div class="asset-name">防晒霜 · 通勤对比</div><div class="asset-meta">透真防晒霜 · 60s · 4 月 25 日</div></div>
</div>
<div class="asset-card video" data-name="速食面 · 加班治愈" data-project="速食面 · 加班治愈" data-duration="30s" data-used="1" data-added="20260420" onclick="Shell.toast('打开成片', '速食面 · 加班治愈')">
<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">9:16 · 30s</span></div>
<div class="asset-body"><div class="asset-name">速食面 · 加班治愈</div><div class="asset-meta">滋啦速食 · 30s · 4 月 20 日</div></div>
</div>
<div class="asset-card video" data-name="咖啡 · 早八剧情" data-project="咖啡 · 早八剧情" data-duration="45s" data-used="2" data-added="20260418" onclick="Shell.toast('打开成片', '咖啡 · 早八剧情')">
<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">9:16 · 45s</span></div>
<div class="asset-body"><div class="asset-name">咖啡 · 早八剧情</div><div class="asset-meta">三顿半同款 · 45s · 4 月 18 日</div></div>
</div>
<div class="asset-card video" data-name="收纳 · 北欧" data-project="收纳 · 北欧" data-duration="15s" data-used="1" data-added="20260410" onclick="Shell.toast('打开成片', '收纳 · 北欧')">
<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">9:16 · 15s</span></div>
<div class="asset-body"><div class="asset-name">收纳 · 北欧</div><div class="asset-meta">家居好物 · 15s · 4 月 10 日</div></div>
</div>
</div>
<!-- ============ 我的上传 (3) ============ -->
<div class="asset-grid" data-tab="uploads" id="grid-uploads" hidden>
<div class="asset-card" data-name="林夕 · 主播照" data-kind="人物" data-source="手动上传" data-used="4" data-added="20260513" onclick="Shell.toast('查看资产', '林夕 · 主播照')">
<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">林夕 · 主播照</span></div>
<div class="asset-body"><div class="asset-name">林夕 · 主播照</div><div class="asset-meta">人物 · 手动上传 · 用过 4 次</div></div>
</div>
<div class="asset-card" data-name="卧室 · 实拍" data-kind="场景" data-source="手动上传" data-used="2" data-added="20260510" onclick="Shell.toast('查看资产', '卧室 · 实拍')">
<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">卧室 · 实拍</span></div>
<div class="asset-body"><div class="asset-name">卧室 · 实拍</div><div class="asset-meta">场景 · 手动上传 · 用过 2 次</div></div>
</div>
<div class="asset-card" data-name="防晒霜 · 官方图" data-kind="商品" data-source="手动上传" data-used="3" data-added="20260507" onclick="Shell.toast('查看资产', '防晒霜 · 官方图')">
<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">防晒霜 · 官方图</span></div>
<div class="asset-body"><div class="asset-name">防晒霜 · 官方图</div><div class="asset-meta">商品 · 手动上传 · 用过 3 次</div></div>
</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>
</div>
<h3>没有匹配的资产</h3>
<p>// 试试切换 tab 或修改搜索词</p>
</div>
<!-- ============ 分页 (吸底) ============ -->
<div class="pagination" id="pagination" hidden>
<span class="total"><b id="page-total">0</b></span>
<button class="page-size" type="button" id="page-size-btn" title="切换每页条数">
<span id="page-size-label">12 条/页</span>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 6l4 4 4-4"/></svg>
</button>
<span class="pages" id="page-list"></span>
<span class="jump">跳至 <input type="number" min="1" value="1" id="page-jump"></span>
</div>
<!-- ============ 上传资产 Modal ============ -->
<div class="modal-bg" id="upload-modal-bg" onclick="if(event.target===this)Shell.closeModal('upload-modal-bg')">
<div class="modal upload-modal">
<span class="corner-tr"></span>
<span class="corner-bl"></span>
<div class="modal-h">
<div class="ic-m">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
</div>
<div class="ti">上传资产<span>// 跨项目共享 · 不消耗 token</span></div>
<button class="modal-x" type="button" onclick="Shell.closeModal('upload-modal-bg')" aria-label="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="modal-b">
<div class="field">
<label class="field-label">资产类型<span class="req">*</span></label>
<select class="select" id="upload-kind">
<option value="people">人物</option>
<option value="scenes">场景</option>
<option value="products">商品图</option>
<option value="finals">成片(视频)</option>
</select>
</div>
<div class="field">
<label class="field-label">资产文件<span class="req">*</span></label>
<input type="file" id="upload-file" accept="image/*" hidden>
<div class="upload-zone" id="upload-zone">
<span class="uz-ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg>
</span>
<span id="upload-zone-text">点击或拖拽上传图片</span>
<span class="uz-hint" id="upload-zone-hint">// JPG / PNG / WEBP · 单文件</span>
</div>
<div class="upload-preview" id="upload-preview" hidden>
<img id="upload-preview-img" alt="预览">
<video id="upload-preview-video" controls hidden></video>
<button class="preview-x" type="button" id="upload-preview-x" aria-label="移除">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 4l8 8M12 4l-8 8"/></svg>
</button>
</div>
</div>
<div class="field">
<label class="field-label">资产名称<span class="req">*</span></label>
<input class="input" id="upload-name" placeholder="例: 林夕 · 都市白领">
</div>
<!-- 人物 字段 -->
<div class="field" data-fields="people">
<label class="field-label">性别</label>
<select class="select" id="upload-gender">
<option value="女"></option>
<option value="男"></option>
</select>
</div>
<div class="field" data-fields="people">
<label class="field-label">年龄段</label>
<select class="select" id="upload-age">
<option value="幼年">幼年</option>
<option value="少年">少年</option>
<option value="青年" selected>青年</option>
<option value="中年">中年</option>
<option value="老年">老年</option>
</select>
</div>
<div class="field" data-fields="people">
<label class="field-label">角色标签</label>
<input class="input" id="upload-role" placeholder="例: 都市白领 / 学生 / 居家">
</div>
<!-- 场景 字段 -->
<div class="field" data-fields="scenes">
<label class="field-label">场景类型<span class="req">*</span></label>
<input class="input" id="upload-scene-type" placeholder="例: 卧室 / 客厅 / 办公室">
</div>
<!-- 商品图 字段 -->
<div class="field" data-fields="products">
<label class="field-label">关联商品<span class="req">*</span></label>
<select class="select" id="upload-product">
<option value="">— 选择商品 —</option>
<option>透真补水面膜</option>
<option>南卡 Lite Pro</option>
<option>滋啦速食</option>
<option>透真防晒霜</option>
<option>三顿半同款</option>
<option>小熊 4L 空气炸锅</option>
<option>露露同款瑜伽裤</option>
</select>
</div>
<!-- 成片 字段 -->
<div class="field" data-fields="finals">
<label class="field-label">关联项目</label>
<input class="input" id="upload-project" placeholder="例: 蓝牙耳机 · 开箱测评">
</div>
<div class="field" data-fields="finals">
<label class="field-label">时长</label>
<select class="select" id="upload-duration">
<option value="15s">15 秒</option>
<option value="30s">30 秒</option>
<option value="45s">45 秒</option>
<option value="60s" selected>60 秒</option>
</select>
</div>
</div>
<div class="modal-f">
<span class="modal-meta">// 跨项目共享 · <span class="accent">不消耗 token</span></span>
<button class="btn" type="button" onclick="Shell.closeModal('upload-modal-bg')">取消</button>
<button class="btn btn-primary" id="upload-submit" type="button" disabled>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l5 5L20 6"/></svg>
上传到资产库
</button>
</div>
</div>
</div>
</div>
<!-- ===== 删除确认 modal ===== -->
<div class="modal-bg" id="del-confirm-bg">
<div class="modal" role="dialog">
<span class="corner-tr" aria-hidden></span>
<span class="corner-bl" aria-hidden></span>
<div class="modal-h">
<div class="ic-m" style="background:var(--crimson-bg,#fdebea);color:var(--accent-crimson,#c43d3d)">
<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>
</div>
<div class="ti">确认删除资产<span>// CONFIRM DELETE</span></div>
</div>
<div class="modal-b" id="del-confirm-body">即将删除该资产。</div>
<div class="modal-f" id="del-confirm-foot">
<button class="btn" type="button" id="del-confirm-cancel">取消</button>
<button class="btn" type="button" id="del-confirm-ok" style="background:var(--accent-crimson,#c43d3d);color:var(--accent-white);border-color:var(--accent-crimson,#c43d3d)">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/></svg>
确认删除
</button>
</div>
</div>
</div>
<!-- ===== bulk-bar ===== -->
<div class="bulk-bar" id="bulk-bar">
<span class="ct">已选 <b id="bulk-count">0</b></span>
<button class="clear-sel" type="button" id="bulk-clear">清空</button>
<span class="sep"></span>
<button class="danger" type="button" id="bulk-del">
<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>
<script src="assets/shell.js?v=202605211643"></script>
<script>
Shell.render({ active: 'library', crumbs: [{ label: '工作台', href: 'index.html' }, { label: '资产库' }] });
/* ─── 给所有资产卡注入下载按钮 · PRD §6.5 所有中间产物可下载 ─── */
(function injectDownloadBtns() {
const dlSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
document.querySelectorAll('.asset-card').forEach(card => {
if (card.querySelector('.card-dl-btn')) return;
const btn = document.createElement('button');
btn.className = 'card-dl-btn';
btn.type = 'button';
btn.title = '下载资产';
btn.innerHTML = dlSvg;
btn.addEventListener('click', (e) => {
e.stopPropagation();
const name = card.dataset.name || '资产';
// 推测卡片类型用作 mono 后缀
const grid = card.closest('.asset-grid');
const kind = grid ? grid.dataset.tab : '';
const kindLabel = { people: '人物 · PNG', scenes: '场景 · PNG', products: '商品 · PNG', finals: '成片 · MP4 1080p', uploads: '原始素材' }[kind] || '资产';
Shell.toast('下载中', name + ' · ' + kindLabel);
});
card.appendChild(btn);
});
})();
// ============== State ==============
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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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',
search: '',
// 人物
gender: 'all', age: 'all', role: 'all',
// 场景
sceneType: 'all',
// 商品图
product: 'all',
// 成片
project: 'all', duration: 'all',
// 我的上传
kind: 'all',
// 通用
source: 'all',
sort: 'used-desc',
// 分页
page: 1,
pageSize: 12,
};
const CHIP_DEFAULT_LABEL = {
gender: '性别', age: '年龄段', role: '角色标签',
sceneType: '场景类型',
product: '关联商品',
project: '关联项目', duration: '时长',
kind: '资产类型',
source: '来源',
};
const SORT_LABEL = {
'used-desc': '最近使用',
'added-desc': '最近添加',
'ref-desc': '引用次数',
};
const checkSvg = '<svg class="mi-check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 8 7 12 13 4"/></svg>';
// ============== Card pools per tab ==============
const cardsByTab = {};
TAB_KEYS.forEach(t => {
cardsByTab[t] = [...document.querySelectorAll(`#grid-${t} .asset-card`)];
});
// 同步 tab count + 副标题计数
TAB_KEYS.forEach(t => {
const tab = document.querySelector(`.tab[data-tab="${t}"] .count`);
if (tab) tab.textContent = cardsByTab[t].length;
});
document.getElementById('sub-people').textContent = cardsByTab.people.length;
document.getElementById('sub-scenes').textContent = cardsByTab.scenes.length;
document.getElementById('sub-products').textContent = cardsByTab.products.length;
document.getElementById('sub-finals').textContent = cardsByTab.finals.length;
// 同步 sidebar 徽章(资产总数)
const sidebarBadge = document.querySelector('aside.sidebar a[href="library.html"] .pill-mini');
if (sidebarBadge) {
const total = TAB_KEYS.reduce((s, t) => s + cardsByTab[t].length, 0);
sidebarBadge.textContent = total;
}
// ============== 构建下拉菜单 ==============
function uniqueValues(tab, attr) {
const set = new Set();
cardsByTab[tab].forEach(c => {
const v = c.dataset[attr];
if (v) set.add(v);
});
return [...set];
}
function buildMenu(key, options, defaultLabel) {
const wrap = document.querySelector(`.chip-wrap[data-key="${key}"]`);
if (!wrap) return;
const menu = wrap.querySelector('.chip-menu');
const all = `<div class="mi selected" data-value="all">${checkSvg}<span>${defaultLabel}</span></div><div class="mi-sep"></div>`;
const items = options.map(o => `<div class="mi" data-value="${o.value}">${checkSvg}<span>${o.label}</span></div>`).join('');
menu.innerHTML = all + items;
}
// 人物
buildMenu('gender', [
{ value: '女', label: '女' },
{ value: '男', label: '男' },
], '全部性别');
buildMenu('age', [
{ value: '幼年', label: '幼年' },
{ value: '少年', label: '少年' },
{ value: '青年', label: '青年' },
{ value: '中年', label: '中年' },
{ value: '老年', label: '老年' },
], '全部年龄段');
buildMenu('role', uniqueValues('people', 'role').map(v => ({ value: v, label: v })), '全部角色');
// 场景
buildMenu('sceneType', uniqueValues('scenes', 'sceneType').map(v => ({ value: v, label: v })), '全部场景');
// 商品图
buildMenu('product', uniqueValues('products', 'product').map(v => ({ value: v, label: v })), '全部商品');
// 成片
buildMenu('project', uniqueValues('finals', 'project').map(v => ({ value: v, label: v })), '全部项目');
buildMenu('duration', [
{ value: '15s', label: '15 秒' },
{ value: '30s', label: '30 秒' },
{ value: '45s', label: '45 秒' },
{ value: '60s', label: '60 秒' },
], '全部时长');
// 我的上传
buildMenu('kind', [
{ value: '人物', label: '人物' },
{ value: '场景', label: '场景' },
{ value: '商品', label: '商品' },
], '全部类型');
// 通用·来源(按当前 tab 的实际数据动态构建)
function rebuildSourceMenu() {
const sources = uniqueValues(state.tab, 'source');
buildMenu('source', sources.map(v => ({ value: v, label: v })), '全部来源');
syncChipUI('source');
}
rebuildSourceMenu();
// 排序(无"全部",默认第一项选中)
const sortWrap = document.querySelector('.chip-wrap[data-key="sort"]');
sortWrap.querySelector('.chip-menu').innerHTML = Object.entries(SORT_LABEL).map(([v, l], i) =>
`<div class="mi${i === 0 ? ' selected' : ''}" data-value="${v}">${checkSvg}<span>${l}</span></div>`
).join('');
// ============== Apply ==============
function applyFilter() {
// 网格切换
TAB_KEYS.forEach(t => {
const g = document.getElementById(`grid-${t}`);
if (!g) return;
g.hidden = t !== state.tab;
});
// chip-wrap 按当前 tab 显隐
document.querySelectorAll('.chip-wrap').forEach(wrap => {
const tabs = (wrap.dataset.tabs || '').split(' ');
const show = tabs.includes('all') || tabs.includes(state.tab);
wrap.style.display = show ? '' : 'none';
});
// 过滤
const cards = cardsByTab[state.tab];
const q = state.search.toLowerCase();
let visible = [];
cards.forEach(c => {
let show = true;
if (q) {
const hay = `${c.dataset.name || ''} ${c.dataset.role || ''} ${c.dataset.sceneType || ''} ${c.dataset.product || ''} ${c.dataset.project || ''} ${c.dataset.source || ''}`.toLowerCase();
if (!hay.includes(q)) show = false;
}
// 通用 source
if (show && state.source !== 'all' && c.dataset.source !== state.source) show = false;
if (state.tab === 'people') {
if (show && state.gender !== 'all' && c.dataset.gender !== state.gender) show = false;
if (show && state.age !== 'all' && c.dataset.age !== state.age) show = false;
if (show && state.role !== 'all' && c.dataset.role !== state.role) show = false;
} else if (state.tab === 'scenes') {
if (show && state.sceneType !== 'all' && c.dataset.sceneType !== state.sceneType) show = false;
} else if (state.tab === 'products') {
if (show && state.product !== 'all' && c.dataset.product !== state.product) show = false;
} else if (state.tab === 'finals') {
if (show && state.project !== 'all' && c.dataset.project !== state.project) show = false;
if (show && state.duration !== 'all' && c.dataset.duration !== state.duration) show = false;
} else if (state.tab === 'uploads') {
if (show && state.kind !== 'all' && c.dataset.kind !== state.kind) show = false;
}
c.style.display = show ? '' : 'none';
if (show) visible.push(c);
});
// 排序
const sorters = {
'used-desc': (a, b) => +b.dataset.used - +a.dataset.used,
'added-desc': (a, b) => +b.dataset.added - +a.dataset.added,
'ref-desc': (a, b) => +b.dataset.used - +a.dataset.used,
};
visible.sort(sorters[state.sort] || sorters['used-desc']);
const grid = document.getElementById(`grid-${state.tab}`);
visible.forEach(c => grid.appendChild(c));
// 分页 · 在排序之后裁页, 把非当前页的卡片隐藏
const totalVisible = visible.length;
const totalPages = Math.max(1, Math.ceil(totalVisible / state.pageSize));
if (state.page > totalPages) state.page = totalPages;
if (state.page < 1) state.page = 1;
const start = (state.page - 1) * state.pageSize;
const end = start + state.pageSize;
visible.forEach((c, i) => {
if (i < start || i >= end) c.style.display = 'none';
});
// 计数 + 空状态
const total = cards.length;
document.getElementById('result-meta').innerHTML = `// 显示 <span class="count">${totalVisible}</span> / ${total} 个资产`;
const empty = document.getElementById('empty');
if (totalVisible === 0) {
empty.classList.add('show');
grid.style.display = 'none';
} else {
empty.classList.remove('show');
grid.style.display = '';
}
// 渲染分页器
renderPagination(totalVisible, totalPages);
// 是否有任意筛选生效 → 显示"清空筛选"
const activeKeys = visibleFilterKeys();
const hasFilter = state.search || activeKeys.some(k => state[k] !== 'all');
document.getElementById('clear-filters').hidden = !hasFilter;
}
// ============== 分页器渲染 ==============
function pageNumberList(cur, total) {
// 智能省略号 · 始终显示首末页 + cur 前后各 1 页
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
const pages = new Set([1, total, cur, cur - 1, cur + 1]);
if (cur <= 4) [2, 3, 4, 5].forEach(p => pages.add(p));
if (cur >= total - 3) [total - 4, total - 3, total - 2, total - 1].forEach(p => pages.add(p));
const sorted = [...pages].filter(p => p >= 1 && p <= total).sort((a, b) => a - b);
const out = [];
for (let i = 0; i < sorted.length; i++) {
if (i > 0 && sorted[i] - sorted[i - 1] > 1) out.push('…');
out.push(sorted[i]);
}
return out;
}
function renderPagination(totalVisible, totalPages) {
const root = document.getElementById('pagination');
if (!root) return;
// 空结果或只有一页且 ≤ pageSize → 不显示(数据不够分页时没必要占视觉)
if (totalVisible === 0 || (totalPages <= 1 && totalVisible <= state.pageSize)) {
root.hidden = true;
return;
}
root.hidden = false;
document.getElementById('page-total').textContent = totalVisible;
document.getElementById('page-size-label').textContent = `${state.pageSize} 条/页`;
document.getElementById('page-jump').value = state.page;
document.getElementById('page-jump').max = totalPages;
const list = document.getElementById('page-list');
const items = pageNumberList(state.page, totalPages);
let html = `<button type="button" data-page="prev" ${state.page <= 1 ? 'disabled' : ''} aria-label="上一页">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M10 12L6 8l4-4"/></svg>
</button>`;
items.forEach(p => {
if (p === '…') html += `<span class="ellipsis">…</span>`;
else html += `<button type="button" data-page="${p}" ${p === state.page ? 'class="active"' : ''}>${p}</button>`;
});
html += `<button type="button" data-page="next" ${state.page >= totalPages ? 'disabled' : ''} aria-label="下一页">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4l4 4-4 4"/></svg>
</button>`;
list.innerHTML = html;
}
function visibleFilterKeys() {
return [...document.querySelectorAll('.chip-wrap')]
.filter(w => {
const tabs = (w.dataset.tabs || '').split(' ');
return (tabs.includes('all') || tabs.includes(state.tab)) && w.dataset.key !== 'sort';
})
.map(w => w.dataset.key);
}
// ============== Sync chip UI ==============
function syncChipUI(key) {
const wrap = document.querySelector(`.chip-wrap[data-key="${key}"]`);
if (!wrap) return;
const label = wrap.querySelector('.chip-label');
const chip = wrap.querySelector('.chip');
const v = state[key];
if (key === 'sort') {
label.textContent = SORT_LABEL[v];
} else if (v === 'all') {
label.textContent = CHIP_DEFAULT_LABEL[key];
chip.classList.remove('active');
} else {
// 找到对应 mi 的 label 文字(优先用菜单里的展示文本)
const mi = wrap.querySelector(`.mi[data-value="${CSS.escape(v)}"] span`);
label.textContent = mi ? mi.textContent : v;
chip.classList.add('active');
}
wrap.querySelectorAll('.mi').forEach(mi => mi.classList.toggle('selected', mi.dataset.value === v));
}
// ============== Bind ==============
document.querySelectorAll('.chip-wrap').forEach(wrap => {
const key = wrap.dataset.key;
wrap.querySelector('.chip').addEventListener('click', e => {
e.stopPropagation();
const isOpen = wrap.classList.contains('open');
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
if (!isOpen) wrap.classList.add('open');
});
wrap.addEventListener('click', e => {
const mi = e.target.closest('.mi');
if (!mi) return;
e.stopPropagation();
state[key] = mi.dataset.value;
state.page = 1;
wrap.classList.remove('open');
syncChipUI(key);
applyFilter();
});
});
document.addEventListener('click', () => {
document.querySelectorAll('.chip-wrap.open').forEach(w => w.classList.remove('open'));
});
// Tab clicks
document.querySelectorAll('#asset-tabs .tab').forEach(t => {
t.addEventListener('click', () => {
document.querySelectorAll('#asset-tabs .tab').forEach(x => x.classList.remove('active'));
t.classList.add('active');
state.tab = t.dataset.tab;
state.page = 1;
// 切 tab 时,清空本 tab 不再可见的筛选
['gender', 'age', 'role', 'sceneType', 'product', 'project', 'duration', 'kind', 'source'].forEach(k => {
const wrap = document.querySelector(`.chip-wrap[data-key="${k}"]`);
if (!wrap) return;
const tabs = (wrap.dataset.tabs || '').split(' ');
const visible = tabs.includes('all') || tabs.includes(state.tab);
if (!visible) { state[k] = 'all'; syncChipUI(k); }
});
// 来源选项随 tab 数据重新构建
state.source = 'all';
rebuildSourceMenu();
applyFilter();
});
});
// 搜索
document.getElementById('search-input').addEventListener('input', e => {
state.search = e.target.value.trim();
state.page = 1;
applyFilter();
});
// 清空筛选
document.getElementById('clear-filters').addEventListener('click', () => {
state.search = '';
document.getElementById('search-input').value = '';
['gender', 'age', 'role', 'sceneType', 'product', 'project', 'duration', 'kind', 'source'].forEach(k => {
state[k] = 'all';
syncChipUI(k);
});
state.page = 1;
applyFilter();
Shell.toast('已清空筛选');
});
// 分页器 · 翻页按钮(事件委托)
document.getElementById('page-list').addEventListener('click', e => {
const btn = e.target.closest('button[data-page]');
if (!btn || btn.disabled) return;
const v = btn.dataset.page;
const totalPages = +document.getElementById('page-jump').max || 1;
if (v === 'prev') state.page = Math.max(1, state.page - 1);
else if (v === 'next') state.page = Math.min(totalPages, state.page + 1);
else state.page = +v;
applyFilter();
// 翻页后滚到顶部, 让首张卡片立刻可见
window.scrollTo({ top: 0, behavior: 'smooth' });
});
// 分页器 · 每页条数(循环切换 12 → 24 → 48 → 96 → 12)
document.getElementById('page-size-btn').addEventListener('click', () => {
const i = PAGE_SIZES.indexOf(state.pageSize);
state.pageSize = PAGE_SIZES[(i + 1) % PAGE_SIZES.length];
state.page = 1;
applyFilter();
});
// 分页器 · 跳转
const _jumpEl = document.getElementById('page-jump');
function _doJump() {
let v = parseInt(_jumpEl.value, 10);
const max = +_jumpEl.max || 1;
if (!Number.isFinite(v)) v = 1;
v = Math.max(1, Math.min(max, v));
state.page = v;
applyFilter();
}
_jumpEl.addEventListener('change', _doJump);
_jumpEl.addEventListener('blur', _doJump);
_jumpEl.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); _doJump(); _jumpEl.blur(); }
});
applyFilter();
// ============== 上传资产 Modal ==============
// 修复:把 modal 提到 <body> 末尾,脱离 main 的 stacking context 干扰
// (main 是 position:relative + overflow:hidden, topbar z-index:2 会盖在 fixed modal-bg 之上)
const _modalRoot = document.getElementById('upload-modal-bg');
if (_modalRoot && _modalRoot.parentElement !== document.body) {
document.body.appendChild(_modalRoot);
}
const uploadState = { kind: 'people', file: null, dataUrl: '', mime: '' };
const KIND_LABEL = { people: '人物', scenes: '场景', products: '商品图', finals: '成片' };
const DEFAULT_THUMB_TEXT = {
people: '新资产', scenes: '新场景', products: '新商品图', finals: '9:16 · 新成片'
};
const FILE_HINT = {
image: '// JPG / PNG / WEBP · 单文件',
video: '// MP4 / WEBM · 9:16 · ≤ 60 秒',
};
const $ = (id) => document.getElementById(id);
const modalBg = $('upload-modal-bg');
const kindSel = $('upload-kind');
const zone = $('upload-zone');
const zoneText = $('upload-zone-text');
const zoneHint = $('upload-zone-hint');
const fileInput = $('upload-file');
const preview = $('upload-preview');
const previewImg = $('upload-preview-img');
const previewVideo = $('upload-preview-video');
const previewX = $('upload-preview-x');
const nameInput = $('upload-name');
const submitBtn = $('upload-submit');
function syncKindFields() {
document.querySelectorAll('.upload-modal .field[data-fields]').forEach(f => {
f.hidden = f.dataset.fields !== uploadState.kind;
});
const isVideo = uploadState.kind === 'finals';
fileInput.accept = isVideo ? 'video/*' : 'image/*';
zoneText.textContent = isVideo ? '点击或拖拽上传视频' : '点击或拖拽上传图片';
zoneHint.textContent = isVideo ? FILE_HINT.video : FILE_HINT.image;
preview.classList.toggle('video', isVideo);
}
function syncSubmit() {
let ok = !!uploadState.file && nameInput.value.trim().length > 0;
if (ok && uploadState.kind === 'scenes' && !$('upload-scene-type').value.trim()) ok = false;
if (ok && uploadState.kind === 'products' && !$('upload-product').value) ok = false;
submitBtn.disabled = !ok;
}
function resetUploadModal() {
uploadState.file = null;
uploadState.dataUrl = '';
uploadState.mime = '';
fileInput.value = '';
preview.hidden = true;
previewImg.removeAttribute('src');
previewVideo.removeAttribute('src');
previewVideo.hidden = true;
previewImg.hidden = false;
zone.hidden = false;
nameInput.value = '';
$('upload-role').value = '';
$('upload-scene-type').value = '';
$('upload-product').selectedIndex = 0;
$('upload-project').value = '';
$('upload-gender').selectedIndex = 0;
$('upload-age').value = '青年';
$('upload-duration').value = '60s';
syncSubmit();
}
function handleFile(file) {
if (!file) return;
const isVideo = uploadState.kind === 'finals';
if (isVideo && !file.type.startsWith('video/')) {
Shell.toast('请上传视频文件', file.type || 'unknown');
return;
}
if (!isVideo && !file.type.startsWith('image/')) {
Shell.toast('请上传图片文件', file.type || 'unknown');
return;
}
uploadState.file = file;
uploadState.mime = file.type;
const reader = new FileReader();
reader.onload = (e) => {
uploadState.dataUrl = e.target.result;
if (isVideo) {
previewVideo.src = uploadState.dataUrl;
previewVideo.hidden = false;
previewImg.hidden = true;
} else {
previewImg.src = uploadState.dataUrl;
previewImg.hidden = false;
previewVideo.hidden = true;
}
preview.hidden = false;
zone.hidden = true;
// 自动填名称(去后缀)
if (!nameInput.value) nameInput.value = file.name.replace(/\.[^.]+$/, '');
syncSubmit();
};
reader.readAsDataURL(file);
}
kindSel.addEventListener('change', () => {
uploadState.kind = kindSel.value;
resetUploadModal();
syncKindFields();
});
zone.addEventListener('click', () => fileInput.click());
zone.addEventListener('dragover', e => { e.preventDefault(); 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) handleFile(e.dataTransfer.files[0]);
});
fileInput.addEventListener('change', e => {
if (e.target.files?.length) handleFile(e.target.files[0]);
});
previewX.addEventListener('click', () => {
uploadState.file = null;
uploadState.dataUrl = '';
fileInput.value = '';
preview.hidden = true;
zone.hidden = false;
previewImg.removeAttribute('src');
previewVideo.removeAttribute('src');
syncSubmit();
});
nameInput.addEventListener('input', syncSubmit);
$('upload-scene-type').addEventListener('input', syncSubmit);
$('upload-product').addEventListener('change', syncSubmit);
// 提交 → 构造卡片插入到对应 grid 首位
submitBtn.addEventListener('click', () => {
if (submitBtn.disabled) return;
const kind = uploadState.kind;
const name = nameInput.value.trim();
const today = new Date();
const stamp = `${today.getFullYear()}${String(today.getMonth() + 1).padStart(2, '0')}${String(today.getDate()).padStart(2, '0')}`;
const card = document.createElement('div');
card.className = 'asset-card' + (kind === 'finals' ? ' video' : '');
card.dataset.name = name;
card.dataset.source = '手动上传';
card.dataset.used = '0';
card.dataset.added = stamp;
card.setAttribute('onclick', `Shell.toast('查看资产', ${JSON.stringify(name)})`);
let metaText = '';
if (kind === 'people') {
const gender = $('upload-gender').value;
const age = $('upload-age').value;
const role = $('upload-role').value.trim() || '—';
card.dataset.gender = gender;
card.dataset.age = age;
card.dataset.role = role;
metaText = `${gender} · ${age} · ${role} · 手动上传`;
} else if (kind === 'scenes') {
const sceneType = $('upload-scene-type').value.trim();
card.dataset.sceneType = sceneType;
metaText = `${sceneType} · 手动上传 · 用过 0 次`;
} else if (kind === 'products') {
const product = $('upload-product').value;
card.dataset.product = product;
metaText = `${product} · 手动上传 · 用过 0 次`;
} else if (kind === 'finals') {
const project = $('upload-project').value.trim() || name;
const duration = $('upload-duration').value;
card.dataset.project = project;
card.dataset.duration = duration;
metaText = `${project} · ${duration} · 手动上传`;
}
// 缩略图:有 dataUrl 用真实预览,否则占位
const thumb = document.createElement('div');
thumb.className = 'placeholder asset-thumb';
if (uploadState.dataUrl) {
if (kind === 'finals') {
thumb.innerHTML = `<video src="${uploadState.dataUrl}" muted playsinline style="width:100%;height:100%;object-fit:cover;display:block;border-radius:inherit;"></video>`;
} else {
thumb.style.backgroundImage = `url("${uploadState.dataUrl}")`;
thumb.style.backgroundSize = 'cover';
thumb.style.backgroundPosition = 'center';
thumb.innerHTML = '';
}
} else {
thumb.innerHTML = `<span class="ph-frame">${DEFAULT_THUMB_TEXT[kind]}</span>`;
}
const body = document.createElement('div');
body.className = 'asset-body';
body.innerHTML = `<div class="asset-name">${name}</div><div class="asset-meta">${metaText}</div>`;
card.appendChild(thumb);
card.appendChild(body);
// 插入 grid 首位 + 更新 cardsByTab + 计数 + 切到该 tab
const targetGrid = $(`grid-${kind}`);
targetGrid.prepend(card);
cardsByTab[kind].unshift(card);
// 刷新 tab count + 副标题计数
const tabCount = document.querySelector(`.tab[data-tab="${kind}"] .count`);
if (tabCount) tabCount.textContent = cardsByTab[kind].length;
if (kind === 'people') $('sub-people').textContent = cardsByTab.people.length;
if (kind === 'scenes') $('sub-scenes').textContent = cardsByTab.scenes.length;
if (kind === 'products') $('sub-products').textContent = cardsByTab.products.length;
if (kind === 'finals') $('sub-finals').textContent = cardsByTab.finals.length;
if (sidebarBadge) {
const total = TAB_KEYS.reduce((s, t) => s + cardsByTab[t].length, 0);
sidebarBadge.textContent = total;
}
// 角色标签来源新值 → 重建角色菜单(可能新增)
if (kind === 'people') {
buildMenu('role', uniqueValues('people', 'role').map(v => ({ value: v, label: v })), '全部角色');
syncChipUI('role');
}
if (kind === 'scenes') {
buildMenu('sceneType', uniqueValues('scenes', 'sceneType').map(v => ({ value: v, label: v })), '全部场景');
syncChipUI('sceneType');
}
if (kind === 'products') {
buildMenu('product', uniqueValues('products', 'product').map(v => ({ value: v, label: v })), '全部商品');
syncChipUI('product');
}
if (kind === 'finals') {
buildMenu('project', uniqueValues('finals', 'project').map(v => ({ value: v, label: v })), '全部项目');
syncChipUI('project');
}
// 切到目标 tab + 清空筛选
document.querySelectorAll('#asset-tabs .tab').forEach(t =>
t.classList.toggle('active', t.dataset.tab === kind)
);
state.tab = kind;
state.search = ''; $('search-input').value = '';
['gender', 'age', 'role', 'sceneType', 'product', 'project', 'duration', 'source'].forEach(k => {
state[k] = 'all';
syncChipUI(k);
});
state.source = 'all';
state.page = 1;
rebuildSourceMenu();
applyFilter();
Shell.closeModal('upload-modal-bg');
Shell.toast(`已上传到${KIND_LABEL[kind]}`, `+ ${name}`);
});
// 打开按钮
$('open-upload-btn').addEventListener('click', () => {
resetUploadModal();
kindSel.value = state.tab in KIND_LABEL ? state.tab : 'people';
uploadState.kind = kindSel.value;
syncKindFields();
Shell.openModal('upload-modal-bg');
setTimeout(() => nameInput.focus(), 100);
});
// ESC 关闭
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && modalBg.classList.contains('show')) {
Shell.closeModal('upload-modal-bg');
}
});
// 初始化
syncKindFields();
// ============================================================
// 资产删除 + 批量管理 (PRD §6.3 软删除/引用检查)
// ============================================================
// 模拟引用记录: 某些资产被项目引用 (实际从后台获取)
const ASSET_REFS = {
'林夕': ['夏日水嫩计划', '春装新品'],
'小七': ['学生季推广'],
'卧室·暖光': ['补水面膜 v1', '面膜对比'],
'客厅·北欧': ['咖啡早八剧情'],
'蓝牙耳机 · 开箱测评': ['南卡 Lite 推广']
};
function getAssetRefs(card) {
const name = card.dataset.name || '';
return ASSET_REFS[name] || [];
}
const delBg = document.getElementById('del-confirm-bg');
const delBody = document.getElementById('del-confirm-body');
const delFoot = document.getElementById('del-confirm-foot');
const delCancel = document.getElementById('del-confirm-cancel');
const delOk = document.getElementById('del-confirm-ok');
let _delQueue = [];
function setFootDeletable() {
delFoot.innerHTML = '';
delFoot.appendChild(delCancel);
delFoot.appendChild(delOk);
}
function setFootBlocked() {
delFoot.innerHTML = '';
const okBtn = document.createElement('button');
okBtn.className = 'btn btn-primary';
okBtn.type = 'button';
okBtn.textContent = '我知道了';
okBtn.addEventListener('click', closeDelConfirm);
delFoot.appendChild(okBtn);
}
function openDelConfirm(targets) {
const blocked = targets.filter(c => getAssetRefs(c).length > 0);
const deletable = targets.filter(c => getAssetRefs(c).length === 0);
if (deletable.length === 0 && blocked.length > 0) {
const c = blocked[0];
const refs = getAssetRefs(c);
if (blocked.length === 1) {
delBody.innerHTML = `<span class="mono-acc">${c.dataset.name}</span> 当前被 <b>${refs.length}</b> 个项目使用,无法直接删除。请先在以下项目中解除引用:<br><br>` +
refs.map(r => `<span class="mono-acc" style="margin-right:6px">${r}</span>`).join('');
} else {
delBody.innerHTML = `所选 <b>${blocked.length}</b> 个资产均被项目引用,无法直接删除。`;
}
setFootBlocked();
delBg.classList.add('show');
_delQueue = [];
return;
}
_delQueue = deletable;
if (deletable.length === 1 && blocked.length === 0) {
delBody.innerHTML = '即将删除 <span class="mono-acc">' + deletable[0].dataset.name + '</span>,此操作无法撤销。';
} else if (blocked.length > 0) {
delBody.innerHTML = '即将删除 <span class="mono-acc">' + deletable.length + ' 个资产</span>,其中 <b>' + blocked.length + '</b> 个被项目引用已跳过。';
} else {
delBody.innerHTML = '即将删除 <span class="mono-acc">' + deletable.length + ' 个资产</span>,此操作无法撤销。';
}
setFootDeletable();
delBg.classList.add('show');
}
function closeDelConfirm() { delBg.classList.remove('show'); _delQueue = []; }
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();
});
// 单卡片删除按钮
document.querySelectorAll('.asset-card .card-del-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const card = btn.closest('.asset-card');
if (!card) return;
openDelConfirm([card]);
});
});
// 管理资产模式
const libManageBtn = document.getElementById('lib-manage-btn');
const libManageLabel = libManageBtn.querySelector('.lib-manage-label');
const bulkBar = document.getElementById('bulk-bar');
const bulkCount = document.getElementById('bulk-count');
const bulkClear = document.getElementById('bulk-clear');
const bulkDel = document.getElementById('bulk-del');
const bulkExit = document.getElementById('bulk-exit');
function getSelected() { return [...document.querySelectorAll('.asset-card.selected')]; }
function updateBulkBar() {
const sel = getSelected();
bulkCount.textContent = sel.length;
bulkDel.disabled = sel.length === 0;
bulkDel.style.opacity = sel.length === 0 ? '.4' : '1';
}
function enterEditMode() {
document.body.classList.add('edit-mode');
libManageBtn.classList.add('active');
libManageLabel.textContent = '完成';
applyDraggableToCards(true);
updateBulkBar();
}
function exitEditMode() {
document.body.classList.remove('edit-mode');
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();
else enterEditMode();
});
bulkExit.addEventListener('click', exitEditMode);
bulkClear.addEventListener('click', () => {
document.querySelectorAll('.asset-card.selected').forEach(c => c.classList.remove('selected'));
updateBulkBar();
});
bulkDel.addEventListener('click', () => {
const sel = getSelected();
if (!sel.length) return;
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 => {
if (!document.body.classList.contains('edit-mode')) return;
e.stopImmediatePropagation();
e.preventDefault();
card.classList.toggle('selected');
updateBulkBar();
}, true);
});
/* ============================================================
资产详情 modal · 与 pipeline.html 共用参考布局 v2
============================================================ */
(function () {
// 注入 modal CSS (与 pipeline.html 保持一致的 .asset-* 命名)
const css = `
.asset-modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 40px; }
.asset-modal-bg.show { display: flex; }
.asset-modal { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: min(1040px, 100%); max-height: calc(100vh - 80px); overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 16px 48px rgba(0,0,0,.18); }
.asset-modal-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }
.asset-modal-h h2 { font-size: 15px; font-weight: 600; }
.asset-modal-h .ad-tag { font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-48); letter-spacing: .02em; }
.asset-modal-h .x { width: 30px; height: 30px; display: grid; place-items: center; background: transparent; border: 0; cursor: pointer; color: var(--black-alpha-56); border-radius: var(--r-sm); margin-left: auto; }
.asset-modal-h .x:hover { background: var(--black-alpha-8); color: var(--accent-black); }
.asset-modal-body { padding: 20px 24px 24px; overflow-y: auto; flex: 1; }
.asset-detail-grid { display: grid; grid-template-columns: 340px 1fr; gap: 24px; }
.asset-detail-lead { display: flex; flex-direction: column; gap: 10px; }
.asset-detail-lead .ad-lead-wrap { position: relative; }
.asset-detail-lead .placeholder.ad-lead-img { aspect-ratio: 3/4; border-radius: var(--r-md); }
.asset-detail-lead .ad-zoom-btn { position: absolute; right: 10px; bottom: 10px; height: 28px; padding: 0 12px; background: rgba(21,20,15,.7); color: #fff; border: 0; border-radius: var(--r-pill); display: inline-flex; align-items: center; gap: 4px; font-size: 11.5px; font-family: inherit; cursor: pointer; }
.asset-detail-lead .ad-zoom-btn:hover { background: rgba(21,20,15,.9); }
.asset-detail-lead .ad-zoom-btn svg { width: 12px; height: 12px; }
.asset-detail-lead .ad-thumbs { display: flex; gap: 8px; }
.asset-detail-lead .ad-thumbs .thumb { flex: 0 0 64px; aspect-ratio: 3/4; border-radius: var(--r-sm); border: 1px solid var(--border-faint); cursor: pointer; overflow: hidden; }
.asset-detail-lead .ad-thumbs .thumb:hover { border-color: var(--heat-40); }
.asset-detail-lead .ad-thumbs .thumb.active { border-color: var(--heat); border-width: 2px; }
.asset-detail-right .ad-section + .ad-section { margin-top: 18px; }
.asset-detail-section-h { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: var(--accent-black); margin-bottom: 10px; }
.asset-detail-section-h .ic { width: 14px; height: 14px; color: var(--heat); display: grid; place-items: center; }
.asset-detail-section-h .ic svg { width: 14px; height: 14px; }
.asset-detail-section-h .ad-ratio-chip { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; padding: 2px 8px; border-radius: var(--r-sm); background: var(--background-lighter); border: 1px solid var(--border-faint); color: var(--black-alpha-56); }
.asset-detail-section-h .ad-icon-btn { width: 28px; height: 28px; display: grid; place-items: center; background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer; }
.asset-detail-section-h .ad-icon-btn:hover { color: var(--heat); border-color: var(--heat-40); }
.asset-detail-section-h .ad-icon-btn svg { width: 12px; height: 12px; }
.asset-detail-tri-row .placeholder { aspect-ratio: 16/9; border-radius: var(--r-md); }
.asset-detail-tri-row .placeholder.missing { display: grid; place-items: center; border: 1px dashed var(--border-faint); background: var(--background-lighter); color: var(--black-alpha-48); font-family: var(--font-mono); font-size: 12px; padding: 12px; text-align: center; cursor: pointer; gap: 8px; }
.asset-detail-tri-row .placeholder.missing:hover { border-color: var(--heat); color: var(--heat); }
.ad-intro { font-size: 13px; line-height: 1.65; color: var(--black-alpha-72); margin: 0 0 12px; }
.ad-tags { display: flex; flex-wrap: wrap; gap: 8px; }
.ad-tags .ad-tag-chip { height: 26px; padding: 0 12px; display: inline-flex; align-items: center; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); font-size: 12px; color: var(--accent-black); }
.ad-tags .ad-tag-add { width: 26px; height: 26px; display: grid; place-items: center; background: var(--background-lighter); border: 1px dashed var(--black-alpha-24); border-radius: var(--r-sm); color: var(--black-alpha-56); cursor: pointer; }
.ad-tags .ad-tag-add:hover { border-color: var(--heat); color: var(--heat); }
.ad-tags .ad-tag-add svg { width: 12px; height: 12px; }
.ad-props { margin-top: 18px; display: grid; grid-template-columns: repeat(3, 1fr); column-gap: 24px; row-gap: 0; border-top: 1px solid var(--border-faint); padding-top: 16px; }
.ad-props .ad-prop { display: flex; align-items: baseline; padding: 10px 0; border-bottom: 1px solid var(--border-faint); font-size: 12.5px; min-height: 38px; }
.ad-props .ad-prop:nth-last-child(-n+3) { border-bottom: 0; }
.ad-props .ad-prop .k { flex: 0 0 64px; color: var(--black-alpha-56); font-family: var(--font-mono); font-size: 11px; }
.ad-props .ad-prop .v { color: var(--accent-black); font-weight: 500; word-break: break-all; }
.asset-detail-tip { margin-top: 10px; padding: 10px 12px; background: var(--heat-12); border: 1px solid var(--heat-20); border-radius: var(--r-sm); font-size: 12px; color: var(--accent-black); display: flex; align-items: center; gap: 8px; line-height: 1.5; }
.asset-detail-tip svg { width: 14px; height: 14px; color: var(--heat); flex-shrink: 0; }
.asset-detail-tip .ai-gen-btn { margin-left: auto; height: 26px; padding: 0 10px; background: var(--heat); color: #fff; border: 1px solid var(--heat); border-radius: var(--r-sm); font-size: 11.5px; cursor: pointer; font-family: inherit; flex-shrink: 0; }
.asset-modal-f { padding: 14px 20px; border-top: 1px solid var(--border-faint); display: flex; align-items: center; gap: 8px; }
.asset-modal-f .ad-foot-stats { display: flex; gap: 6px; margin-right: auto; }
.asset-modal-f .ad-stat-btn { height: 32px; padding: 0 12px; display: inline-flex; align-items: center; gap: 6px; background: transparent; border: 1px solid var(--border-faint); border-radius: var(--r-sm); color: var(--black-alpha-72); font-size: 12.5px; font-family: inherit; cursor: pointer; }
.asset-modal-f .ad-stat-btn:hover { background: var(--background-lighter); color: var(--accent-black); border-color: var(--black-alpha-24); }
.asset-modal-f .ad-stat-btn svg { width: 13px; height: 13px; }
.asset-modal-f .ad-stat-btn b { color: var(--accent-black); font-weight: 600; }
`;
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
const modalHTML = `
<div class="asset-modal-bg" id="lib-detail-bg">
<div class="asset-modal">
<div class="asset-modal-h">
<h2 id="lib-detail-title">资产详情</h2>
<span class="ad-tag" id="lib-detail-kind">/ kind</span>
<button class="x" id="lib-detail-x" type="button" aria-label="关闭"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M6 18L18 6"/></svg></button>
</div>
<div class="asset-modal-body">
<div class="asset-detail-grid">
<div class="asset-detail-lead">
<div class="ad-lead-wrap">
<div class="placeholder ad-lead-img" id="lib-detail-lead-img"><span class="ph-frame">立绘 / 主图</span></div>
<button class="ad-zoom-btn" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5"/></svg>
查看大图
</button>
</div>
<div class="ad-thumbs" id="lib-detail-thumbs"></div>
</div>
<div class="asset-detail-right">
<div class="ad-section" id="lib-detail-tri-section">
<div class="asset-detail-section-h">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></span>
<span class="t">三视图</span>
<span class="ad-ratio-chip" id="lib-detail-ratio">16:9</span>
<button class="ad-icon-btn" type="button" title="下载"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
</div>
<div class="asset-detail-tri-row" id="lib-detail-tri">
<div class="placeholder"><span class="ph-frame">正 / 侧 / 背 · 三视图</span></div>
</div>
<div class="asset-detail-tip" id="lib-detail-tip" style="display:none;">
<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 8v4M12 16h.01"/></svg>
<span>暂无三视图,建议用 AI 生成以保证多角度一致性</span>
<button class="ai-gen-btn" type="button" id="lib-detail-aigen">AI 生成三视图</button>
</div>
</div>
<div class="ad-section">
<div class="asset-detail-section-h">
<span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h10"/></svg></span>
<span class="t">简介</span>
</div>
<p class="ad-intro" id="lib-detail-intro"></p>
<div class="ad-tags" id="lib-detail-tags"></div>
</div>
<div class="ad-props" id="lib-detail-props"></div>
</div>
</div>
</div>
<div class="asset-modal-f">
<div class="ad-foot-stats">
<button class="ad-stat-btn" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
下载
</button>
</div>
<button class="btn btn-primary" type="button" id="lib-detail-apply">使用该资产</button>
</div>
</div>
</div>`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
const bg = document.getElementById('lib-detail-bg');
const titleEl = document.getElementById('lib-detail-title');
const kindEl = document.getElementById('lib-detail-kind');
const leadImg = document.getElementById('lib-detail-lead-img');
const thumbsEl = document.getElementById('lib-detail-thumbs');
const triSection = document.getElementById('lib-detail-tri-section');
const triEl = document.getElementById('lib-detail-tri');
const ratioChip = document.getElementById('lib-detail-ratio');
const introEl = document.getElementById('lib-detail-intro');
const tagsEl = document.getElementById('lib-detail-tags');
const propsEl = document.getElementById('lib-detail-props');
const tipEl = document.getElementById('lib-detail-tip');
function _hash(s) { let h = 0; for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0; return Math.abs(h); }
function _fmtAssetId(name, k) { return 'ASSET-20240520-' + (k === 'person' ? 'M' : k === 'scene' ? 'S' : 'P') + String(_hash(name) % 1000).padStart(3, '0'); }
function _fmtSize(name) { return (4 + (_hash(name) % 100) / 10).toFixed(1) + 'MB'; }
function _fmtFav(name) { return String(8 + _hash(name) % 80); }
function _fmtDl(name) { const n = 200 + _hash(name) % 1800; return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); }
function open(card) {
const name = card.dataset.name || '资产';
const used = card.dataset.used || '0';
const source = card.dataset.source || '平台预设';
let tagText = 'AI 素材', intro = '', tags = [], props = [], hasTri = false, isActor = false;
if (card.dataset.gender) {
tagText = '人物 · 模特';
isActor = true; hasTri = true;
intro = (card.dataset.role || '人物模特') + ' · 可用于本项目人物资产生成';
tags = [card.dataset.gender, card.dataset.age, card.dataset.role].filter(Boolean);
props = [
['性别', card.dataset.gender || '-'], ['种族', '东亚'], ['作品ID', _fmtAssetId(name, 'person')],
['年龄段', card.dataset.age || '-'], ['角色', card.dataset.role || '-'], ['创作人', '流·Studio'],
['身高', '中等'], ['来源', source], ['文件大小', _fmtSize(name)],
['使用次数', used + ' 次'], ['授权', '商用'], ['发布时间', '2024-05-20'],
];
} else if (card.dataset.sceneType) {
tagText = '场景 · ' + card.dataset.sceneType;
isActor = false; hasTri = false;
intro = card.dataset.sceneType + ' 场景资产';
tags = [card.dataset.sceneType, source].filter(Boolean);
props = [
['场景类型', card.dataset.sceneType], ['来源', source], ['作品ID', _fmtAssetId(name, 'scene')],
['镜头', '通用'], ['光线', '自然光'], ['创作人', '流·Studio'],
['用途', '本项目场景资产'], ['使用次数', used + ' 次'], ['文件大小', _fmtSize(name)],
];
} else if (card.dataset.product) {
tagText = '商品资产';
isActor = false;
intro = '关联商品: ' + card.dataset.product;
tags = ['商品', source].filter(Boolean);
props = [
['关联商品', card.dataset.product], ['来源', source], ['作品ID', _fmtAssetId(name, 'product')],
['用途', '商品资产'], ['使用次数', used + ' 次'], ['创作人', '流·Studio'],
['授权', '商用'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],
];
} else {
tagText = 'AI 素材';
isActor = false;
intro = name;
tags = [source].filter(Boolean);
props = [
['名称', name], ['来源', source], ['作品ID', _fmtAssetId(name, 'asset')],
['创作人', '流·Studio'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],
];
}
titleEl.textContent = name;
kindEl.textContent = '/ ' + tagText;
leadImg.innerHTML = '<span class="ph-frame">' + name + '</span>';
thumbsEl.innerHTML = ['v1','v2','v3'].map((t, i) => `<div class="thumb placeholder${i === 0 ? ' active' : ''}"><span class="ph-frame">${t}</span></div>`).join('');
thumbsEl.querySelectorAll('.thumb').forEach(t => t.addEventListener('click', () => {
thumbsEl.querySelectorAll('.thumb').forEach(x => x.classList.remove('active'));
t.classList.add('active');
}));
if (card.dataset.sceneType) {
triSection.style.display = 'none';
} else if (isActor) {
triSection.style.display = '';
triEl.classList.remove('actor');
triEl.innerHTML = '<div class="placeholder"><span class="ph-frame">' + name + ' · 三视图 (正/侧/背)</span></div>';
ratioChip.textContent = '16:9';
tipEl.style.display = 'none';
} else {
triSection.style.display = '';
triEl.classList.remove('actor');
ratioChip.textContent = '16:9';
if (hasTri) {
triEl.innerHTML = '<div class="placeholder"><span class="ph-frame">' + name + ' · 三视图</span></div>';
tipEl.style.display = 'none';
} else {
triEl.innerHTML = '<div class="placeholder missing" data-tri="0"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg><span>暂未生成三视图(16:9 单图)</span></div>';
tipEl.style.display = 'flex';
}
}
introEl.textContent = intro || '暂无简介';
tagsEl.innerHTML = tags.map(t => '<span class="ad-tag-chip">' + t + '</span>').join('') +
'<button class="ad-tag-add" type="button" title="添加标签"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg></button>';
propsEl.innerHTML = props.map(([k, v]) => '<div class="ad-prop"><span class="k">' + k + '</span><span class="v">' + v + '</span></div>').join('');
bg.classList.add('show');
}
function close() { bg.classList.remove('show'); }
bg.addEventListener('click', e => { if (e.target === bg) close(); });
document.getElementById('lib-detail-x').addEventListener('click', close);
document.getElementById('lib-detail-apply').addEventListener('click', () => {
Shell.toast('已应用「' + titleEl.textContent + '」', '已加入当前项目');
close();
});
document.getElementById('lib-detail-aigen').addEventListener('click', () => {
Shell.toast('AI 生成三视图中', '约 12s · POST /assets/tri-view');
});
// 把所有 .asset-card 的旧 onclick="Shell.toast(...)" 清掉,改成 open(card)
document.querySelectorAll('.asset-card').forEach(card => {
if (card.onclick) card.onclick = null;
card.removeAttribute('onclick');
card.style.cursor = 'pointer';
card.addEventListener('click', e => {
if (document.body.classList.contains('edit-mode')) return; // 编辑模式由更早 capture handler 处理
if (e.target.closest('.card-del-btn')) return;
open(card);
});
});
})();
</script>
</body>
</html>