Files
ai-image/web/index.html
T

350 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>画廊 · ai-image</title>
<link rel="stylesheet" href="styles.css" />
<script src="api.js"></script>
<script src="components.js"></script>
</head>
<body data-page="gallery">
<aside class="app-nav">
<div class="app-brand">
<span class="brand-mark" aria-hidden="true"></span>
<span class="brand-text"><b>ai-image</b><small>绘图聚合控制台</small></span>
</div>
<nav class="nav-list">
<p class="nav-group">工作台</p>
<a class="nav-item" data-nav="create" href="create.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 3-1.9 5.8L4 10.7l6.1 1.9L12 18.4l1.9-5.8 6.1-1.9-6.1-1.9Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></svg></span>出图</a>
<a class="nav-item" data-nav="gallery" href="index.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg></span>画廊</a>
<a class="nav-item" data-nav="records" href="records.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg></span>生成记录</a>
<p class="nav-group">配置</p>
<a class="nav-item" data-nav="providers" href="providers.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v3a6 6 0 0 1-12 0V8Z"/></svg></span>供应商</a>
<a class="nav-item" data-nav="models" href="models.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z"/><path d="m7 16.5-4.74-2.85"/><path d="m7 16.5 5-3"/><path d="M7 16.5v5.17"/><path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z"/><path d="m17 16.5-5-3"/><path d="m17 16.5 4.74-2.85"/><path d="M17 16.5v5.17"/><path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z"/><path d="M12 8 7.26 5.15"/><path d="m12 8 4.74-2.85"/><path d="M12 13.5V8"/></svg></span>模型</a>
<p class="nav-group">Agent</p>
<a class="nav-item" data-nav="skills" href="skills.html"><span class="nav-ico"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg></span>SKILL.md</a>
</nav>
<div class="nav-foot">
<span class="dot dot-ok"></span>服务在线 <span class="mono">:8765</span>
</div>
</aside>
<main class="app-content">
<header class="top-bar">
<div class="crumb">
<span class="crumb-sec">工作台</span>
<span class="crumb-sep">/</span>
<h1 id="pageTitle">画廊</h1>
</div>
<div class="top-actions">
<span class="tag tag-info mono" id="outDirTag">output/</span>
<a class="btn btn-secondary btn-sm" href="records.html">生成记录</a>
<a class="btn btn-sm" href="providers.html">接入供应商</a>
</div>
</header>
<section class="page-body">
<!-- ============ 视图一:画廊(output/ 下的文件夹)列表 ============ -->
<div id="viewFolders">
<div class="toolbar">
<div class="input-group" style="max-width:280px;flex:1">
<input type="search" id="folderSearch" placeholder="搜索画廊名称(profile_name)" />
</div>
<span class="spacer"></span>
<span class="dim mono" id="folderCount">—</span>
<button class="btn btn-secondary btn-sm" id="refreshFolders"><span class="ico" data-icon="refresh"></span>刷新</button>
</div>
<div class="grid grid-4" style="margin-bottom:16px">
<div class="card stat">
<div class="k">画廊</div>
<div class="v" id="statFolders">—</div>
<div class="sub">output/ 下的子目录</div>
</div>
<div class="card stat">
<div class="k">画廊图片</div>
<div class="v" id="statTotal">—</div>
<div class="sub">全部目录累计</div>
</div>
<div class="card stat">
<div class="k">占用空间</div>
<div class="v" id="statSize">—</div>
<div class="sub">output/ 累计</div>
</div>
<div class="card stat">
<div class="k">最新产出</div>
<div class="v" id="statLatest" style="font-size:18px">—</div>
<div class="sub">最近一次生成时间</div>
</div>
</div>
<div class="card">
<div class="card-head">
<div>
<h3>画廊列表</h3>
<div class="sub">每个文件夹即一个画廊(对应生成时的 profile_name),点击进入查看图片</div>
</div>
<div class="top-actions">
<span class="tag tag-info mono">output/</span>
</div>
</div>
<div class="card-body">
<div class="folder-grid" id="folderGrid"></div>
<div class="empty hidden" id="folderEmpty">
<span class="ico" data-icon="images"></span>
<h4>output/ 下还没有画廊</h4>
<p>出图时传入 <code>profile_name</code>,图片会自动落到 output/&lt;profile_name&gt;/ 并生成一个画廊。</p>
<a class="btn btn-secondary btn-sm" href="providers.html">配置供应商</a>
</div>
</div>
</div>
</div>
<!-- ============ 视图二:单个画廊内的图片 ============ -->
<div id="viewGallery" class="hidden">
<div class="toolbar">
<button class="btn btn-secondary btn-sm" id="backBtn"><span class="ico" data-icon="chevronLeft"></span>返回画廊列表</button>
<span class="mono dim" id="galleryPath">output/</span>
<span class="spacer"></span>
<select id="modelFilter" style="width:220px">
<option value="all">全部模型</option>
</select>
<div class="input-group" style="max-width:240px;flex:1">
<input type="search" id="imageSearch" placeholder="搜索文件名 / 模型" />
</div>
<span class="dim mono" id="galCount">—</span>
</div>
<div class="grid grid-3" style="margin-bottom:16px">
<div class="card stat">
<div class="k">图片数</div>
<div class="v" id="gStatCount">—</div>
<div class="sub">当前画廊</div>
</div>
<div class="card stat">
<div class="k">占用空间</div>
<div class="v" id="gStatSize">—</div>
<div class="sub">当前画廊累计</div>
</div>
<div class="card stat">
<div class="k">最新产出</div>
<div class="v" id="gStatLatest" style="font-size:18px">—</div>
<div class="sub">该画廊最后生成时间</div>
</div>
</div>
<div class="card">
<div class="card-head">
<div>
<h3 id="galleryTitle">—</h3>
<div class="sub">点击缩略图查看大图与元信息</div>
</div>
<div class="top-actions">
<span class="tag tag-info mono" id="galleryPathTag">output/</span>
</div>
</div>
<div class="card-body">
<div class="gal-grid" id="galleryGrid"></div>
<div class="empty hidden" id="galEmpty">
<span class="ico" data-icon="images"></span>
<h4>这个画廊还没有图片</h4>
<p>图片会落到 <code>output/&lt;profile_name&gt;/</code>,出图成功后自动出现。</p>
<a class="btn btn-secondary btn-sm" href="providers.html">配置供应商</a>
</div>
</div>
</div>
</div>
<div class="lightbox" id="lightbox">
<div class="lb-inner">
<div class="lb-stage">
<button class="lb-nav" id="lbPrev"><span class="ico" data-icon="chevronLeft"></span></button>
<img id="lbImg" alt="图片预览" />
<button class="lb-nav" id="lbNext"><span class="ico" data-icon="chevronRight"></span></button>
</div>
<div class="lb-info" id="lbInfo"></div>
<div class="lb-prompt hidden" id="lbPrompt">
<b>提示词</b>
<div id="lbPromptText"></div>
</div>
<div style="display:flex;justify-content:flex-end">
<button class="btn btn-secondary btn-sm" id="lbDelete">删除图片</button>
<button class="btn btn-secondary btn-sm" id="lbClose">关闭</button>
</div>
</div>
</div>
<script>
(function () {
let folders = [];
let items = [];
let view = [];
let cur = 0;
let current = null;
const $ = (s) => document.querySelector(s);
const folderGrid = $("#folderGrid");
const grid = $("#galleryGrid");
/* ------------------------------------------------ 视图一:画廊列表 */
function renderFolders() {
const q = $("#folderSearch").value.trim().toLowerCase();
const list = folders.filter((f) => !q || f.name.toLowerCase().includes(q));
folderGrid.innerHTML = "";
$("#folderEmpty").classList.toggle("hidden", list.length !== 0);
$("#folderCount").textContent = list.length + " / " + folders.length + " 个画廊";
list.forEach((f) => {
const el = document.createElement("div");
el.className = "folder-card";
el.innerHTML =
'<div class="folder-cover"><img src="' + esc(f.cover) + '" alt="' + esc(f.name) + '" loading="lazy" />' +
'<span class="folder-count">' + f.count + " 张</span></div>" +
'<div class="folder-body">' +
'<div class="folder-name" title="' + esc(f.name) + '">' + esc(f.name) + "</div>" +
'<div class="folder-meta">' + fmtBytes(f.bytes) + " · " + esc(f.updated_at) + "</div>" +
'<div class="folder-models">' +
f.models.map((m) => '<span class="tag">' + esc(m) + "</span>").join("") +
'</div></div>';
el.addEventListener("click", () => openGallery(f.name));
folderGrid.appendChild(el);
});
}
async function loadFolders() {
folders = await window.API.listGalleries();
const bytes = folders.reduce((s, f) => s + (f.bytes || 0), 0);
const total = folders.reduce((s, f) => s + (f.count || 0), 0);
const latest = folders.length
? folders.map((f) => f.updated_at).sort().slice(-1)[0]
: "—";
$("#statFolders").textContent = folders.length;
$("#statTotal").textContent = total;
$("#statSize").textContent = fmtBytes(bytes);
$("#statLatest").textContent = latest;
renderFolders();
}
/* ------------------------------------------- 视图二:画廊内图片 */
function openGallery(name) {
current = name;
$("#viewFolders").classList.add("hidden");
$("#viewGallery").classList.remove("hidden");
$("#pageTitle").textContent = name;
$("#galleryTitle").textContent = name;
$("#galleryPath").textContent = "output/" + name + "/";
$("#galleryPathTag").textContent = "output/" + name + "/";
$("#imageSearch").value = "";
$("#modelFilter").value = "all";
runTask(loadImages);
}
function closeGallery() {
current = null;
$("#viewGallery").classList.add("hidden");
$("#viewFolders").classList.remove("hidden");
$("#pageTitle").textContent = "画廊";
}
function applyImageFilter() {
const m = $("#modelFilter").value;
const q = $("#imageSearch").value.trim().toLowerCase();
view = items.filter((g) => {
if (m !== "all" && g.model !== m) return false;
if (q && !((g.filename + g.model + g.provider_name).toLowerCase().includes(q))) return false;
return true;
});
renderImages();
}
function renderImages() {
grid.innerHTML = "";
$("#galEmpty").classList.toggle("hidden", view.length !== 0);
$("#galCount").textContent = view.length + " / " + items.length + " 张";
view.forEach((g, i) => {
const el = document.createElement("div");
el.className = "gal-item";
el.innerHTML =
'<img src="' + esc(g.url) + '" alt="' + esc(g.filename) + '" loading="lazy" />' +
'<span class="tag gal-tag">' + esc(g.model) + "</span>" +
'<div class="gal-meta"><b>' + esc(g.filename) + "</b><span>" + fmtBytes(g.bytes) + " · " + esc(g.created_at) + "</span></div>";
el.addEventListener("click", () => openLb(i));
grid.appendChild(el);
});
}
async function loadImages() {
items = await window.API.listGallery(current);
const models = Array.from(new Set(items.map((g) => g.model)));
$("#modelFilter").innerHTML =
'<option value="all">全部模型</option>' +
models.map((m) => '<option value="' + esc(m) + '">' + esc(m) + "</option>").join("");
const bytes = items.reduce((s, g) => s + (g.bytes || 0), 0);
const latest = items.length
? items.map((g) => g.created_at).sort().slice(-1)[0]
: "—";
$("#gStatCount").textContent = items.length;
$("#gStatSize").textContent = fmtBytes(bytes);
$("#gStatLatest").textContent = latest;
applyImageFilter();
}
function openLb(i) {
if (!view.length) return;
cur = i;
const g = view[i];
$("#lbImg").src = g.url;
$("#lbImg").alt = g.filename;
$("#lbInfo").innerHTML =
"<span><b>文件</b> " + esc(g.filename) + "</span>" +
"<span><b>画廊</b> " + esc(g.profile_name) + "</span>" +
"<span><b>模型</b> " + esc(g.model) + "</span>" +
"<span><b>供应商</b> " + esc(g.provider_name) + "</span>" +
"<span><b>大小</b> " + fmtBytes(g.bytes) + "</span>" +
"<span><b>时间</b> " + esc(g.created_at) + "</span>";
const prompt = typeof g.prompt === "string" ? g.prompt : "";
$("#lbPromptText").textContent = prompt;
$("#lbPrompt").classList.toggle("hidden", !prompt.trim());
$("#lightbox").classList.add("open");
}
$("#lbPrev").addEventListener("click", (e) => { e.stopPropagation(); if (view.length) openLb((cur - 1 + view.length) % view.length); });
$("#lbNext").addEventListener("click", (e) => { e.stopPropagation(); if (view.length) openLb((cur + 1) % view.length); });
$("#lbClose").addEventListener("click", () => $("#lightbox").classList.remove("open"));
$("#lbDelete").addEventListener("click", (event) => {
const image = view[cur];
if (!image || !window.confirm("确定删除图片「" + image.filename + "」?")) return;
runTask(async () => {
await window.API.deleteGalleryImage(image.profile_name, image.filename);
$("#lightbox").classList.remove("open");
await loadImages();
await loadFolders();
toast("图片已删除");
}, event.currentTarget);
});
$("#lightbox").addEventListener("click", (e) => { if (e.target === $("#lightbox")) $("#lightbox").classList.remove("open"); });
document.addEventListener("keydown", (e) => {
if (!$("#lightbox").classList.contains("open")) return;
if (e.key === "ArrowLeft") $("#lbPrev").click();
if (e.key === "ArrowRight") $("#lbNext").click();
});
$("#backBtn").addEventListener("click", closeGallery);
$("#folderSearch").addEventListener("input", renderFolders);
$("#modelFilter").addEventListener("change", applyImageFilter);
$("#imageSearch").addEventListener("input", applyImageFilter);
$("#refreshFolders").addEventListener("click", (event) => runTask(loadFolders, event.currentTarget));
paintIcons();
runTask(loadFolders);
})();
</script>
</section>
</main>
</body>
</html>