v1.2.0 对比车+运行速度对比图+图表可配置:移除前台后台管理链接(网址直达登录);新增🛒对比车(排行榜行加入,浮动条隐藏默认,对比页按加入顺序对比,无车默认前5,默认只对比预填充+解码);首页解码速度对比图改运行速度对比(预填充左轴空心柱+解码右轴实心柱+首字虚线);对比图支持柱状(空心/实心)/折线(实线/虚线)+左右双轴+三色深度+X轴旋转/间隔/字号防拥挤

This commit is contained in:
2026-09-02 13:59:57 +08:00
parent c6eef5e67f
commit 1021a037cf
7 changed files with 275 additions and 91 deletions
+17 -42
View File
@@ -135,34 +135,29 @@ def leaderboard():
keys.add(("", item))
rows = [r for r in rows if (r["provider"], r["model"]) in keys]
# 按排行榜生成柱状图 CSV(解码速度 TOP,默认全部模型,可指定)
chart_rows = rows[: int(request.args.get("chart_top", 20) or 20)]
csv_lines = ["模型, 解码速度(tok/s), 预填充速度(tok/s), 首字延迟(ms)"]
chart_rows = rows[: int(request.args.get("chart_top", 30) or 30)]
chart_csv = _speed_csv(chart_rows)
return jsonify({"ok": True, "rows": rows, "chart_csv": chart_csv})
def _speed_csv(chart_rows):
"""模型 运行速度 CSV:预填充 / 解码 / 首字 三列(前端据此配置柱/折线/双轴/颜色)"""
csv_lines = ["模型, 预填充速度(tok/s), 解码速度(tok/s), 首字延迟(ms)"]
for r in chart_rows:
label = "%s %s" % (r["provider"], r["model"])
csv_lines.append("%s, %s, %s, %s" % (
label.replace(",", " "), _fmt(r["avg_decode_speed"]),
_fmt(r["avg_prefill_speed"]), _fmt(r["avg_ttft_ms"])))
chart_csv = "\n".join(csv_lines)
bar_payload = {
"data": chart_csv, "chartType": "bar",
"title": "模型解码速度对比(%d 个模型)" % len(chart_rows),
"theme": "default", "showLegend": True, "showGrid": True, "showLabel": True,
"seriesTypes": ["bar", "bar", "line"],
"seriesAxis": [0, 0, 1],
"seriesStyles": ["solid", "hollow", "dashed"],
"width": 1100, "height": 560, "pixelRatio": 2,
}
return jsonify({"ok": True, "rows": rows, "chart_csv": chart_csv,
"bar_payload": bar_payload})
label.replace(",", " "), _fmt(r["avg_prefill_speed"]),
_fmt(r["avg_decode_speed"]), _fmt(r["avg_ttft_ms"])))
return "\n".join(csv_lines)
# ───────────────────────── 模型对比(硬件 + 可去留对比项) ─────────────────────────
@app.route("/api/compare")
def compare():
"""返回指定模型(默认全部)的聚合对比行 + 图表数据
models: 逗号分隔 provider|modelchart_metric: 解码/预填充/首字;chart_type: bar/line
"""返回指定模型(默认全部)的聚合对比行 + 运行速度 CSV
models: 逗号分隔 provider|model(保持传入顺序=对比车添加顺序);
不传时默认全部(按解码速度排序,前端默认取前5)。
"""
sel = request.args.get("models", "").strip()
keys = []
@@ -176,35 +171,15 @@ def compare():
keys.append((p.strip(), m.strip()))
else:
keys.append(("", item))
rows = db.compare_models(keys) # 保持传入顺序(对比车顺序)
else:
lb = db.leaderboard(limit=500)
keys = [(r["provider"], r["model"]) for r in lb]
rows = db.compare_models(keys)
# 有硬件优先展示
rows.sort(key=lambda r: (r.get("avg_decode_speed") or 0), reverse=True)
# 图表数据(按所选指标)
metric = request.args.get("chart_metric", "avg_decode_speed")
ctype = request.args.get("chart_type", "bar")
metric_name = {"avg_decode_speed": "解码速度", "avg_prefill_speed": "预填充速度",
"avg_ttft_ms": "首字延迟", "best_decode": "最佳解码"}.get(metric, "解码速度")
unit = "tok/s" if metric in ("avg_decode_speed", "avg_prefill_speed", "best_decode") else "ms"
chart_rows = rows[: int(request.args.get("chart_top", 50) or 50)]
csv_lines = ["模型, %s(%s)" % (metric_name, unit)]
for r in chart_rows:
label = "%s %s" % (r["provider"], r["model"])
csv_lines.append("%s, %s" % (label.replace(",", " "), _fmt(r.get(metric))))
chart_csv = "\n".join(csv_lines)
chart_payload = {
"data": chart_csv, "chartType": "bar" if ctype != "line" else "line",
"title": "模型%s对比(%d 个模型)" % (metric_name, len(chart_rows)),
"theme": "default", "showLegend": False, "showGrid": True, "showLabel": True,
"smoothLine": True,
"seriesTypes": ["bar"] if ctype != "line" else ["line"],
"width": 1100, "height": 520, "pixelRatio": 2,
}
return jsonify({"ok": True, "rows": rows, "chart_csv": chart_csv,
"chart_payload": chart_payload, "metric": metric})
chart_csv = _speed_csv(rows[: int(request.args.get("chart_top", 60) or 60)])
return jsonify({"ok": True, "rows": rows, "chart_csv": chart_csv})
@app.route("/api/model")
-1
View File
@@ -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
View File
@@ -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>
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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">