AirShelf/电商AI平台/library.html
iye bbe29622c2
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 4m39s
Polish static UI flows
2026-05-28 12:29:12 +08:00

2484 lines
149 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>资产库 · Airshelf</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/restraint.css?v=2026052607">
<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" aria-hidden="true"><path d="m3 7 2 2 4-4"/><path d="m3 17 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></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.5" 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.5" 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.5" 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.5" 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.5" 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="1.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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="1.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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="1.5" 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.5" 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.5" 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.5" 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.5" 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.5" 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/icons.js?v=2026052608"></script>
<script src="assets/shell.js?v=2026052607"></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.5" 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.5" 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.5" 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.5" 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.5" 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); }
/* 查看大图 icon · 悬浮容器才显示 · 32×32 icon-only */
.ad-zoom-btn { position: absolute; right: 8px; bottom: 8px; width: 32px; height: 32px; padding: 0; background: rgba(21,20,15,.7); color: #fff; border: 0; border-radius: var(--r-sm); display: grid; place-items: center; cursor: pointer; backdrop-filter: blur(4px); opacity: 0; transition: opacity var(--t-base), background var(--t-base); z-index: 3; }
.ad-zoom-btn:hover { background: rgba(21,20,15,.92); }
.ad-zoom-btn svg { width: 14px; height: 14px; }
.asset-detail-lead .ad-lead-wrap:hover .ad-zoom-btn,
.asset-detail-tri-row .placeholder:hover .ad-zoom-btn { opacity: 1; }
.asset-detail-tri-row .placeholder { position: relative; }
.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; display: inline-flex; align-items: center; }
.asset-detail-tip .ai-gen-btn:disabled { opacity: .55; cursor: not-allowed; }
.asset-detail-tip.is-loading svg { animation: ad-spin 1s linear infinite; }
@keyframes ad-spin { to { transform: rotate(360deg); } }
.asset-detail-history { margin-top: 10px; padding: 10px 12px; background: var(--background-lighter); border: 1px solid var(--border-faint); border-radius: var(--r-sm); }
.asset-detail-history .adh-h { font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-56); margin-bottom: 8px; letter-spacing: .02em; }
.asset-detail-history .adh-h .adh-cur { color: var(--heat); }
.asset-detail-history .adh-row { display: flex; gap: 8px; flex-wrap: wrap; }
.asset-detail-history .adh-thumb { width: 64px; height: 36px; border-radius: var(--r-sm); background: var(--background-base); border: 1px solid var(--border-faint); display: grid; place-items: center; font-family: var(--font-mono); font-size: 11px; color: var(--black-alpha-72); cursor: pointer; position: relative; transition: border-color var(--t-base), color var(--t-base); }
.asset-detail-history .adh-thumb:hover { border-color: var(--heat-40); color: var(--heat); }
.asset-detail-history .adh-thumb.active { border-color: var(--heat); color: var(--heat); background: var(--heat-12); }
.asset-detail-history .adh-thumb.active::after { content: ""; position: absolute; top: -3px; right: -3px; width: 8px; height: 8px; background: var(--heat); border: 2px solid var(--surface); border-radius: 50%; }
.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; }
/* ── 缺保存 · 二次确认弹窗(模仿 model-photo .mc-leave) ── */
.lib-confirm-bg { position: fixed; inset: 0; z-index: 1300; background: rgba(21,20,15,.42); display: none; align-items: center; justify-content: center; padding: 40px; }
.lib-confirm-bg.show { display: flex; }
.lib-confirm { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: 440px; max-width: 92vw; box-shadow: 0 24px 64px rgba(0,0,0,.16); overflow: hidden; }
.lib-confirm .lc-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px 10px; }
.lib-confirm .lc-h .ic { width: 28px; height: 28px; display: grid; place-items: center; background: var(--heat-12); color: var(--heat); border-radius: var(--r-sm); flex-shrink: 0; }
.lib-confirm .lc-h .ic svg { width: 16px; height: 16px; }
.lib-confirm .lc-h h3 { font-size: 15px; font-weight: 600; color: var(--accent-black); margin: 0; }
.lib-confirm .lc-h .mono { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; color: var(--black-alpha-48); letter-spacing: .04em; }
.lib-confirm .lc-b { padding: 4px 20px 18px; font-size: 13px; line-height: 1.65; color: var(--black-alpha-72); }
.lib-confirm .lc-b b { color: var(--accent-black); font-weight: 600; }
.lib-confirm .lc-f { display: flex; align-items: center; gap: 8px; padding: 12px 20px; border-top: 1px solid var(--border-faint); background: var(--background-lighter); }
.lib-confirm .lc-f .spacer { flex: 1; }
.lib-confirm .lc-f .btn { height: 34px; padding: 0 14px; font-size: 13px; }
`;
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" id="lib-detail-lead-zoom" aria-label="查看大图" title="查看大图">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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.5" 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.5" 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.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>
<span id="lib-detail-tip-text">暂无三视图,建议用 AI 生成以保证多角度一致性</span>
<button class="ai-gen-btn" type="button" id="lib-detail-aigen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="width:11px;height:11px;display:inline-block;vertical-align:-1px;margin-right:3px;"><path d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z"/></svg>
<span id="lib-detail-aigen-label">AI 生成三视图</span>
</button>
</div>
<div class="asset-detail-history" id="lib-detail-history" style="display:none;">
<div class="adh-h">// 三视图版本 · <span class="adh-ct" id="lib-detail-history-count">0</span> 版 · <span class="adh-cur" id="lib-detail-history-cur">v1</span></div>
<div class="adh-row" id="lib-detail-history-row"></div>
</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.5" 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.5" 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>
<div class="lib-confirm-bg" id="lib-confirm-bg" aria-hidden="true">
<div class="lib-confirm" role="dialog" aria-modal="true" aria-labelledby="lib-confirm-title">
<div class="lc-h">
<span class="ic">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4M12 17h.01M10.3 3.86l-8.18 14.18A2 2 0 0 0 3.84 21h16.32a2 2 0 0 0 1.72-2.96L13.7 3.86a2 2 0 0 0-3.4 0z"/></svg>
</span>
<h3 id="lib-confirm-title">三视图尚未保存</h3>
<span class="mono">// UNSAVED</span>
</div>
<div class="lc-b" id="lib-confirm-body">
已生成 <b id="lib-confirm-count">1</b> 版三视图但<b>尚未保存</b>。直接退出会丢失这些版本,且当前资产仍标记为「<b>缺三视图</b>」。
</div>
<div class="lc-f">
<span class="spacer"></span>
<button class="btn" type="button" id="lib-confirm-discard">退出</button>
<button class="btn btn-primary" type="button" id="lib-confirm-save">保存并退出</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');
const tipTextEl = document.getElementById('lib-detail-tip-text');
const aigenBtn = document.getElementById('lib-detail-aigen');
const aigenLabel = document.getElementById('lib-detail-aigen-label');
const historyEl = document.getElementById('lib-detail-history');
const historyRowEl = document.getElementById('lib-detail-history-row');
const historyCountEl = document.getElementById('lib-detail-history-count');
const historyCurEl = document.getElementById('lib-detail-history-cur');
const applyBtn = document.getElementById('lib-detail-apply');
// 当前打开资产的状态(仅 isActor + missing tri 时启用)
let _curCard = null;
let _curName = '';
let _versions = []; // [{ ts, label }]
let _curIdx = -1;
let _dirty = false; // 已生成但未保存
let _generating = false;
let _allowGen = false; // 是否启用生成入口(missing tri-view 才启用)
function _renderHistory() {
if (!_versions.length) { historyEl.style.display = 'none'; return; }
historyEl.style.display = 'block';
historyCountEl.textContent = _versions.length;
historyCurEl.textContent = _versions[_curIdx]?.label || '';
historyRowEl.innerHTML = _versions.map((v, i) =>
'<div class="adh-thumb' + (i === _curIdx ? ' active' : '') + '" data-idx="' + i + '" title="' + v.label + ' · ' + v.ts + '">' + v.label + '</div>'
).join('');
historyRowEl.querySelectorAll('.adh-thumb').forEach(el => {
el.addEventListener('click', () => {
const i = Number(el.dataset.idx);
if (i === _curIdx) return;
_curIdx = i;
_renderTriPreview();
_renderHistory();
});
});
}
function _renderTriPreview() {
if (_curIdx < 0) return;
const ver = _versions[_curIdx];
triEl.innerHTML = '<div class="placeholder"><span class="ph-frame">' + _curName + ' · 三视图(正/侧/背)· ' + ver.label + '</span><button class="ad-zoom-btn" type="button" data-zoom-tri aria-label="查看大图" title="查看大图"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5"/></svg></button></div>';
triEl.querySelector('[data-zoom-tri]')?.addEventListener('click', e => {
e.stopPropagation();
if (window.Shell?._openLightbox) Shell._openLightbox('', _curName + ' · 三视图 · ' + ver.label);
});
}
function _renderTriLoading() {
triEl.innerHTML = '<div class="placeholder" style="display:grid;place-items:center;gap:6px;"><div class="spinner" style="width:22px;height:22px;border:2px solid var(--border-faint);border-top-color:var(--heat);border-radius:50%;animation:ad-spin 1s linear infinite;"></div><span class="ph-frame" style="font-size:10.5px;">生成中 · 约 12s</span></div>';
}
function _setAigenLabel(text, loading) {
aigenLabel.textContent = text;
aigenBtn.disabled = !!loading;
tipEl.classList.toggle('is-loading', !!loading);
}
function _startGenerate() {
if (!_allowGen || _generating) return;
_generating = true;
_setAigenLabel(_versions.length ? '生成中…' : '生成中…', true);
_renderTriLoading();
setTimeout(() => {
_generating = false;
const now = new Date();
const ts = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');
_versions.push({ ts, label: 'v' + (_versions.length + 1) });
_curIdx = _versions.length - 1;
_dirty = true;
_renderTriPreview();
_renderHistory();
// 第一次生成后,按钮文案 → 再次生成;并隐藏 tip 文案,只留按钮在右侧
tipEl.style.display = 'flex';
tipTextEl.textContent = '已生成 ' + _versions.length + ' 版三视图 · 不满意可重跑,保存后写入资产';
_setAigenLabel('再次生成', false);
if (window.Shell?.toast) {
Shell.toast('三视图已生成', _curName + ' · ' + _versions[_curIdx].label + ' · 满意请点「保存」');
}
}, 1500);
}
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) {
// 重置本次打开的状态
_curCard = card;
_versions = [];
_curIdx = -1;
_dirty = false;
_generating = false;
_allowGen = false;
historyEl.style.display = 'none';
tipEl.classList.remove('is-loading');
_setAigenLabel('AI 生成三视图', false);
const name = card.dataset.name || '资产';
_curName = 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 || '-'], ['创作人', 'Airshelf'],
['身高', '中等'], ['来源', 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')],
['镜头', '通用'], ['光线', '自然光'], ['创作人', 'Airshelf'],
['用途', '本项目场景资产'], ['使用次数', 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 + ' 次'], ['创作人', 'Airshelf'],
['授权', '商用'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],
];
} else {
tagText = 'AI 素材';
isActor = false;
intro = name;
tags = [source].filter(Boolean);
props = [
['名称', name], ['来源', source], ['作品ID', _fmtAssetId(name, 'asset')],
['创作人', 'Airshelf'], ['文件大小', _fmtSize(name)], ['发布时间', '2024-05-20'],
];
}
titleEl.textContent = name;
kindEl.textContent = '/ ' + tagText;
leadImg.innerHTML = '<span class="ph-frame">' + name + '</span>';
// 立绘 zoom 按钮(单次绑定 · 通过 name 闭包始终读最新 _curName)
const _leadZoomBtn = document.getElementById('lib-detail-lead-zoom');
if (_leadZoomBtn && !_leadZoomBtn.dataset.bound) {
_leadZoomBtn.dataset.bound = '1';
_leadZoomBtn.addEventListener('click', e => {
e.stopPropagation();
if (window.Shell?._openLightbox) Shell._openLightbox('', _curName || titleEl.textContent);
});
}
// 平台预设资产 · 仅 1 张缩略图(用户上传多张的场景在 pipeline 工作台详情中处理)
const _thumbLabel = card.dataset.sceneType ? '场景' : (isActor ? '立绘' : '主图');
thumbsEl.innerHTML = `<div class="thumb placeholder active"><span class="ph-frame">${_thumbLabel}</span></div>`;
// 卡片是否标注「缺三视图」(data-triview="0")
const cardMissingTri = card.dataset.triview === '0';
if (card.dataset.sceneType) {
triSection.style.display = 'none';
} else if (isActor) {
triSection.style.display = '';
triEl.classList.remove('actor');
ratioChip.textContent = '16:9';
if (cardMissingTri) {
// 人物 · 缺三视图 → 显示生成入口
_allowGen = true;
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>暂未生成三视图 · 点击右侧按钮 AI 生成</span></div>';
tipTextEl.textContent = '暂无三视图,建议用 AI 生成以保证多角度一致性';
_setAigenLabel('AI 生成三视图', false);
tipEl.style.display = 'flex';
} else {
triEl.innerHTML = '<div class="placeholder"><span class="ph-frame">' + name + ' · 三视图 (正/侧/背)</span><button class="ad-zoom-btn" type="button" data-zoom-tri aria-label="查看大图" title="查看大图"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5"/></svg></button></div>';
tipEl.style.display = 'none';
}
} else {
triSection.style.display = '';
triEl.classList.remove('actor');
ratioChip.textContent = '16:9';
if (hasTri && !cardMissingTri) {
triEl.innerHTML = '<div class="placeholder"><span class="ph-frame">' + name + ' · 三视图</span><button class="ad-zoom-btn" type="button" data-zoom-tri aria-label="查看大图" title="查看大图"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5"/></svg></button></div>';
tipEl.style.display = 'none';
} else {
// 商品/其他 · 缺三视图 → 同样启用生成入口
_allowGen = true;
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>';
tipTextEl.textContent = '暂无三视图,建议用 AI 生成以保证多角度一致性';
_setAigenLabel('AI 生成三视图', false);
tipEl.style.display = 'flex';
}
}
// 三视图 zoom 按钮 click
triEl.querySelector('[data-zoom-tri]')?.addEventListener('click', e => {
e.stopPropagation();
if (window.Shell?._openLightbox) Shell._openLightbox('', name + ' · 三视图');
});
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="1.5" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg></button>';
const tagAddBtn = tagsEl.querySelector('.ad-tag-add');
tagAddBtn?.addEventListener('click', e => {
e.stopPropagation();
const existing = [...tagsEl.querySelectorAll('.ad-tag-chip')].map(el => el.textContent);
const next = ['精选素材', '常用资产', '待复用', '已核准'].find(t => !existing.includes(t)) || '新标签';
const chip = document.createElement('span');
chip.className = 'ad-tag-chip';
chip.textContent = next;
tagsEl.insertBefore(chip, tagAddBtn);
Shell.toast('已添加标签', next);
});
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');
}
// ── 二次确认弹窗 ──
const confirmBg = document.getElementById('lib-confirm-bg');
const confirmCountEl = document.getElementById('lib-confirm-count');
const confirmSaveBtn = document.getElementById('lib-confirm-save');
const confirmDiscardBtn = document.getElementById('lib-confirm-discard');
function _openConfirm() {
confirmCountEl.textContent = _versions.length;
confirmBg.classList.add('show');
confirmBg.setAttribute('aria-hidden', 'false');
}
function _closeConfirm() {
confirmBg.classList.remove('show');
confirmBg.setAttribute('aria-hidden', 'true');
}
confirmBg.addEventListener('click', e => { if (e.target === confirmBg) _closeConfirm(); });
function _doSave() {
if (_curCard) {
const badge = _curCard.querySelector('.tri-missing-badge');
if (badge) badge.remove();
_curCard.dataset.triview = '1';
}
_dirty = false;
Shell.toast('已保存', _curName + ' · 三视图(' + _versions[_curIdx].label + ')已写入资产');
}
function close(force) {
if (!force && _dirty) {
_openConfirm();
return;
}
bg.classList.remove('show');
_dirty = false;
}
bg.addEventListener('click', e => { if (e.target === bg) close(); });
document.getElementById('lib-detail-x').addEventListener('click', () => close());
applyBtn.addEventListener('click', () => {
if (_allowGen && _versions.length) {
_doSave();
close(true);
return;
}
if (_allowGen && !_versions.length) {
Shell.toast('请先生成三视图', '点击「AI 生成三视图」开始');
return;
}
Shell.toast('已保存', _curName);
close(true);
});
bg.querySelectorAll('.ad-icon-btn, .ad-stat-btn').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
Shell.toast('已加入下载', (_curName || '资产') + ' · 原图 / 三视图');
});
});
// 弹窗按钮 · 主按钮「保存并退出」/ 次按钮「退出」
confirmSaveBtn.addEventListener('click', () => {
if (_versions.length) _doSave();
_closeConfirm();
close(true);
});
confirmDiscardBtn.addEventListener('click', () => {
_dirty = false;
_closeConfirm();
close(true);
});
aigenBtn.addEventListener('click', _startGenerate);
// Esc · 若确认弹窗开着,先关确认;否则尝试关详情(经过 dirty 检查)
document.addEventListener('keydown', e => {
if (e.key !== 'Escape') return;
if (confirmBg.classList.contains('show')) { _closeConfirm(); return; }
if (!bg.classList.contains('show')) return;
close();
});
// 把所有 .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>