feat: 全面打磨平台交互
- 名单导入容错:rowsToStudents 加去重/跳过统计、表头正则扩充、.0 尾巴清理;importFile 返回精确 count
- 看板:修复 .repeat(0) 空状态坏代码;新增实时点 stale 指示(手动/自动刷新失败变红 + 提示刷新中断);
手动刷新按钮加刷新中…反馈;记住自动刷新开关与间隔(localStorage + ?interval= 深链)
- 主统计页:后台静默刷新连续失败时连接指示转实时刷新中断告警;记住用户手动调整的刷新间隔
- 设置页:新增 config_test 端点 + 独立测试连接按钮(用弹窗内未保存凭据测连接,不落盘);
保存按钮加保存中…防重提交;登录失败不清旧 token
- 抽取页:重置结果加二次确认;新增复制名单按钮(两列制表符分隔,可粘到微信/Excel)
- 修复看板预先存在 JS 语法错误(少一个右括号导致整页 JS 失效)
This commit is contained in:
+57
-3
@@ -21,7 +21,8 @@
|
||||
sortAsc: false,
|
||||
timer: null,
|
||||
loading: false,
|
||||
lastSig: ''
|
||||
lastSig: '',
|
||||
connFail: 0
|
||||
};
|
||||
|
||||
/* ---------------- 本地存储 ---------------- */
|
||||
@@ -41,6 +42,7 @@
|
||||
const LS_LASTQID = 'ny_last_qid';
|
||||
const LS_FILTERS = 'ny_filters'; // {cls, grp} -> 跟随 projectId
|
||||
const LS_LAYOUT = 'ny_layout';
|
||||
const LS_INTERVAL = 'ny_interval'; // 用户手动调整的刷新间隔(跟随本机)
|
||||
const LS_ROSTER = 'ny_roster_cache'; // 名单 cache(用于首屏补齐班级/组别下拉)
|
||||
const dataKey = pid => 'ny_data_' + pid;
|
||||
|
||||
@@ -243,6 +245,7 @@
|
||||
try {
|
||||
const d = await api('data', { params: { projectId: pid, nameQid: $('#nameFieldSel').value } });
|
||||
applyData(d);
|
||||
state.connFail = 0;
|
||||
LS.set(dataKey(pid), d);
|
||||
LS.set(LS_LASTPID, pid);
|
||||
LS.set(LS_LASTQID, $('#nameFieldSel').value || '');
|
||||
@@ -259,6 +262,9 @@
|
||||
if (!silent) setConn('conn-err', '数据获取失败');
|
||||
showAlert(e.message);
|
||||
} else {
|
||||
// 后台静默刷新失败:保留缓存数据,但把连接指示转为"中断"告警,成功即恢复
|
||||
state.connFail++;
|
||||
if (state.connFail >= 1) setConn('conn-warn', '⚠️ 实时刷新中断 · 已显示本地缓存');
|
||||
showAlert('⚠️ 后台刷新失败,已显示本地缓存:' + e.message);
|
||||
}
|
||||
} finally {
|
||||
@@ -697,7 +703,10 @@
|
||||
// 用缓存数据先快速渲染,再后台拉新数据
|
||||
refresh();
|
||||
});
|
||||
$('#intervalSel').addEventListener('change', schedule);
|
||||
$('#intervalSel').addEventListener('change', () => {
|
||||
LS.set(LS_INTERVAL, parseInt($('#intervalSel').value, 10) || 15);
|
||||
schedule();
|
||||
});
|
||||
$('#autoRefresh').addEventListener('change', e => {
|
||||
if (e.target.checked) { schedule(); refresh(true); }
|
||||
else if (state.timer) clearInterval(state.timer);
|
||||
@@ -740,6 +749,8 @@
|
||||
|
||||
/* 设置保存 */
|
||||
$('#btnSaveCfg').addEventListener('click', async () => {
|
||||
const btnSave = $('#btnSaveCfg');
|
||||
btnSave.disabled = true; btnSave.textContent = '保存中…';
|
||||
const pass = $('#cfgPass').value;
|
||||
const payload = {
|
||||
page_size: parseInt($('#cfgPageSize').value, 10) || 200,
|
||||
@@ -758,7 +769,7 @@
|
||||
let resp;
|
||||
try {
|
||||
resp = await api('config_save', { method: 'POST', body: payload });
|
||||
} catch (e) { showAlert(e.message); return; }
|
||||
} catch (e) { showAlert(e.message); btnSave.disabled = false; btnSave.textContent = '保存'; return; }
|
||||
$('#cfgPass').value = ''; // 提交后清空密码框
|
||||
const note = (resp && resp.conn_changed) ? '账号/连接已更新' : '设置已保存';
|
||||
if (resp && resp.ping) {
|
||||
@@ -783,8 +794,44 @@
|
||||
if (!resp || !resp.ping || resp.ping.ok) {
|
||||
$('#settingsModal').classList.remove('open');
|
||||
}
|
||||
btnSave.disabled = false; btnSave.textContent = '保存';
|
||||
});
|
||||
|
||||
/* 独立测试连接(使用弹窗内尚未保存的账号/密码,不落盘) */
|
||||
async function testConnection() {
|
||||
const btn = $('#btnTestConn');
|
||||
const box = $('#pingResult');
|
||||
if (!btn || !box) return;
|
||||
btn.disabled = true; btn.textContent = '测试中…';
|
||||
box.classList.remove('hidden', 'err'); box.classList.add('loading');
|
||||
box.textContent = '正在连接卷王系统测试…';
|
||||
try {
|
||||
// 仅传可编辑的连接字段;base_url / api_prefix 在弹窗中只读,沿用已保存值
|
||||
const r = await api('config_test', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
username: $('#cfgUser').value.trim(),
|
||||
password: $('#cfgPass').value,
|
||||
verify_ssl: $('#cfgSsl').checked
|
||||
}
|
||||
});
|
||||
box.classList.remove('loading');
|
||||
if (r && r.connected) {
|
||||
box.classList.remove('err');
|
||||
box.textContent = '✅ 连接成功:检测到 ' + r.projects + ' 个问卷 / 用时 ' + r.elapsed + 'ms';
|
||||
} else {
|
||||
box.classList.add('err');
|
||||
box.textContent = '❌ 连接失败:' + ((r && r.msg) || '未知错误');
|
||||
}
|
||||
} catch (e) {
|
||||
box.classList.remove('loading'); box.classList.add('err');
|
||||
box.textContent = '❌ 测试请求异常:' + e.message;
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = '测试连接';
|
||||
}
|
||||
}
|
||||
$('#btnTestConn').addEventListener('click', testConnection);
|
||||
|
||||
// 收集"各试卷独立满分" -> {projectId: score}
|
||||
function collectFullScores() {
|
||||
const out = {};
|
||||
@@ -871,6 +918,13 @@
|
||||
// 2) 支持 ?projectId=xxx&nameQid=yyy 直接打开某份问卷
|
||||
const qs = new URLSearchParams(location.search);
|
||||
if (qs.get('interval')) $('#intervalSel').value = qs.get('interval');
|
||||
else {
|
||||
const iv = LS.get(LS_INTERVAL, 0);
|
||||
if (iv) {
|
||||
const opts = $$('#intervalSel option').map(o => parseInt(o.value, 10));
|
||||
if (opts.indexOf(iv) >= 0) $('#intervalSel').value = String(iv);
|
||||
}
|
||||
}
|
||||
state.deeplinkProject = qs.get('projectId') || '';
|
||||
state.deeplinkQid = qs.get('nameQid') || '';
|
||||
|
||||
|
||||
@@ -710,6 +710,9 @@
|
||||
|
||||
// 重置结果:清空本次结果 + 累计已抽取(历史记录保留)
|
||||
$('#btnReset').addEventListener('click', () => {
|
||||
if (state.picked.length || state.history.size) {
|
||||
if (!confirm('确定重置?将清空本次结果与累计已抽取的 ' + state.history.size + ' 人(历史记录仍保留)。')) return;
|
||||
}
|
||||
state.picked = [];
|
||||
state.history = new Set();
|
||||
renderResult();
|
||||
@@ -721,6 +724,37 @@
|
||||
showAlert('已重置结果与累计已抽取(历史记录仍保留)', 'info');
|
||||
});
|
||||
|
||||
// 复制名单:两列「姓名 / 登陆账号」,制表符分隔,便于直接粘贴到 Excel / 微信
|
||||
$('#btnCopy').addEventListener('click', async () => {
|
||||
if (!state.picked.length) {
|
||||
showAlert('请先点击「🎲 开始抽取」再复制');
|
||||
return;
|
||||
}
|
||||
const lines = state.picked.map(m =>
|
||||
(m.name || '') + '\t' + (m.login || m.sid || ''));
|
||||
const text = lines.join('\n');
|
||||
let ok = false;
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
ok = true;
|
||||
}
|
||||
} catch (e) { /* 降级到 textarea */ }
|
||||
if (!ok) {
|
||||
try {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.opacity = '0';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
ok = document.execCommand('copy');
|
||||
ta.remove();
|
||||
} catch (e) { ok = false; }
|
||||
}
|
||||
showAlert(ok ? ('已复制 ' + state.picked.length + ' 人的姓名 / 登陆账号到剪贴板') : '复制失败,请手动选择结果复制', ok ? 'info' : '');
|
||||
});
|
||||
|
||||
// 清空历史(顶栏)
|
||||
$('#btnClearLog').addEventListener('click', () => {
|
||||
if (!state.log.length) return showAlert('暂无历史记录');
|
||||
|
||||
@@ -80,6 +80,8 @@ body {
|
||||
.conn-err { background: var(--danger-weak); color: var(--danger); }
|
||||
.conn-err i { background: var(--danger); }
|
||||
.conn-load i { background: var(--warn); animation: blink 1s infinite; }
|
||||
.conn-warn { background: var(--warn-weak, #fff4e0); color: var(--warn, #e08c1a); }
|
||||
.conn-warn i { background: var(--warn, #e08c1a); animation: blink 1s infinite; }
|
||||
@keyframes blink { 50% { opacity: .25; } }
|
||||
|
||||
/* ---------- 按钮 ---------- */
|
||||
@@ -231,6 +233,7 @@ select:focus, input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px
|
||||
.updated { font-size: 12px; color: var(--text-3); display: flex; align-items: center; gap: 6px; }
|
||||
.dot { width: 7px; height: 7px; border-radius: 50%; background: #cbd3e2; display: inline-block; }
|
||||
.dot.on { background: var(--ok); animation: pulse 1.6s infinite; }
|
||||
.dot.stale { background: var(--danger); animation: blink 1s infinite; }
|
||||
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
|
||||
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
@@ -367,6 +370,7 @@ tr.clickable:hover td { background: var(--border-2); }
|
||||
background: var(--ok-weak); color: #0d7a4d; border: 1px solid #c8ecda;
|
||||
}
|
||||
.ping-result.err { background: var(--danger-weak); color: #a3342c; border-color: #f5d3cf; }
|
||||
.ping-result.loading { background: var(--border-2); color: var(--text-2); border-color: var(--border-2); }
|
||||
|
||||
/* ---------- 名单 ---------- */
|
||||
.upload-zone {
|
||||
|
||||
Reference in New Issue
Block a user