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