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