v2.6.0 历史分页+筛选+搜索+表头升降排序;一键清空历史(DELETE /api/tests);右下角置顶/置底悬浮按钮
This commit is contained in:
@@ -184,6 +184,17 @@ body {
|
||||
.console .ln.sys { color: #5b6b8c; font-style: italic; }
|
||||
|
||||
/* ── 历史表 ── */
|
||||
.hist-filters {
|
||||
display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.hist-filters select {
|
||||
background: var(--panel2); color: var(--text); border: 1px solid var(--border);
|
||||
border-radius: 8px; padding: 7px 10px; font-size: 12.5px; outline: none;
|
||||
}
|
||||
.hist-filters select:focus { border-color: var(--accent); }
|
||||
.hist-filters .icon-input { min-width: 160px; }
|
||||
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table.history { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.history th, .history td { padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
||||
@@ -192,6 +203,17 @@ table.history { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.history td.num { font-family: var(--mono); }
|
||||
.history .chk-col { width: 34px; text-align: center; }
|
||||
.history .chk-col input { width: 15px; height: 15px; cursor: pointer; accent-color: var(--accent); }
|
||||
.history th.sortable { cursor: pointer; user-select: none; }
|
||||
.history th.sortable:hover { color: var(--accent); }
|
||||
.history th.sortable.asc::after { content: " ▲"; font-size: 10px; }
|
||||
.history th.sortable.desc::after { content: " ▼"; font-size: 10px; }
|
||||
|
||||
/* 历史分页条 */
|
||||
.hist-pager {
|
||||
display: flex; align-items: center; justify-content: center; gap: 14px;
|
||||
margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border);
|
||||
}
|
||||
.hist-pager .btn:disabled { opacity: .4; cursor: not-allowed; }
|
||||
.status-pill { padding: 2px 10px; border-radius: 12px; font-size: 11px; }
|
||||
.status-pill.running { background: rgba(79,140,255,.15); color: var(--accent); }
|
||||
.status-pill.done { background: rgba(34,197,139,.15); color: var(--accent2); }
|
||||
@@ -257,6 +279,18 @@ pre.json-box {
|
||||
padding: 10px; font: 11.5px/1.6 var(--mono); overflow: auto; max-height: 260px; color: #a8f0d0;
|
||||
}
|
||||
|
||||
/* ── 右下角一键置顶/置底 ── */
|
||||
.scroll-fab {
|
||||
position: fixed; right: 20px; bottom: 20px;
|
||||
display: flex; flex-direction: column; gap: 8px; z-index: 60;
|
||||
}
|
||||
.fab {
|
||||
width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--border);
|
||||
background: var(--panel2); color: var(--text); font-size: 16px; cursor: pointer;
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,.45); transition: .15s; line-height: 1;
|
||||
}
|
||||
.fab:hover { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.config-panel { position: static; }
|
||||
|
||||
+48
-3
@@ -175,23 +175,62 @@
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<h2>🗂 测试历史</h2>
|
||||
<h2>🗂 测试历史 <span class="hint" id="hist-total"></span></h2>
|
||||
<div class="log-actions">
|
||||
<span class="hint" id="hist-selected"></span>
|
||||
<button class="btn small primary" id="btn-compare">⚖️ 对比所选</button>
|
||||
<button class="btn small danger" id="btn-clear-history" title="一键清空全部历史记录(不可恢复)">🗑 清空历史</button>
|
||||
<button class="btn small" id="btn-refresh-history">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hist-filters">
|
||||
<div class="icon-input" style="flex:2;min-width:160px"><span class="icon">🔍</span><input id="hist-q" placeholder="搜索:编号/名称/模型/提供商/状态/时间"></div>
|
||||
<select id="hist-status" title="按状态筛选">
|
||||
<option value="">状态:全部</option>
|
||||
<option value="running">测试中</option>
|
||||
<option value="done">完成</option>
|
||||
<option value="error">出错</option>
|
||||
<option value="canceled">已取消</option>
|
||||
</select>
|
||||
<select id="hist-provider" title="按提供商筛选">
|
||||
<option value="">提供商:全部</option>
|
||||
<option value="openai">OpenAI 兼容</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="google">Google Gemini</option>
|
||||
</select>
|
||||
<select id="hist-size" title="每页条数">
|
||||
<option value="10">10 条/页</option>
|
||||
<option value="20" selected>20 条/页</option>
|
||||
<option value="50">50 条/页</option>
|
||||
<option value="100">100 条/页</option>
|
||||
</select>
|
||||
<button class="btn small" id="hist-reset" title="清空筛选条件并回到第一页">✕ 重置</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="history" id="history">
|
||||
<thead><tr>
|
||||
<th class="chk-col"><input type="checkbox" id="hist-check-all" title="全选"></th>
|
||||
<th>#</th><th>时间</th><th>名称</th><th>提供商</th><th>模型</th><th>并发</th><th>采样</th>
|
||||
<th>首字 ms</th><th>预填充 tok/s</th><th>解码 tok/s</th><th>状态</th><th>操作</th>
|
||||
<th class="sortable" data-sort="id" title="点击排序">#</th>
|
||||
<th class="sortable" data-sort="created_at" title="点击排序">时间</th>
|
||||
<th class="sortable" data-sort="name" title="点击排序">名称</th>
|
||||
<th class="sortable" data-sort="provider" title="点击排序">提供商</th>
|
||||
<th class="sortable" data-sort="model" title="点击排序">模型</th>
|
||||
<th>并发</th>
|
||||
<th class="sortable" data-sort="samples" title="点击排序">采样</th>
|
||||
<th class="sortable" data-sort="ttft" title="点击排序">首字 ms</th>
|
||||
<th class="sortable" data-sort="prefill" title="点击排序">预填充 tok/s</th>
|
||||
<th class="sortable" data-sort="decode" title="点击排序">解码 tok/s</th>
|
||||
<th class="sortable" data-sort="status" title="点击排序">状态</th>
|
||||
<th>操作</th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="hist-pager">
|
||||
<button class="btn small" id="hist-prev">◀ 上一页</button>
|
||||
<span class="hint" id="hist-page-info"></span>
|
||||
<button class="btn small" id="hist-next">下一页 ▶</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -223,6 +262,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右下角:一键置顶 / 置底 -->
|
||||
<div class="scroll-fab">
|
||||
<button class="fab" id="fab-top" title="回到顶部">⬆</button>
|
||||
<button class="fab" id="fab-bottom" title="滚到底部">⬇</button>
|
||||
</div>
|
||||
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+143
-30
@@ -422,6 +422,7 @@ function stopTest() {
|
||||
/* ───────────────────────── 测试历史 ───────────────────────── */
|
||||
|
||||
let histSelected = new Set(); // 勾选用于对比的测试 id
|
||||
const histState = { page: 1, pageSize: 20, q: "", status: "", provider: "", sort: "id", order: "desc" };
|
||||
|
||||
function updateHistSelectedLabel() {
|
||||
const el = $("#hist-selected");
|
||||
@@ -429,43 +430,74 @@ function updateHistSelectedLabel() {
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
const list = await api("/api/tests");
|
||||
const p = new URLSearchParams({
|
||||
page: histState.page, page_size: histState.pageSize,
|
||||
q: histState.q, status: histState.status, provider: histState.provider,
|
||||
sort: histState.sort, order: histState.order,
|
||||
});
|
||||
let d;
|
||||
try {
|
||||
d = await api("/api/tests?" + p.toString());
|
||||
} catch (e) { return; }
|
||||
if (!d || !d.ok) return;
|
||||
renderHistory(d);
|
||||
}
|
||||
|
||||
function renderHistory(d) {
|
||||
const list = d.items || [];
|
||||
const tb = $("#history tbody");
|
||||
tb.innerHTML = "";
|
||||
const selAll = $("#hist-check-all");
|
||||
if (selAll) selAll.checked = false;
|
||||
const hasFilter = histState.q || histState.status || histState.provider;
|
||||
$("#hist-total").textContent = d.total ? `(共 ${d.total} 条)` : "";
|
||||
if (!list.length) {
|
||||
tb.innerHTML = '<tr><td colspan="13" style="color:var(--muted);text-align:center">暂无测试记录</td></tr>';
|
||||
tb.innerHTML = `<tr><td colspan="13" style="color:var(--muted);text-align:center">暂无${hasFilter ? "匹配的" : ""}测试记录${hasFilter ? "(试试调整筛选条件)" : ""}</td></tr>`;
|
||||
updateHistSelectedLabel();
|
||||
return;
|
||||
}
|
||||
for (const t of list) {
|
||||
const s = t.summary || {};
|
||||
const g = t.gen || {};
|
||||
const cls = s.concurrency_levels || g.concurrency_levels || [1];
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td class="chk-col"><input type="checkbox" class="hist-chk" data-id="${t.id}" ${histSelected.has(t.id) ? "checked" : ""}></td>
|
||||
<td>#${t.id}</td>
|
||||
<td>${esc(t.created_at)}</td>
|
||||
<td title="${esc(t.name || "")}">${esc(t.name || "—")}</td>
|
||||
<td>${esc(PROVIDER_LABEL[t.provider] || t.provider)}</td>
|
||||
<td>${esc(t.model)}</td>
|
||||
<td class="num">${esc(cls.join("/"))}</td>
|
||||
<td class="num">${fmt(s.samples_ok)}/${fmt(s.samples_total)}</td>
|
||||
<td class="num">${fmt(s.avg_ttft_ms)}</td>
|
||||
<td class="num">${fmt(s.avg_prefill_speed)}</td>
|
||||
<td class="num">${fmt(s.avg_decode_speed)}</td>
|
||||
<td><span class="status-pill ${esc(t.status)}">${STATUS_LABEL[t.status] || t.status}</span></td>
|
||||
<td>
|
||||
<button class="btn small" data-view="${t.id}">查看</button>
|
||||
<button class="btn small" data-send="${t.id}" title="发送到评测站对应账号下">📤 发送</button>
|
||||
<button class="btn small" data-xlsx="${t.id}">Excel</button>
|
||||
<button class="btn small danger" data-del="${t.id}">删除</button>
|
||||
</td>`;
|
||||
tb.appendChild(tr);
|
||||
} else {
|
||||
for (const t of list) {
|
||||
const s = t.summary || {};
|
||||
const g = t.gen || {};
|
||||
const cls = s.concurrency_levels || g.concurrency_levels || [1];
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td class="chk-col"><input type="checkbox" class="hist-chk" data-id="${t.id}" ${histSelected.has(t.id) ? "checked" : ""}></td>
|
||||
<td>#${t.id}</td>
|
||||
<td>${esc(t.created_at)}</td>
|
||||
<td title="${esc(t.name || "")}">${esc(t.name || "—")}</td>
|
||||
<td>${esc(PROVIDER_LABEL[t.provider] || t.provider)}</td>
|
||||
<td>${esc(t.model)}</td>
|
||||
<td class="num">${esc(cls.join("/"))}</td>
|
||||
<td class="num">${fmt(s.samples_ok)}/${fmt(s.samples_total)}</td>
|
||||
<td class="num">${fmt(s.avg_ttft_ms)}</td>
|
||||
<td class="num">${fmt(s.avg_prefill_speed)}</td>
|
||||
<td class="num">${fmt(s.avg_decode_speed)}</td>
|
||||
<td><span class="status-pill ${esc(t.status)}">${STATUS_LABEL[t.status] || t.status}</span></td>
|
||||
<td>
|
||||
<button class="btn small" data-view="${t.id}">查看</button>
|
||||
<button class="btn small" data-send="${t.id}" title="发送到评测站对应账号下">📤 发送</button>
|
||||
<button class="btn small" data-xlsx="${t.id}">Excel</button>
|
||||
<button class="btn small danger" data-del="${t.id}">删除</button>
|
||||
</td>`;
|
||||
tb.appendChild(tr);
|
||||
}
|
||||
}
|
||||
updateHistSelectedLabel();
|
||||
|
||||
// 分页信息
|
||||
const totalPages = Math.max(1, d.total_pages || 1);
|
||||
$("#hist-page-info").textContent = `第 ${d.page} / ${totalPages} 页 · 共 ${d.total} 条`;
|
||||
$("#hist-prev").disabled = d.page <= 1;
|
||||
$("#hist-next").disabled = d.page >= totalPages;
|
||||
histState.page = d.page;
|
||||
updateSortIndicators();
|
||||
}
|
||||
|
||||
function updateSortIndicators() {
|
||||
$$("#history thead th.sortable").forEach((th) => {
|
||||
th.classList.remove("asc", "desc");
|
||||
if (th.dataset.sort === histState.sort) th.classList.add(histState.order);
|
||||
});
|
||||
}
|
||||
|
||||
/* ───────────────────────── 多测试对比 ───────────────────────── */
|
||||
@@ -1064,8 +1096,57 @@ function bind() {
|
||||
|
||||
$("#btn-clear-console").addEventListener("click", clearConsole);
|
||||
$("#btn-export-log").addEventListener("click", exportCurrentLog);
|
||||
$("#btn-refresh-history").addEventListener("click", loadHistory);
|
||||
$("#btn-refresh-history").addEventListener("click", () => { histState.page = 1; loadHistory(); });
|
||||
$("#btn-compare").addEventListener("click", openCompare);
|
||||
$("#btn-clear-history").addEventListener("click", clearAllHistory);
|
||||
|
||||
// 历史:筛选 / 搜索 / 每页条数 / 分页 / 重置
|
||||
let histQTimer = null;
|
||||
$("#hist-q").addEventListener("input", () => {
|
||||
clearTimeout(histQTimer);
|
||||
histQTimer = setTimeout(() => {
|
||||
histState.q = $("#hist-q").value.trim();
|
||||
histState.page = 1;
|
||||
loadHistory();
|
||||
}, 300);
|
||||
});
|
||||
$("#hist-status").addEventListener("change", () => {
|
||||
histState.status = $("#hist-status").value;
|
||||
histState.page = 1;
|
||||
loadHistory();
|
||||
});
|
||||
$("#hist-provider").addEventListener("change", () => {
|
||||
histState.provider = $("#hist-provider").value;
|
||||
histState.page = 1;
|
||||
loadHistory();
|
||||
});
|
||||
$("#hist-size").addEventListener("change", () => {
|
||||
histState.pageSize = parseInt($("#hist-size").value) || 20;
|
||||
histState.page = 1;
|
||||
loadHistory();
|
||||
});
|
||||
$("#hist-prev").addEventListener("click", () => {
|
||||
if (histState.page > 1) { histState.page--; loadHistory(); }
|
||||
});
|
||||
$("#hist-next").addEventListener("click", () => {
|
||||
if (!$("#hist-next").disabled) { histState.page++; loadHistory(); }
|
||||
});
|
||||
$("#hist-reset").addEventListener("click", resetHistoryFilters);
|
||||
|
||||
// 历史:点击表头升降排序
|
||||
$("#history thead").addEventListener("click", (e) => {
|
||||
const th = e.target.closest("th.sortable");
|
||||
if (!th) return;
|
||||
const key = th.dataset.sort;
|
||||
if (histState.sort === key) {
|
||||
histState.order = histState.order === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
histState.sort = key;
|
||||
histState.order = (key === "id" || key === "created_at") ? "desc" : "asc";
|
||||
}
|
||||
histState.page = 1;
|
||||
loadHistory();
|
||||
});
|
||||
|
||||
$("#history tbody").addEventListener("click", (e) => {
|
||||
const v = e.target.closest("[data-view]");
|
||||
@@ -1108,6 +1189,38 @@ function bind() {
|
||||
$("#dt-send-eval").addEventListener("click", () => { if (window.__detail) sendToEval(window.__detail.id); });
|
||||
|
||||
document.addEventListener("keydown", (e) => { if (e.key === "Escape") { hideModelPick(); closeDetail(); closeCompare(); } });
|
||||
|
||||
// 右下角一键置顶 / 置底
|
||||
$("#fab-top").addEventListener("click", () => window.scrollTo({ top: 0, behavior: "smooth" }));
|
||||
$("#fab-bottom").addEventListener("click", () =>
|
||||
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "smooth" }));
|
||||
}
|
||||
|
||||
/* ───────────────────────── 历史筛选工具 ───────────────────────── */
|
||||
|
||||
function resetHistoryFilters() {
|
||||
histState.q = ""; histState.status = ""; histState.provider = "";
|
||||
histState.sort = "id"; histState.order = "desc";
|
||||
histState.page = 1;
|
||||
$("#hist-q").value = "";
|
||||
$("#hist-status").value = "";
|
||||
$("#hist-provider").value = "";
|
||||
loadHistory();
|
||||
}
|
||||
|
||||
async function clearAllHistory() {
|
||||
if (!confirm("确定清空全部测试历史?\n所有测试记录、采样数据与日志将被永久删除,无法恢复!")) return;
|
||||
if (!confirm("再次确认:真的要清空全部历史记录吗?")) return;
|
||||
const r = await api("/api/tests", "DELETE");
|
||||
if (r.ok) {
|
||||
histSelected.clear();
|
||||
histState.page = 1;
|
||||
toast("✅ 历史记录已全部清空");
|
||||
loadHistory();
|
||||
updateHistSelectedLabel();
|
||||
} else {
|
||||
toast("❌ 清空失败:" + (r.error || "未知错误"));
|
||||
}
|
||||
}
|
||||
|
||||
/* ───────────────────────── 初始化 ───────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user