feat: 全面打磨平台交互
- 名单导入容错:rowsToStudents 加去重/跳过统计、表头正则扩充、.0 尾巴清理;importFile 返回精确 count
- 看板:修复 .repeat(0) 空状态坏代码;新增实时点 stale 指示(手动/自动刷新失败变红 + 提示刷新中断);
手动刷新按钮加刷新中…反馈;记住自动刷新开关与间隔(localStorage + ?interval= 深链)
- 主统计页:后台静默刷新连续失败时连接指示转实时刷新中断告警;记住用户手动调整的刷新间隔
- 设置页:新增 config_test 端点 + 独立测试连接按钮(用弹窗内未保存凭据测连接,不落盘);
保存按钮加保存中…防重提交;登录失败不清旧 token
- 抽取页:重置结果加二次确认;新增复制名单按钮(两列制表符分隔,可粘到微信/Excel)
- 修复看板预先存在 JS 语法错误(少一个右括号导致整页 JS 失效)
This commit is contained in:
+74
-13
@@ -118,6 +118,8 @@ const LS = {
|
||||
};
|
||||
const LS_PROJECTS = 'ny_projects';
|
||||
const LS_BOARD = (pid, cls) => 'ny_board_' + pid + '_' + encodeURIComponent(cls);
|
||||
const LS_DASH_AUTO = 'ny_dash_auto';
|
||||
const LS_DASH_INTERVAL= 'ny_dash_interval';
|
||||
|
||||
let DASH = null;
|
||||
let CUR = readUrl();
|
||||
@@ -235,7 +237,7 @@ function membersTable(members){
|
||||
'<thead><tr><th style="width:60px">排名</th><th>姓名</th><th style="width:80px">成绩</th></tr></thead><tbody>' +
|
||||
members.map(m => {
|
||||
const rk = m.rank;
|
||||
const badge = rk === 1 ? 'rk rk-1' : (rk === 2 ? 'rk rk-2' : (rk === 3 ? 'rk rk-3' : 'rk');
|
||||
const badge = rk === 1 ? 'rk rk-1' : (rk === 2 ? 'rk rk-2' : (rk === 3 ? 'rk rk-3' : 'rk'));
|
||||
const sc = m.score;
|
||||
const cls = sc == null ? '' : (sc >= 90 ? 'score-high' : (sc >= 60 ? 'score-mid' : 'score-low'));
|
||||
const att = (m.attempts || 1) > 1;
|
||||
@@ -274,6 +276,37 @@ function closeAttempts(){
|
||||
if (m) m.classList.remove('open');
|
||||
}
|
||||
|
||||
/* ---------- 看板占位(未选问卷/班级) ---------- */
|
||||
function renderPlaceholderBoard(){
|
||||
const cards = [
|
||||
['总人数','-'],['已提交','-'],['缺考','-'],
|
||||
['提交率','-'],['平均分','-'],['合格率','-']
|
||||
];
|
||||
$('#boardSummary').innerHTML = cards.map(c =>
|
||||
'<div class="card"><span>' + c[0] + '</span><b>' + c[1] + '</b></div>').join('');
|
||||
$('#boardBody').innerHTML = '<div class="empty">请选择问卷与班级查看考试整体情况</div>';
|
||||
$('#heroTitle').textContent = '班级看板';
|
||||
$('#heroSub').textContent = '选择一个问卷与班级查看考试整体情况';
|
||||
const d = $('#liveDot'); if (d) { d.classList.remove('on','stale'); }
|
||||
}
|
||||
|
||||
/* ---------- 实时状态指示 ---------- */
|
||||
function markLive(){
|
||||
const d = $('#liveDot');
|
||||
if (d) { d.classList.remove('stale'); d.classList.add('on'); }
|
||||
setTimeout(() => { const e = $('#liveDot'); if (e) e.classList.remove('on'); }, 900);
|
||||
}
|
||||
function markStale(msg){
|
||||
const d = $('#liveDot');
|
||||
if (d) { d.classList.remove('on'); d.classList.add('stale'); }
|
||||
const lu = $('#lastUpdate');
|
||||
if (lu) lu.textContent = '刷新中断(仍显示上次数据)';
|
||||
}
|
||||
function handleAuthFail(){
|
||||
$('#boardBody').innerHTML = '<div class="empty">卷王账号未配置或登录已失效,<a href="login.php">点击重新登录</a></div>';
|
||||
setTimeout(() => { location.href = 'login.php'; }, 1200);
|
||||
}
|
||||
|
||||
/* ---------- 渲染单班看板 ---------- */
|
||||
function renderBoard(){
|
||||
const pid = $('#projectSel').value;
|
||||
@@ -282,11 +315,7 @@ function renderBoard(){
|
||||
writeUrl({ pid, cls });
|
||||
|
||||
if (!pid || !cls || !DASH) {
|
||||
$('#boardSummary').innerHTML = '<div class="card accent-blue"><span>总人数</span><b>-</b></div>'.repeat(0) ||
|
||||
'<div class="empty">请选择问卷与班级</div>';
|
||||
$('#boardBody').innerHTML = '<div class="empty">请选择问卷与班级查看考试整体情况</div>';
|
||||
$('#heroTitle').textContent = '班级看板';
|
||||
$('#heroSub').textContent = '选择一个问卷与班级查看考试整体情况';
|
||||
renderPlaceholderBoard();
|
||||
return;
|
||||
}
|
||||
LS.set('ny_board_last_pid', JSON.stringify(pid));
|
||||
@@ -384,9 +413,11 @@ function schedule(){
|
||||
const sec = parseInt($('#intervalSel').value, 10) || 15;
|
||||
timer = setInterval(refreshBoard, sec * 1000);
|
||||
}
|
||||
async function refreshBoard(){
|
||||
async function refreshBoard(manual){
|
||||
const pid = $('#projectSel').value;
|
||||
if (!pid) return;
|
||||
const btn = $('#btnRefresh');
|
||||
if (manual && btn) { btn.disabled = true; btn.textContent = '刷新中…'; }
|
||||
try {
|
||||
const fresh = await api('dashboard', { projectId: pid });
|
||||
DASH = fresh;
|
||||
@@ -403,24 +434,54 @@ async function refreshBoard(){
|
||||
}
|
||||
renderBoard();
|
||||
$('#lastUpdate').textContent = fresh.updated_at || '-';
|
||||
$('#liveDot').classList.add('on');
|
||||
setTimeout(() => $('#liveDot').classList.remove('on'), 900);
|
||||
markLive();
|
||||
} catch (e) {
|
||||
/* 静默续期:保留旧数据不打扰 */
|
||||
if (e.auth) { handleAuthFail(); return; }
|
||||
// 静默续期:保留旧数据,但把实时点变红提示中断
|
||||
markStale(e.message);
|
||||
} finally {
|
||||
if (manual && btn) { btn.disabled = false; btn.textContent = '刷新'; }
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 事件 ---------- */
|
||||
$('#projectSel').addEventListener('change', () => { CUR.pid = $('#projectSel').value; CUR.cls = ''; refreshClassSel(); schedule(); });
|
||||
$('#classSel').addEventListener('change', () => { CUR.cls = $('#classSel').value; renderBoard(); });
|
||||
$('#btnRefresh').addEventListener('click', refreshBoard);
|
||||
$('#autoRefresh').addEventListener('change', () => { writeUrl({ auto: $('#autoRefresh').checked }); schedule(); });
|
||||
$('#intervalSel').addEventListener('change', schedule);
|
||||
$('#btnRefresh').addEventListener('click', () => refreshBoard(true));
|
||||
$('#autoRefresh').addEventListener('change', () => {
|
||||
writeUrl({ auto: $('#autoRefresh').checked });
|
||||
LS.set(LS_DASH_AUTO, $('#autoRefresh').checked);
|
||||
schedule();
|
||||
});
|
||||
$('#intervalSel').addEventListener('change', () => {
|
||||
LS.set(LS_DASH_INTERVAL, parseInt($('#intervalSel').value, 10) || 15);
|
||||
schedule();
|
||||
});
|
||||
$('#btnFullscreen').addEventListener('click', toggleFullscreen);
|
||||
document.addEventListener('fullscreenchange', onFsChange);
|
||||
document.addEventListener('webkitfullscreenchange', onFsChange);
|
||||
|
||||
/* ---------- 偏好恢复(自动刷新开关 + 间隔,含 ?interval= 深链) ---------- */
|
||||
function restoreDashPrefs(){
|
||||
const qs = new URLSearchParams(location.search);
|
||||
if (qs.get('interval')) {
|
||||
$('#intervalSel').value = qs.get('interval');
|
||||
LS.set(LS_DASH_INTERVAL, parseInt($('#intervalSel').value, 10) || 15);
|
||||
} else {
|
||||
const iv = LS.get(LS_DASH_INTERVAL, 0);
|
||||
if (iv) {
|
||||
const opts = Array.prototype.slice.call($('#intervalSel').options).map(o => parseInt(o.value, 10));
|
||||
if (opts.includes(iv)) $('#intervalSel').value = String(iv);
|
||||
}
|
||||
}
|
||||
if (CUR.auto !== '0') {
|
||||
const auto = LS.get(LS_DASH_AUTO, null);
|
||||
if (auto !== null) $('#autoRefresh').checked = !!auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 启动 ---------- */
|
||||
restoreDashPrefs();
|
||||
loadProjects().then(() => {
|
||||
if (CUR.pid) {
|
||||
$('#projectSel').value = CUR.pid;
|
||||
|
||||
Reference in New Issue
Block a user