AirShelf/v2/library.html
UI 设计 e293aa43be
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6s
feat(v2): 添加 V2.1 设计稿目录 · 团队/设置页 · pipeline 多项 mock 优化
2026-05-21 16:18:28 +08:00

1806 lines
110 KiB
HTML

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