v1.2.0 对比车+运行速度对比图+图表可配置:移除前台后台管理链接(网址直达登录);新增🛒对比车(排行榜行加入,浮动条隐藏默认,对比页按加入顺序对比,无车默认前5,默认只对比预填充+解码);首页解码速度对比图改运行速度对比(预填充左轴空心柱+解码右轴实心柱+首字虚线);对比图支持柱状(空心/实心)/折线(实线/虚线)+左右双轴+三色深度+X轴旋转/间隔/字号防拥挤
This commit is contained in:
@@ -15,7 +15,6 @@
|
||||
<a href="/compare.html">⚖️ 模型对比</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<a href="/admin.html" class="hint">⚙️ 后台管理</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
+38
-15
@@ -15,21 +15,21 @@
|
||||
<a href="/compare.html">⚖️ 模型对比</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<a href="/admin.html" class="hint">⚙️ 后台管理</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="panel">
|
||||
<h2>⚖️ 模型对比 <span class="hint">(默认对比全部模型;支持硬件显示;下方勾选可去留对比项)</span></h2>
|
||||
<h2>⚖️ 模型对比 <span class="hint">(默认对比前5个;有对比车则按加入顺序对比;默认只对比预填充+解码)</span></h2>
|
||||
|
||||
<div class="hint" style="margin-bottom:8px">① 选择要对比的模型(默认全部):</div>
|
||||
<div class="hint" style="margin-bottom:8px">① 选择要对比的模型(来自对比车,或默认前5;可勾选增删):</div>
|
||||
<div class="chips" id="cmp-models"></div>
|
||||
<div class="form-row" style="margin-top:8px">
|
||||
<button class="btn small" id="cmp-all">✅ 全选</button>
|
||||
<button class="btn small" id="cmp-none">❌ 清空</button>
|
||||
<button class="btn small danger" id="cmp-clear-cart">🗑 清空对比车</button>
|
||||
</div>
|
||||
|
||||
<div class="hint" style="margin:12px 0 6px">② 勾选需要展示的对比项(可去留):</div>
|
||||
<div class="hint" style="margin:12px 0 6px">② 勾选需要展示的对比项(可去留,默认预填充+解码):</div>
|
||||
<div class="col-toggles" id="cmp-cols"></div>
|
||||
|
||||
<div class="form-row" style="margin-top:10px">
|
||||
@@ -43,25 +43,48 @@
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>📊 对比图表</h2>
|
||||
<div class="chart-toolbar">
|
||||
<select id="cmp-metric">
|
||||
<option value="avg_decode_speed">解码速度 (tok/s)</option>
|
||||
<option value="avg_prefill_speed">预填充速度 (tok/s)</option>
|
||||
<option value="avg_ttft_ms">首字延迟 (ms)</option>
|
||||
<option value="best_decode">最佳解码 (tok/s)</option>
|
||||
<h2>📊 运行速度对比图 <span class="hint">(柱状·空心/实心 / 折线·实线/虚线 / 左右双轴 / 三色深度 / X轴可旋转防拥挤)</span></h2>
|
||||
<div class="hint" style="margin-bottom:8px">③ 每个系列设置类型与左右轴,颜色按深浅三色区分;默认只对比预填充(左轴·空心柱)+ 解码(右轴·实心柱):</div>
|
||||
<div class="tbl-wrap">
|
||||
<table class="mini" id="cmp-series-cfg">
|
||||
<thead><tr><th>系列</th><th>显示</th><th>类型</th><th>轴</th><th>颜色</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:10px">
|
||||
<label>X轴标签旋转</label>
|
||||
<select id="cmp-xrotate">
|
||||
<option value="auto">自动</option>
|
||||
<option value="0">水平 0°</option>
|
||||
<option value="30">30°</option>
|
||||
<option value="45">45°</option>
|
||||
<option value="60">60°</option>
|
||||
<option value="90">垂直 90°</option>
|
||||
</select>
|
||||
<select id="cmp-chart-type">
|
||||
<option value="bar">柱状图</option>
|
||||
<option value="line">折线图</option>
|
||||
<label>标签字号</label>
|
||||
<input type="number" id="cmp-xfont" value="12" min="9" max="22" style="width:64px">
|
||||
<label>标签间隔</label>
|
||||
<select id="cmp-xinterval">
|
||||
<option value="auto">不跳(全显示)</option>
|
||||
<option value="1">隔1个</option>
|
||||
<option value="2">隔2个</option>
|
||||
<option value="3">隔3个</option>
|
||||
</select>
|
||||
<button class="btn small primary" id="cmp-chart-gen">🎨 生成/刷新图表</button>
|
||||
<span class="hint" id="cmp-chart-status"></span>
|
||||
</div>
|
||||
<div class="chart-img-wrap" id="cmp-chart-img"><div class="hint">选择指标后点击生成(默认对比全部选中模型)</div></div>
|
||||
<div class="chart-img-wrap" id="cmp-chart-img"><div class="hint">配置好后点击生成;底部横轴字太多可旋转/加间隔避免挤在一起</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对比车 -->
|
||||
<div id="cart-bar" hidden>
|
||||
<span class="cart-label">🛒 对比车 (<b id="cart-count">0</b>)</span>
|
||||
<span class="cart-items" id="cart-items"></span>
|
||||
<button class="btn small danger" id="cart-clear">清空</button>
|
||||
<button class="btn small primary" id="cart-go">⚖️ 对比</button>
|
||||
</div>
|
||||
|
||||
<div id="toast"></div>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -238,3 +238,18 @@ label { font-size: 12px; color: var(--muted); display: block; margin-bottom: 4px
|
||||
.cap-edit-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
@media (max-width: 760px) { .cap-edit-grid { grid-template-columns: 1fr; } }
|
||||
textarea.items-editor { min-height: 160px; font-family: ui-monospace, monospace; font-size: 12px; width: 100%; }
|
||||
|
||||
/* 对比车(浮动条) */
|
||||
#cart-bar {
|
||||
position: fixed; right: 18px; bottom: 18px; z-index: 90;
|
||||
display: flex; align-items: center; gap: 10px; max-width: 640px;
|
||||
background: var(--panel); border: 1px solid var(--accent); border-radius: 12px;
|
||||
padding: 9px 14px; box-shadow: 0 10px 30px rgba(0,0,0,.45);
|
||||
}
|
||||
#cart-bar[hidden] { display: none; }
|
||||
#cart-bar .cart-label { font-weight: 700; color: var(--text-strong); white-space: nowrap; }
|
||||
#cart-bar .cart-items {
|
||||
color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis;
|
||||
white-space: nowrap; max-width: 260px;
|
||||
}
|
||||
#cart-bar .cart-items b { color: var(--text); font-weight: 600; }
|
||||
+13
-5
@@ -15,7 +15,6 @@
|
||||
<a href="/compare.html">⚖️ 模型对比</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<a href="/admin.html" class="hint">⚙️ 后台管理</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
@@ -43,15 +42,16 @@
|
||||
<th class="num" data-sort="best_decode">最佳解码 tok/s<span class="arrow"></span></th>
|
||||
<th>硬件</th>
|
||||
<th class="num" data-sort="last_tested">最后测试<span class="arrow"></span></th>
|
||||
<th>对比车</th>
|
||||
</tr></thead>
|
||||
<tbody><tr><td colspan="11" class="empty">加载中…</td></tr></tbody>
|
||||
<tbody><tr><td colspan="12" class="empty">加载中…</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>📊 解码速度对比图 <span class="hint">(默认对比全部模型,可勾选指定模型后重新生成)</span></h2>
|
||||
<div class="hint" style="margin-bottom:8px">选择要对比的模型(默认全部):</div>
|
||||
<h2>📊 运行速度对比图 <span class="hint">(默认对比全部模型,可勾选指定模型后重新生成)</span></h2>
|
||||
<div class="hint" style="margin-bottom:8px">选择要对比的模型(默认全部;也可点排行榜每行「🛒 加入对比车」后到「⚖️ 模型对比」页对比):</div>
|
||||
<div class="chips" id="lb-chart-models"></div>
|
||||
<div class="form-row" style="margin-top:10px">
|
||||
<button class="btn small" id="lb-chart-all">✅ 全选</button>
|
||||
@@ -60,11 +60,19 @@
|
||||
<span class="hint" id="lb-chart-status"></span>
|
||||
</div>
|
||||
<div class="chart-img-wrap" id="lb-chart-img">
|
||||
<div class="hint">点击上方按钮绘制:模型解码速度(实心柱)+ 预填充(空心柱)+ 首字延迟(虚线)</div>
|
||||
<div class="hint">运行速度对比:预填充(左轴·空心柱)+ 解码(右轴·实心柱)+ 首字延迟(虚线)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对比车 -->
|
||||
<div id="cart-bar" hidden>
|
||||
<span class="cart-label">🛒 对比车 (<b id="cart-count">0</b>)</span>
|
||||
<span class="cart-items" id="cart-items"></span>
|
||||
<button class="btn small danger" id="cart-clear">清空</button>
|
||||
<button class="btn small primary" id="cart-go">⚖️ 对比</button>
|
||||
</div>
|
||||
|
||||
<div id="toast"></div>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
|
||||
+189
-25
@@ -35,6 +35,76 @@ function providerLabel(p) {
|
||||
|
||||
const hwTag = (hw) => (hw ? `<span class="hw-tag" title="${esc(hw)}">🖥 ${esc(hw)}</span>` : "—");
|
||||
|
||||
/* ───────────────────────── 对比车(localStorage 持久化) ───────────────────────── */
|
||||
const CART_KEY = "evalCompareCart";
|
||||
|
||||
function getCart() {
|
||||
try { const c = JSON.parse(localStorage.getItem(CART_KEY) || "[]"); return Array.isArray(c) ? c : []; }
|
||||
catch (e) { return []; }
|
||||
}
|
||||
function saveCart(c) { localStorage.setItem(CART_KEY, JSON.stringify(c)); }
|
||||
function cartAdd(key) {
|
||||
const c = getCart();
|
||||
if (!c.includes(key)) { c.push(key); saveCart(c); toast("🛒 已加入对比车"); }
|
||||
else { toast("已在对比车中"); }
|
||||
renderCartBar();
|
||||
}
|
||||
function cartRemove(key) { saveCart(getCart().filter((k) => k !== key)); renderCartBar(); }
|
||||
function cartClear() { saveCart([]); renderCartBar(); }
|
||||
function cartModelName(key) {
|
||||
const i = String(key).indexOf("|");
|
||||
return i >= 0 ? String(key).slice(i + 1) : String(key);
|
||||
}
|
||||
function renderCartBar() {
|
||||
const bar = $("#cart-bar");
|
||||
if (!bar) return;
|
||||
const c = getCart();
|
||||
if (!c.length) { bar.hidden = true; return; }
|
||||
bar.hidden = false;
|
||||
const cnt = $("#cart-count");
|
||||
if (cnt) cnt.textContent = c.length;
|
||||
const items = $("#cart-items");
|
||||
if (items) items.innerHTML = c.map((k) => `<b>${esc(cartModelName(k))}</b>`).join("、");
|
||||
}
|
||||
function bindCartBar() {
|
||||
const bar = $("#cart-bar");
|
||||
if (!bar) return;
|
||||
const clear = $("#cart-clear");
|
||||
if (clear) clear.addEventListener("click", cartClear);
|
||||
const go = $("#cart-go");
|
||||
if (go) go.addEventListener("click", () => { location.href = "/compare.html"; });
|
||||
renderCartBar();
|
||||
}
|
||||
|
||||
/* 运行速度对比:3 个系列(颜色深浅三色),前端可配置类型/轴 */
|
||||
const RUN_SERIES = [
|
||||
{ key: "avg_prefill_speed", label: "预填充速度", unit: "tok/s", color: "#5b94ff", depth: "浅" },
|
||||
{ key: "avg_decode_speed", label: "解码速度", unit: "tok/s", color: "#2ee67a", depth: "深" },
|
||||
{ key: "avg_ttft_ms", label: "首字延迟", unit: "ms", color: "#ffb43c", depth: "中" },
|
||||
];
|
||||
|
||||
function buildRunPayload(opts) {
|
||||
const series = (opts.series || RUN_SERIES).filter((s) => s.show);
|
||||
const payload = {
|
||||
data: opts.csv, chartType: "bar", title: opts.title || "模型运行速度对比",
|
||||
theme: "default", showLegend: series.length > 1, showGrid: true,
|
||||
showLabel: opts.showLabel ?? false, smoothLine: true,
|
||||
dualYAxis: series.some((s) => s.axis === 1),
|
||||
leftAxisName: opts.leftAxisName || "预填充速度(tok/s)",
|
||||
rightAxisName: opts.rightAxisName || "解码速度(tok/s)",
|
||||
seriesTypes: series.map((s) => s.type),
|
||||
seriesStyles: series.map((s) => s.style),
|
||||
seriesAxis: series.map((s) => s.axis),
|
||||
seriesColors: series.map((s) => s.color),
|
||||
width: 1100, height: 560, pixelRatio: 2,
|
||||
};
|
||||
if (opts.xRotate !== undefined && opts.xRotate !== "" && opts.xRotate !== null) payload.xLabelRotate = Number(opts.xRotate);
|
||||
if (opts.xFont) payload.xLabelFontSize = Number(opts.xFont);
|
||||
if (opts.xInterval !== undefined && opts.xInterval !== "" && opts.xInterval !== null) payload.xLabelInterval = Number(opts.xInterval);
|
||||
if (!payload.dualYAxis) payload.seriesAxis = null;
|
||||
return payload;
|
||||
}
|
||||
|
||||
function navActive() {
|
||||
const path = location.pathname.split("/").pop() || "index.html";
|
||||
$$(".nav a").forEach((a) => {
|
||||
@@ -89,19 +159,19 @@ function renderLbChips() {
|
||||
async function loadLeaderboard() {
|
||||
const tb = $("#lb tbody");
|
||||
if (!tb) return;
|
||||
tb.innerHTML = '<tr><td colspan="11" class="empty">加载中…</td></tr>';
|
||||
tb.innerHTML = '<tr><td colspan="12" class="empty">加载中…</td></tr>';
|
||||
let d;
|
||||
try {
|
||||
d = await api(`/api/leaderboard?sort=${sortKey}&order=${sortOrder}`);
|
||||
} catch (e) {
|
||||
tb.innerHTML = `<tr><td colspan="11" class="empty">加载失败:${esc(e.message)}</td></tr>`;
|
||||
tb.innerHTML = `<tr><td colspan="12" class="empty">加载失败:${esc(e.message)}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
$$("#lb th[data-sort]").forEach((th) => {
|
||||
th.querySelector(".arrow").textContent = (th.dataset.sort === sortKey) ? (sortOrder === "desc" ? " ▼" : " ▲") : "";
|
||||
});
|
||||
if (!d.rows.length) {
|
||||
tb.innerHTML = '<tr><td colspan="11" class="empty">暂无评测数据。可在后台管理「提交管理」里点击「🎲 生成演示数据」或从 llm-speed-tester 一键发送。</td></tr>';
|
||||
tb.innerHTML = '<tr><td colspan="12" class="empty">暂无评测数据。可在后台管理「提交管理」里点击「🎲 生成演示数据」或从 llm-speed-tester 一键发送。</td></tr>';
|
||||
return;
|
||||
}
|
||||
// 记录模型列表,用于对比图选择
|
||||
@@ -125,6 +195,7 @@ async function loadLeaderboard() {
|
||||
<td class="num">${fmt(r.best_decode)}</td>
|
||||
<td>${hwTag(r.hardware)}</td>
|
||||
<td class="num">${esc((r.last_tested || "").slice(5, 16))}</td>
|
||||
<td><button class="btn small" data-cart="${modelKey(r.provider, r.model)}" title="加入对比车">🛒 加入对比车</button></td>
|
||||
</tr>`;
|
||||
}).join("");
|
||||
}
|
||||
@@ -140,9 +211,24 @@ async function genLbChart() {
|
||||
const models = [...lbSelected].join(",");
|
||||
const d = await api(`/api/leaderboard?sort=avg_decode_speed&order=desc&models=${encodeURIComponent(models)}`);
|
||||
if (!d.rows.length) { status.textContent = "所选模型暂无数据"; return; }
|
||||
const n = d.rows.length;
|
||||
// 运行速度对比:预填充(左轴·空心柱) + 解码(右轴·实心柱) + 首字(虚线),三色
|
||||
const series = RUN_SERIES.map((s, i) => ({
|
||||
...s, show: true,
|
||||
type: ["bar", "bar", "line"][i],
|
||||
style: ["hollow", "solid", "dashed"][i],
|
||||
axis: [0, 1, 1][i],
|
||||
}));
|
||||
const payload = buildRunPayload({
|
||||
csv: d.chart_csv,
|
||||
title: `模型运行速度对比(${n} 个模型)`,
|
||||
series,
|
||||
xRotate: n > 14 ? 45 : (n > 8 ? 30 : 0),
|
||||
xFont: n > 14 ? 11 : 12,
|
||||
});
|
||||
const resp = await fetch("/api/chart", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(d.bar_payload),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) { const j = await resp.json().catch(() => ({})); status.textContent = "❌ " + (j.error || "生成失败"); return; }
|
||||
const blob = await resp.blob();
|
||||
@@ -151,8 +237,8 @@ async function genLbChart() {
|
||||
imgWrap.innerHTML = "";
|
||||
const img = document.createElement("img");
|
||||
img.src = lbChartUrl;
|
||||
img.alt = "模型解码速度对比图";
|
||||
img.onload = () => { status.textContent = `✅ 已生成(${d.rows.length} 个模型)`; };
|
||||
img.alt = "模型运行速度对比图";
|
||||
img.onload = () => { status.textContent = `✅ 已生成(${n} 个模型)`; };
|
||||
imgWrap.appendChild(img);
|
||||
} catch (e) { status.textContent = "❌ " + e.message; }
|
||||
}
|
||||
@@ -287,11 +373,13 @@ function openCapDetail(c) {
|
||||
`;
|
||||
}
|
||||
|
||||
/* ───────────────────────── 模型对比页 ───────────────────────── */
|
||||
/* ───────────────────────── 模型对比页(对比车驱动) ───────────────────────── */
|
||||
let cmpModels = [];
|
||||
let cmpSelected = new Set();
|
||||
let cmpCols = new Set(["provider", "model", "hardware", "avg_decode_speed", "avg_prefill_speed", "avg_ttft_ms"]);
|
||||
let cmpOrder = []; // 对比顺序:对比车加入顺序 或 默认前5
|
||||
let cmpCols = new Set(["model", "avg_prefill_speed", "avg_decode_speed"]); // 默认只对比预填充+解码
|
||||
let cmpRows = [];
|
||||
let cmpCsv = "";
|
||||
let cmpChartUrl = "";
|
||||
|
||||
const CMP_COL_DEFS = [
|
||||
@@ -325,15 +413,27 @@ function renderCmpColToggles() {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCmpModels() {
|
||||
async function initCompare() {
|
||||
const box = $("#cmp-models");
|
||||
if (!box) return;
|
||||
let d;
|
||||
try { d = await api("/api/leaderboard?limit=500"); }
|
||||
catch (e) { box.innerHTML = `<span class="hint">加载失败</span>`; return; }
|
||||
cmpModels = d.rows.map((r) => ({ provider: r.provider, model: r.model, key: modelKey(r.provider, r.model) }));
|
||||
if (!cmpSelected.size) cmpModels.forEach((m) => cmpSelected.add(m.key));
|
||||
// 对比车优先(按加入顺序);否则默认前5
|
||||
const cart = getCart();
|
||||
const cartValid = cart.filter((k) => cmpModels.some((m) => m.key === k));
|
||||
if (cartValid.length) {
|
||||
cmpOrder = cartValid;
|
||||
cmpSelected = new Set(cartValid);
|
||||
} else {
|
||||
cmpOrder = cmpModels.slice(0, 5).map((m) => m.key);
|
||||
cmpSelected = new Set(cmpOrder);
|
||||
}
|
||||
renderCmpChips();
|
||||
renderCmpColToggles();
|
||||
renderSeriesCfg();
|
||||
runCompare();
|
||||
}
|
||||
|
||||
function renderCmpChips() {
|
||||
@@ -346,7 +446,8 @@ function renderCmpChips() {
|
||||
c.className = "chip" + (on ? " on" : "");
|
||||
c.innerHTML = `<span>${esc(m.model)}</span>`;
|
||||
c.addEventListener("click", () => {
|
||||
if (cmpSelected.has(m.key)) cmpSelected.delete(m.key); else cmpSelected.add(m.key);
|
||||
if (cmpSelected.has(m.key)) { cmpSelected.delete(m.key); cmpOrder = cmpOrder.filter((k) => k !== m.key); }
|
||||
else { cmpSelected.add(m.key); if (!cmpOrder.includes(m.key)) cmpOrder.push(m.key); }
|
||||
c.classList.toggle("on");
|
||||
});
|
||||
box.appendChild(c);
|
||||
@@ -356,14 +457,15 @@ function renderCmpChips() {
|
||||
async function runCompare() {
|
||||
const status = $("#cmp-status");
|
||||
const tbody = $("#cmp-table tbody");
|
||||
if (!cmpSelected.size) { status.textContent = "请至少选择一个模型"; return; }
|
||||
if (!cmpOrder.length) { status.textContent = "请至少选择一个模型"; return; }
|
||||
status.textContent = "⏳ 正在对比...";
|
||||
const models = [...cmpSelected].join(",");
|
||||
const models = cmpOrder.join(",");
|
||||
try {
|
||||
const d = await api(`/api/compare?models=${encodeURIComponent(models)}`);
|
||||
cmpRows = d.rows;
|
||||
cmpCsv = d.chart_csv;
|
||||
renderCmpTable();
|
||||
status.textContent = `✅ 已对比 ${cmpRows.length} 个模型`;
|
||||
status.textContent = `✅ 已对比 ${cmpRows.length} 个模型(${cmpOrder.length} 个选中)`;
|
||||
} catch (e) {
|
||||
status.textContent = "❌ " + e.message;
|
||||
}
|
||||
@@ -387,20 +489,71 @@ function renderCmpTable() {
|
||||
}).join("") + "</tr>").join("");
|
||||
}
|
||||
|
||||
/* 对比图系列配置:柱状(实心/空心) / 折线(实线/虚线) / 左右双轴 / 三色深度 */
|
||||
function renderSeriesCfg() {
|
||||
const tbody = $("#cmp-series-cfg tbody");
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = RUN_SERIES.map((s, i) => {
|
||||
const show = i < 2; // 默认预填充+解码
|
||||
const type = ["bar", "bar", "line"][i];
|
||||
const style = ["hollow", "solid", "dashed"][i];
|
||||
const axis = [0, 1, 1][i];
|
||||
return `<tr data-skey="${s.key}">
|
||||
<td><span style="display:inline-block;width:12px;height:12px;border-radius:3px;background:${s.color};margin-right:6px;vertical-align:middle"></span>${esc(s.label)} <span class="hint">${s.unit}·${s.depth}色</span></td>
|
||||
<td><input type="checkbox" class="sf-show" ${show ? "checked" : ""}></td>
|
||||
<td><select class="sf-type">
|
||||
<option value="bar|solid" ${type === "bar" && style === "solid" ? "selected" : ""}>柱状·实心</option>
|
||||
<option value="bar|hollow" ${type === "bar" && style === "hollow" ? "selected" : ""}>柱状·空心</option>
|
||||
<option value="line|solid" ${type === "line" && style === "solid" ? "selected" : ""}>折线·实线</option>
|
||||
<option value="line|dashed" ${type === "line" && style === "dashed" ? "selected" : ""}>折线·虚线</option>
|
||||
</select></td>
|
||||
<td><select class="sf-axis">
|
||||
<option value="0" ${axis === 0 ? "selected" : ""}>左轴</option>
|
||||
<option value="1" ${axis === 1 ? "selected" : ""}>右轴</option>
|
||||
</select></td>
|
||||
<td><span class="hint" style="color:${s.color}">${s.color}</span></td>
|
||||
</tr>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function collectSeriesCfg() {
|
||||
return RUN_SERIES.map((s) => {
|
||||
const tr = $(`#cmp-series-cfg tr[data-skey="${s.key}"]`);
|
||||
if (!tr) return { ...s, show: false };
|
||||
const show = tr.querySelector(".sf-show").checked;
|
||||
const [type, style] = tr.querySelector(".sf-type").value.split("|");
|
||||
const axis = Number(tr.querySelector(".sf-axis").value);
|
||||
return { ...s, show, type, style, axis };
|
||||
});
|
||||
}
|
||||
|
||||
async function genCmpChart() {
|
||||
const status = $("#cmp-chart-status");
|
||||
const imgWrap = $("#cmp-chart-img");
|
||||
if (!cmpSelected.size) { status.textContent = "请至少选择一个模型"; return; }
|
||||
const metric = $("#cmp-metric").value;
|
||||
const ctype = $("#cmp-chart-type").value;
|
||||
if (!cmpOrder.length) { status.textContent = "请至少选择一个模型"; return; }
|
||||
const series = collectSeriesCfg();
|
||||
if (!series.some((s) => s.show)) { status.textContent = "请至少勾选一个系列"; return; }
|
||||
status.textContent = "⏳ 正在生成...";
|
||||
const xRotate = $("#cmp-xrotate").value;
|
||||
const xFont = parseInt($("#cmp-xfont").value) || 12;
|
||||
const xInterval = $("#cmp-xinterval").value;
|
||||
try {
|
||||
const models = [...cmpSelected].join(",");
|
||||
const d = await api(`/api/compare?models=${encodeURIComponent(models)}&chart_metric=${metric}&chart_type=${ctype}`);
|
||||
if (!d.rows.length) { status.textContent = "所选模型暂无数据"; return; }
|
||||
if (!cmpCsv) {
|
||||
const models = cmpOrder.join(",");
|
||||
const d = await api(`/api/compare?models=${encodeURIComponent(models)}`);
|
||||
cmpCsv = d.chart_csv;
|
||||
}
|
||||
const payload = buildRunPayload({
|
||||
csv: cmpCsv,
|
||||
title: `模型运行速度对比(${cmpOrder.length} 个模型)`,
|
||||
series,
|
||||
xRotate: xRotate === "auto" ? "" : xRotate,
|
||||
xFont,
|
||||
xInterval: xInterval === "auto" ? "" : xInterval,
|
||||
});
|
||||
const resp = await fetch("/api/chart", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(d.chart_payload),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!resp.ok) { const j = await resp.json().catch(() => ({})); status.textContent = "❌ " + (j.error || "生成失败"); return; }
|
||||
const blob = await resp.blob();
|
||||
@@ -409,7 +562,7 @@ async function genCmpChart() {
|
||||
imgWrap.innerHTML = "";
|
||||
const img = document.createElement("img");
|
||||
img.src = cmpChartUrl;
|
||||
img.onload = () => { status.textContent = `✅ 已生成(${d.rows.length} 个模型)`; };
|
||||
img.onload = () => { status.textContent = "✅ 已生成"; };
|
||||
imgWrap.appendChild(img);
|
||||
} catch (e) { status.textContent = "❌ " + e.message; }
|
||||
}
|
||||
@@ -683,6 +836,13 @@ function bindIndex() {
|
||||
if (all) all.addEventListener("click", () => { lbModels.forEach((m) => lbSelected.add(m.key)); renderLbChips(); });
|
||||
const none = $("#lb-chart-none");
|
||||
if (none) none.addEventListener("click", () => { lbSelected.clear(); renderLbChips(); });
|
||||
// 对比车按钮
|
||||
const tb = $("#lb tbody");
|
||||
if (tb) tb.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("[data-cart]");
|
||||
if (btn) cartAdd(btn.dataset.cart);
|
||||
});
|
||||
bindCartBar();
|
||||
}
|
||||
|
||||
function bindModel() {
|
||||
@@ -720,14 +880,18 @@ function bindCapabilities() {
|
||||
|
||||
function bindCompare() {
|
||||
const all = $("#cmp-all");
|
||||
if (all) all.addEventListener("click", () => { cmpModels.forEach((m) => cmpSelected.add(m.key)); renderCmpChips(); });
|
||||
if (all) all.addEventListener("click", () => { cmpModels.forEach((m) => { cmpSelected.add(m.key); if (!cmpOrder.includes(m.key)) cmpOrder.push(m.key); }); renderCmpChips(); });
|
||||
const none = $("#cmp-none");
|
||||
if (none) none.addEventListener("click", () => { cmpSelected.clear(); renderCmpChips(); });
|
||||
if (none) none.addEventListener("click", () => { cmpSelected.clear(); cmpOrder = []; renderCmpChips(); });
|
||||
const clearCart = $("#cmp-clear-cart");
|
||||
if (clearCart) clearCart.addEventListener("click", () => { cartClear(); initCompare(); });
|
||||
const run = $("#cmp-run");
|
||||
if (run) run.addEventListener("click", runCompare);
|
||||
const gen = $("#cmp-chart-gen");
|
||||
if (gen) gen.addEventListener("click", genCmpChart);
|
||||
renderCmpColToggles();
|
||||
renderSeriesCfg();
|
||||
bindCartBar();
|
||||
}
|
||||
|
||||
function bindAdmin() {
|
||||
@@ -876,7 +1040,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
loadCapabilities();
|
||||
bindCapabilities();
|
||||
} else if (page === "compare.html") {
|
||||
loadCmpModels();
|
||||
initCompare();
|
||||
bindCompare();
|
||||
} else if (page === "admin.html") {
|
||||
checkAdmin();
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
<a href="/compare.html">⚖️ 模型对比</a>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
<a href="/admin.html" class="hint">⚙️ 后台管理</a>
|
||||
<a href="/index.html" class="hint">← 返回排行</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
Reference in New Issue
Block a user