Files
hangbiao/app/static/js/app.js
2026-09-22 10:48:18 +08:00

113 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function closeDrawer() {
const el = document.getElementById("drawer");
if (el) el.classList.remove("show");
}
function sparkline(svg, exams, zs, xs, lastRank) {
const w = 520, h = 220, pad = 28;
const vals = [];
zs.forEach((z, i) => { if (z !== null && z !== undefined) vals.push(z); });
xs.forEach((z) => { if (z !== null && z !== undefined) vals.push(z); });
if (!vals.length) return;
const min = Math.min(...vals, -0.5) - 0.3;
const max = Math.max(...vals, 0.5) + 0.3;
const xAt = (i) => pad + (i * (w - pad * 2)) / Math.max(exams.length - 1, 1);
const yAt = (v) => pad + (max - v) * (h - pad * 2) / (max - min);
const pts = (arr) => arr.map((v, i) => v == null ? null : `${xAt(i)},${yAt(v)}`).filter(Boolean).join(" ");
let raw = "";
zs.forEach((v, i) => {
if (v == null) return;
raw += `<circle cx="${xAt(i)}" cy="${yAt(v)}" r="4.5" fill="#c47b17"></circle>`;
});
const pathXs = xs.map((v, i) => (v == null ? null : [xAt(i), yAt(v)])).filter(Boolean);
let d = "";
pathXs.forEach((p, i) => { d += (i === 0 ? `M${p[0]} ${p[1]}` : ` L${p[0]} ${p[1]}`); });
const labels = exams.map((e, i) => {
const short = String(e).replace("-总分", "").replace("高", "");
return `<text x="${xAt(i)}" y="${h - 6}" text-anchor="middle" font-size="10" fill="#3a4a63">${short}</text>`;
}).join("");
svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
svg.innerHTML = `
<line x1="${pad}" y1="${yAt(0)}" x2="${w - pad}" y2="${yAt(0)}" stroke="#d8cfbf" stroke-dasharray="4 4"></line>
<path d="${d}" fill="none" stroke="#1c6f63" stroke-width="2.4"></path>
${raw}
${labels}
<text x="${pad}" y="16" font-size="11" fill="#3a4a63">标准分轨迹 · 橙点考试,绿线滤波估计</text>
`;
}
async function openStudent(aid, sid) {
const res = await fetch(`/analysis/${aid}/api/student/${encodeURIComponent(sid)}`);
if (!res.ok) return;
const s = await res.json();
const box = document.getElementById("drawer");
const body = document.getElementById("drawer-body");
const probs = Object.entries(s.probs || {}).map(([k, v]) => `${k}: ${(v * 100).toFixed(1)}%`).join(" ");
body.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:start">
<div>
<h2 style="margin:0">${s.name} <span class="tiny">${s.sid}</span></h2>
<div class="tiny">${s.class_name}班 · ${s.group} · ${s.status || "在籍"} · ${s.subjects || ""}</div>
</div>
<button class="ghost" onclick="closeDrawer()">关闭</button>
</div>
<p class="explain" style="margin-top:14px">
绿线是卡尔曼估计的稳定水平。如果橙点突然跳开绿线,系统会当成发挥波动,不会立刻改判断。
达线概率 ${probs || "—"}。
</p>
<svg class="spark" id="spark"></svg>
<table class="table">
<thead><tr><th>考试</th><th>分数</th><th>方向内名次</th><th>标准分</th><th>滤波</th></tr></thead>
<tbody id="spark-rows"></tbody>
</table>
`;
const rows = document.getElementById("spark-rows");
(s.z || []).forEach((z, i) => {
const exam = (window.HB_EXAMS || [])[i] || `第${i + 1}次`;
const score = s.scores ? s.scores[exam] : "";
const rank = (s.ranks || [])[i];
const kf = (s.kf_xs || [])[i];
rows.insertAdjacentHTML("beforeend", `<tr>
<td>${exam}</td><td>${score ?? "缺考"}</td><td>${rank ?? "—"}</td>
<td>${z == null ? "—" : z.toFixed(2)}</td>
<td>${kf == null ? "—" : kf.toFixed(2)}</td>
</tr>`);
});
sparkline(document.getElementById("spark"), window.HB_EXAMS || [], s.z || [], s.kf_xs || []);
box.classList.add("show");
}
async function openClass(aid, group, target, className) {
const url = `/analysis/${aid}/api/class?group=${encodeURIComponent(group)}&target=${encodeURIComponent(target)}&class_name=${encodeURIComponent(className)}`;
const res = await fetch(url);
if (!res.ok) return;
const data = await res.json();
const box = document.getElementById("drawer");
const body = document.getElementById("drawer-body");
const rows = data.students.map((s) => {
const p = ((s.probs || {})[target] || 0) * 100;
const tag = p >= 80 ? "high" : p >= 20 ? "mid" : "";
return `<tr class="clickable" onclick="openStudent(${aid}, '${s.sid}')">
<td>${s.name}</td><td>${s.status || ""}</td>
<td>${(s.ranks || []).slice(-1)[0] ?? "—"}</td>
<td><span class="tag ${tag}">${p.toFixed(1)}%</span></td>
</tr>`;
}).join("");
body.innerHTML = `
<div style="display:flex;justify-content:space-between">
<h2 style="margin:0">${className}班学生概率</h2>
<button class="ghost" onclick="closeDrawer()">关闭</button>
</div>
<p class="tiny">点击学生可看成绩轨迹。共 ${data.n} 人,下列按达线概率排序。</p>
<table class="table">
<thead><tr><th>姓名</th><th>学籍</th><th>最近名次</th><th>达线概率</th></tr></thead>
<tbody>${rows}</tbody>
</table>
`;
box.classList.add("show");
}
document.addEventListener("click", (e) => {
if (e.target.id === "drawer") closeDrawer();
});