Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64d0116c0e | ||
|
|
7345f9c29b |
@@ -93,7 +93,8 @@ try {
|
|||||||
'name' => ny_str($p['name'] ?? '') ?: '未命名',
|
'name' => ny_str($p['name'] ?? '') ?: '未命名',
|
||||||
'mode' => ny_str($p['mode'] ?? ''),
|
'mode' => ny_str($p['mode'] ?? ''),
|
||||||
'total' => (int)($p['total'] ?? 0),
|
'total' => (int)($p['total'] ?? 0),
|
||||||
'status'=> (int)($p['status'] ?? 0),
|
'status' => (int)($p['status'] ?? 0),
|
||||||
|
'parent_id' => (string)($p['parentId'] ?? '0'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
ny_ok($out);
|
ny_ok($out);
|
||||||
|
|||||||
+214
-35
@@ -24,6 +24,26 @@
|
|||||||
lastSig: ''
|
lastSig: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ---------------- 本地存储 ---------------- */
|
||||||
|
// 浏览器关闭后仍然保留,账号/密码等敏感配置除外;进入页面时优先读缓存,再后台异步刷新
|
||||||
|
const LS = {
|
||||||
|
get(k, def) {
|
||||||
|
try { const v = localStorage.getItem(k); return v == null ? def : JSON.parse(v); }
|
||||||
|
catch (e) { return def; }
|
||||||
|
},
|
||||||
|
set(k, v) {
|
||||||
|
try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) { /* 隐私模式 */ }
|
||||||
|
},
|
||||||
|
del(k) { try { localStorage.removeItem(k); } catch (e) {} }
|
||||||
|
};
|
||||||
|
const LS_PROJECTS = 'ny_projects';
|
||||||
|
const LS_LASTPID = 'ny_last_pid';
|
||||||
|
const LS_LASTQID = 'ny_last_qid';
|
||||||
|
const LS_FILTERS = 'ny_filters'; // {cls, grp} -> 跟随 projectId
|
||||||
|
const LS_LAYOUT = 'ny_layout';
|
||||||
|
const LS_ROSTER = 'ny_roster_cache'; // 名单 cache(用于首屏补齐班级/组别下拉)
|
||||||
|
const dataKey = pid => 'ny_data_' + pid;
|
||||||
|
|
||||||
/* ---------------- 通用 ---------------- */
|
/* ---------------- 通用 ---------------- */
|
||||||
async function api(action, opts) {
|
async function api(action, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
@@ -73,11 +93,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 问卷列表 ---------------- */
|
/* ---------------- 问卷列表 ---------------- */
|
||||||
async function loadProjects() {
|
// 把数组渲染到 #projectSel(抽出便于缓存复用)
|
||||||
setConn('conn-load', '连接中…');
|
function renderProjects() {
|
||||||
try {
|
|
||||||
const list = await api('projects');
|
|
||||||
state.projects = list || [];
|
|
||||||
const sel = $('#projectSel');
|
const sel = $('#projectSel');
|
||||||
const keep = sel.value;
|
const keep = sel.value;
|
||||||
sel.innerHTML = '';
|
sel.innerHTML = '';
|
||||||
@@ -87,33 +104,86 @@
|
|||||||
const ph = document.createElement('option');
|
const ph = document.createElement('option');
|
||||||
ph.value = ''; ph.textContent = '请选择问卷 / 考试';
|
ph.value = ''; ph.textContent = '请选择问卷 / 考试';
|
||||||
sel.appendChild(ph);
|
sel.appendChild(ph);
|
||||||
|
|
||||||
|
// 双策略识别文件夹:
|
||||||
|
// 1) mode 字段含 'folder'(卷王默认)
|
||||||
|
// 2) 被其他项目作为 parent_id 引用(部分版本无 mode 字段,回退推断)
|
||||||
|
const isFolder = p => (p.mode || '').toLowerCase().includes('folder');
|
||||||
|
const parentIds = new Set();
|
||||||
state.projects.forEach(p => {
|
state.projects.forEach(p => {
|
||||||
|
const pid = (p.parent_id || '0');
|
||||||
|
if (pid && pid !== '0' && pid !== '') parentIds.add(pid);
|
||||||
|
});
|
||||||
|
const folderSet = new Set();
|
||||||
|
state.projects.forEach(p => {
|
||||||
|
if (isFolder(p) || parentIds.has(p.id)) folderSet.add(p.id);
|
||||||
|
});
|
||||||
|
const childOf = fid => state.projects.filter(p =>
|
||||||
|
!folderSet.has(p.id) && (p.parent_id || '0') === fid);
|
||||||
|
|
||||||
|
const mkOpt = p => {
|
||||||
const o = document.createElement('option');
|
const o = document.createElement('option');
|
||||||
o.value = p.id;
|
o.value = p.id;
|
||||||
o.textContent = (p.mode ? '[' + p.mode + '] ' : '') + p.name + '(' + (p.total || 0) + '份)';
|
o.textContent = (p.mode ? '[' + p.mode + '] ' : '') + p.name + '(' + (p.total || 0) + '份)';
|
||||||
sel.appendChild(o);
|
return o;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根项(不在任何文件夹里的)
|
||||||
|
state.projects.filter(p => !folderSet.has(p.id))
|
||||||
|
.forEach(p => sel.appendChild(mkOpt(p)));
|
||||||
|
// 文件夹及子项(空文件夹不显示,避免点击报错)
|
||||||
|
state.projects.filter(p => folderSet.has(p.id)).forEach(f => {
|
||||||
|
const kids = childOf(f.id);
|
||||||
|
if (!kids.length) return;
|
||||||
|
const og = document.createElement('optgroup');
|
||||||
|
og.label = '📁 ' + f.name;
|
||||||
|
kids.forEach(p => og.appendChild(mkOpt(p)));
|
||||||
|
sel.appendChild(og);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (keep && state.projects.some(p => p.id === keep)) sel.value = keep;
|
if (keep && state.projects.some(p => p.id === keep)) sel.value = keep;
|
||||||
else if (state.deeplinkProject && state.projects.some(p => p.id === state.deeplinkProject)) sel.value = state.deeplinkProject;
|
else if (state.deeplinkProject && state.projects.some(p => p.id === state.deeplinkProject)) sel.value = state.deeplinkProject;
|
||||||
setConn('conn-on', '已连接 · ' + state.projects.length + ' 个问卷');
|
else {
|
||||||
|
const last = LS.get(LS_LASTPID, '');
|
||||||
|
if (last && state.projects.some(p => p.id === last)) sel.value = last;
|
||||||
|
}
|
||||||
|
const surveyCount = state.projects.filter(p => !((p.mode || '').toLowerCase().includes('folder'))).length;
|
||||||
|
setConn('conn-on', (state._fromCache ? '缓存 · ' : '') + '已连接 · ' + surveyCount + ' 个问卷');
|
||||||
showAlert('');
|
showAlert('');
|
||||||
if (sel.value) refresh();
|
if (sel.value) refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProjects() {
|
||||||
|
setConn('conn-load', '连接中…');
|
||||||
|
|
||||||
|
// 1) 先读本地缓存 → 立刻渲染,避免空白
|
||||||
|
const cached = LS.get(LS_PROJECTS, []);
|
||||||
|
if (Array.isArray(cached) && cached.length) {
|
||||||
|
state.projects = cached;
|
||||||
|
state._fromCache = true;
|
||||||
|
renderProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 再异步刷新
|
||||||
|
try {
|
||||||
|
const list = await api('projects');
|
||||||
|
state.projects = list || [];
|
||||||
|
state._fromCache = false;
|
||||||
|
LS.set(LS_PROJECTS, state.projects);
|
||||||
|
renderProjects();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!cached || !cached.length) {
|
||||||
setConn('conn-err', '连接失败');
|
setConn('conn-err', '连接失败');
|
||||||
showAlert('无法连接卷王系统:' + e.message + '\n请点击右上角【设置】检查系统地址、接口前缀与账号密码。');
|
showAlert('无法连接卷王系统:' + e.message + '\n请点击右上角【设置】检查系统地址、接口前缀与账号密码。');
|
||||||
|
} else {
|
||||||
|
showAlert('⚠️ 后台刷新失败,已显示本地缓存:' + e.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 数据刷新 ---------------- */
|
/* ---------------- 数据刷新 ---------------- */
|
||||||
async function refresh(silent) {
|
function applyData(d, opts) {
|
||||||
const pid = $('#projectSel').value;
|
opts = opts || {};
|
||||||
if (!pid) return;
|
|
||||||
if (state.loading) return;
|
|
||||||
state.loading = true;
|
|
||||||
if (!silent) setConn('conn-load', '拉取数据…');
|
|
||||||
try {
|
|
||||||
const d = await api('data', { params: { projectId: pid, nameQid: $('#nameFieldSel').value } });
|
|
||||||
state.questions = d.questions || [];
|
state.questions = d.questions || [];
|
||||||
state.rows = d.rows || [];
|
state.rows = d.rows || [];
|
||||||
state.absent = d.absent || [];
|
state.absent = d.absent || [];
|
||||||
@@ -121,7 +191,6 @@
|
|||||||
state.roster = d.roster || [];
|
state.roster = d.roster || [];
|
||||||
state.fullScore = (d.config && d.config.full_score) ? (parseInt(d.config.full_score, 10) || 100) : 100;
|
state.fullScore = (d.config && d.config.full_score) ? (parseInt(d.config.full_score, 10) || 100) : 100;
|
||||||
|
|
||||||
// 计算本次刷新新增的 sid(用于绿色底纹);首次加载不标记
|
|
||||||
const cur = new Set(state.rows.map(r => r.sid).filter(Boolean));
|
const cur = new Set(state.rows.map(r => r.sid).filter(Boolean));
|
||||||
if (state.hasPrev) state.newSids = new Set(Array.from(cur).filter(s => !state.prevSids.has(s)));
|
if (state.hasPrev) state.newSids = new Set(Array.from(cur).filter(s => !state.prevSids.has(s)));
|
||||||
else state.newSids = new Set();
|
else state.newSids = new Set();
|
||||||
@@ -132,16 +201,47 @@
|
|||||||
renderTable();
|
renderTable();
|
||||||
state.prevSids = cur;
|
state.prevSids = cur;
|
||||||
populateClasses();
|
populateClasses();
|
||||||
|
populateGroups();
|
||||||
renderAbsent();
|
renderAbsent();
|
||||||
renderUnmatched();
|
renderUnmatched();
|
||||||
renderHints();
|
renderHints();
|
||||||
|
|
||||||
$('#lastUpdate').textContent = d.updated_at;
|
$('#lastUpdate').textContent = d.updated_at || '—';
|
||||||
setConn('conn-on', '实时 · ' + d.project.name);
|
setConn('conn-on', (opts.fromCache ? '缓存 · ' : '') + '实时 · ' + (d.project && d.project.name || ''));
|
||||||
showAlert('');
|
if (!opts.fromCache) showAlert('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh(silent) {
|
||||||
|
const pid = $('#projectSel').value;
|
||||||
|
if (!pid) return;
|
||||||
|
if (state.loading && !silent) return;
|
||||||
|
state.loading = true;
|
||||||
|
if (!silent) setConn('conn-load', '拉取数据…');
|
||||||
|
|
||||||
|
// 1) 先读本地缓存 → 立即渲染,刷新期间保持视觉稳定
|
||||||
|
const cached = LS.get(dataKey(pid), null);
|
||||||
|
if (cached && cached.updated_at) {
|
||||||
|
try {
|
||||||
|
applyData(cached, { fromCache: true });
|
||||||
|
} catch (e) { /* 旧格式缓存忽略 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 后台异步拉真实数据
|
||||||
|
try {
|
||||||
|
const d = await api('data', { params: { projectId: pid, nameQid: $('#nameFieldSel').value } });
|
||||||
|
applyData(d);
|
||||||
|
LS.set(dataKey(pid), d);
|
||||||
|
LS.set(LS_LASTPID, pid);
|
||||||
|
LS.set(LS_LASTQID, $('#nameFieldSel').value || '');
|
||||||
|
// 名单缓存一份(便于刷新前/失败时仍能渲染班级/组别下拉)
|
||||||
|
LS.set(LS_ROSTER, { ts: Date.now(), students: d.roster || [] });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!cached) {
|
||||||
if (!silent) setConn('conn-err', '数据获取失败');
|
if (!silent) setConn('conn-err', '数据获取失败');
|
||||||
showAlert(e.message);
|
showAlert(e.message);
|
||||||
|
} else {
|
||||||
|
showAlert('⚠️ 后台刷新失败,已显示本地缓存:' + e.message);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
$('#liveDot').classList.add('on');
|
$('#liveDot').classList.add('on');
|
||||||
@@ -167,8 +267,10 @@
|
|||||||
o.textContent = (q.title || q.id) + '(' + (q.type || '') + ')';
|
o.textContent = (q.title || q.id) + '(' + (q.type || '') + ')';
|
||||||
sel.appendChild(o);
|
sel.appendChild(o);
|
||||||
});
|
});
|
||||||
|
const lastQid = LS.get(LS_LASTQID, '');
|
||||||
if (keep) sel.value = keep;
|
if (keep) sel.value = keep;
|
||||||
else if (state.deeplinkQid) sel.value = state.deeplinkQid;
|
else if (state.deeplinkQid) sel.value = state.deeplinkQid;
|
||||||
|
else if (lastQid && (cands.some(c => c.id === lastQid) || (d.questions || []).some(q => q.id === lastQid))) sel.value = lastQid;
|
||||||
else if (d.name_qid) sel.value = d.name_qid;
|
else if (d.name_qid) sel.value = d.name_qid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,15 +282,52 @@
|
|||||||
const vals = Array.from(set).sort((a, b) => a.localeCompare(b, 'zh', { numeric: true }));
|
const vals = Array.from(set).sort((a, b) => a.localeCompare(b, 'zh', { numeric: true }));
|
||||||
sel.innerHTML = '<option value="">全部班级</option>' +
|
sel.innerHTML = '<option value="">全部班级</option>' +
|
||||||
vals.map(v => '<option value="' + esc(v) + '">' + esc(v) + '</option>').join('');
|
vals.map(v => '<option value="' + esc(v) + '">' + esc(v) + '</option>').join('');
|
||||||
|
// 优先保留用户当前已选项;否则按本地缓存恢复(跟随 projectId)
|
||||||
|
const pid = $('#projectSel').value;
|
||||||
|
const cache = pid ? (LS.get(LS_FILTERS, {})[pid] || {}) : {};
|
||||||
if (vals.indexOf(cur) >= 0) sel.value = cur;
|
if (vals.indexOf(cur) >= 0) sel.value = cur;
|
||||||
|
else if (cache.cls && vals.indexOf(cache.cls) >= 0) sel.value = cache.cls;
|
||||||
else sel.value = '';
|
else sel.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按当前所选班级(或无)客户端重算统计卡片
|
// 组别下拉:仅显示当前所选班级内出现的组别("全部班级" 时显示所有组别)
|
||||||
function computeDisplayStats(cls) {
|
function populateGroups() {
|
||||||
|
const cls = $('#clsSel').value;
|
||||||
|
const grpSel = $('#grpSel');
|
||||||
|
if (!grpSel) return;
|
||||||
|
const cur = grpSel.value;
|
||||||
|
const set = new Set();
|
||||||
|
(state.roster || []).forEach(s => {
|
||||||
|
if (cls && (s.cls || '') !== cls) return;
|
||||||
|
if (s.grp) set.add(s.grp);
|
||||||
|
});
|
||||||
|
const vals = Array.from(set).sort();
|
||||||
|
grpSel.innerHTML = '<option value="">全部组别</option>' +
|
||||||
|
vals.map(v => '<option value="' + esc(v) + '">' + esc(v) + '</option>').join('');
|
||||||
|
const pid = $('#projectSel').value;
|
||||||
|
const cache = pid ? (LS.get(LS_FILTERS, {})[pid] || {}) : {};
|
||||||
|
if (vals.indexOf(cur) >= 0) grpSel.value = cur;
|
||||||
|
else if (cache.grp && vals.indexOf(cache.grp) >= 0) grpSel.value = cache.grp;
|
||||||
|
else grpSel.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function rememberFilters() {
|
||||||
|
const pid = $('#projectSel').value;
|
||||||
|
if (!pid) return;
|
||||||
|
const all = LS.get(LS_FILTERS, {});
|
||||||
|
all[pid] = { cls: $('#clsSel').value, grp: $('#grpSel').value, ts: Date.now() };
|
||||||
|
LS.set(LS_FILTERS, all);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按当前所选班级 + 组别(或无)客户端重算统计卡片
|
||||||
|
function computeDisplayStats(cls, grp) {
|
||||||
const allStudents = state.roster || [];
|
const allStudents = state.roster || [];
|
||||||
const rows = cls ? state.rows.filter(r => (r.cls || '') === cls) : state.rows;
|
let students = cls ? allStudents.filter(s => (s.cls || '') === cls) : allStudents;
|
||||||
const students = cls ? allStudents.filter(s => (s.cls || '') === cls) : allStudents;
|
let rows = cls ? state.rows.filter(r => (r.cls || '') === cls) : state.rows;
|
||||||
|
if (grp) {
|
||||||
|
students = students.filter(s => (s.grp || '') === grp);
|
||||||
|
rows = rows.filter(r => (r.grp || '') === grp);
|
||||||
|
}
|
||||||
const total = students.length;
|
const total = students.length;
|
||||||
const submitted = rows.length;
|
const submitted = rows.length;
|
||||||
const absent = Math.max(0, total - submitted);
|
const absent = Math.max(0, total - submitted);
|
||||||
@@ -222,12 +361,12 @@
|
|||||||
|
|
||||||
function renderStats() {
|
function renderStats() {
|
||||||
const cls = $('#clsSel').value;
|
const cls = $('#clsSel').value;
|
||||||
const st = computeDisplayStats(cls);
|
const grp = $('#grpSel').value;
|
||||||
const set = (id, v) => { $(id).textContent = v; };
|
const st = computeDisplayStats(cls, grp);
|
||||||
|
const set = (id, v) => { const el = $(id); if (el) el.textContent = v; };
|
||||||
set('#sTotal', st.total);
|
set('#sTotal', st.total);
|
||||||
set('#sSubmit', st.submitted);
|
set('#sSubmit', st.submitted);
|
||||||
set('#sAbsent', st.absent);
|
set('#sAbsent', st.absent);
|
||||||
set('#sRate', st.rate + '%');
|
|
||||||
set('#sAvg', st.avg == null ? '-' : st.avg);
|
set('#sAvg', st.avg == null ? '-' : st.avg);
|
||||||
set('#sMax', st.maxScore == null ? '-' : st.maxScore);
|
set('#sMax', st.maxScore == null ? '-' : st.maxScore);
|
||||||
set('#sMin', st.minScore == null ? '-' : st.minScore);
|
set('#sMin', st.minScore == null ? '-' : st.minScore);
|
||||||
@@ -235,15 +374,18 @@
|
|||||||
set('#sDur', st.avgDur == null ? '-' : fmtDur(st.avgDur));
|
set('#sDur', st.avgDur == null ? '-' : fmtDur(st.avgDur));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 各面板计数文字按所选班级过滤(成绩明细 / 缺考名单)
|
// 各面板计数文字按所选班级 + 组别过滤(成绩明细 / 缺考名单)
|
||||||
// 缺考数与统计卡片保持一致:过滤后名单 − 过滤后提交(而非二次过滤后端 absent 数组)
|
// 缺考数与统计卡片保持一致:过滤后名单 − 过滤后提交(而非二次过滤后端 absent 数组)
|
||||||
function renderHints() {
|
function renderHints() {
|
||||||
const cls = $('#clsSel').value;
|
const cls = $('#clsSel').value;
|
||||||
|
const grp = $('#grpSel').value;
|
||||||
const rowsForCls = cls ? state.rows.filter(r => (r.cls || '') === cls) : state.rows;
|
const rowsForCls = cls ? state.rows.filter(r => (r.cls || '') === cls) : state.rows;
|
||||||
const rosterForCls = cls ? (state.roster || []).filter(s => (s.cls || '') === cls) : (state.roster || []);
|
const rosterForCls = cls ? (state.roster || []).filter(s => (s.cls || '') === cls) : (state.roster || []);
|
||||||
const absentForCls = Math.max(0, rosterForCls.length - rowsForCls.length);
|
const rowsForGrp = grp ? rowsForCls.filter(r => (r.grp || '') === grp) : rowsForCls;
|
||||||
$('#tableHint').textContent = '共 ' + rowsForCls.length + ' 人已提交 · 名单 ' + rosterForCls.length + ' 人';
|
const rosterForGrp = grp ? rosterForCls.filter(s => (s.grp || '') === grp) : rosterForCls;
|
||||||
$('#absentHint').textContent = absentForCls ? '共 ' + absentForCls + ' 人' : '';
|
const absentForGrp = Math.max(0, rosterForGrp.length - rowsForGrp.length);
|
||||||
|
$('#tableHint').textContent = '共 ' + rowsForGrp.length + ' 人已提交 · 名单 ' + rosterForGrp.length + ' 人';
|
||||||
|
$('#absentHint').textContent = absentForGrp ? '共 ' + absentForGrp + ' 人' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtDur(sec) {
|
function fmtDur(sec) {
|
||||||
@@ -265,9 +407,11 @@
|
|||||||
function renderTable() {
|
function renderTable() {
|
||||||
const kw = $('#kw').value.trim().toLowerCase();
|
const kw = $('#kw').value.trim().toLowerCase();
|
||||||
const clsFilter = $('#clsSel').value;
|
const clsFilter = $('#clsSel').value;
|
||||||
|
const grpFilter = $('#grpSel').value;
|
||||||
let rows = state.rows.slice();
|
let rows = state.rows.slice();
|
||||||
|
|
||||||
if (clsFilter) rows = rows.filter(r => (r.cls || '') === clsFilter);
|
if (clsFilter) rows = rows.filter(r => (r.cls || '') === clsFilter);
|
||||||
|
if (grpFilter) rows = rows.filter(r => (r.grp || '') === grpFilter);
|
||||||
if (kw) rows = rows.filter(r =>
|
if (kw) rows = rows.filter(r =>
|
||||||
String(r.sid).toLowerCase().includes(kw) || String(r.name).toLowerCase().includes(kw));
|
String(r.sid).toLowerCase().includes(kw) || String(r.name).toLowerCase().includes(kw));
|
||||||
|
|
||||||
@@ -319,11 +463,14 @@
|
|||||||
|
|
||||||
function renderAbsent() {
|
function renderAbsent() {
|
||||||
const clsFilter = $('#clsSel').value;
|
const clsFilter = $('#clsSel').value;
|
||||||
|
const grpFilter = $('#grpSel').value;
|
||||||
// 与统计卡片保持一致:缺考 = 过滤后名单中未出现在提交记录里的学生(按 sid 匹配)
|
// 与统计卡片保持一致:缺考 = 过滤后名单中未出现在提交记录里的学生(按 sid 匹配)
|
||||||
const rowsForCls = clsFilter ? state.rows.filter(r => (r.cls || '') === clsFilter) : state.rows;
|
const rowsForCls = clsFilter ? state.rows.filter(r => (r.cls || '') === clsFilter) : state.rows;
|
||||||
const submittedSids = new Set(rowsForCls.map(r => r.sid).filter(Boolean));
|
const rowsForGrp = grpFilter ? rowsForCls.filter(r => (r.grp || '') === grpFilter) : rowsForCls;
|
||||||
|
const submittedSids = new Set(rowsForGrp.map(r => r.sid).filter(Boolean));
|
||||||
const rosterForCls = clsFilter ? (state.roster || []).filter(s => (s.cls || '') === clsFilter) : (state.roster || []);
|
const rosterForCls = clsFilter ? (state.roster || []).filter(s => (s.cls || '') === clsFilter) : (state.roster || []);
|
||||||
const list = rosterForCls.filter(s => !submittedSids.has(s.sid));
|
const rosterForGrp = grpFilter ? rosterForCls.filter(s => (s.grp || '') === grpFilter) : rosterForCls;
|
||||||
|
const list = rosterForGrp.filter(s => !submittedSids.has(s.sid));
|
||||||
const tb = $('#absentBody');
|
const tb = $('#absentBody');
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
tb.innerHTML = '<tr><td colspan="4" class="empty">' + (clsFilter ? '本班全部已提交 🎉' : '全部已提交 🎉') + '</td></tr>';
|
tb.innerHTML = '<tr><td colspan="4" class="empty">' + (clsFilter ? '本班全部已提交 🎉' : '全部已提交 🎉') + '</td></tr>';
|
||||||
@@ -370,6 +517,16 @@
|
|||||||
if (btn) btn.textContent = on ? '⛶ 退出全屏' : '⛶ 全屏';
|
if (btn) btn.textContent = on ? '⛶ 退出全屏' : '⛶ 全屏';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- 布局切换 ---------------- */
|
||||||
|
function applyLayout(mode) {
|
||||||
|
const m = (mode === 'side') ? 'side' : 'default';
|
||||||
|
const lay = document.getElementById('dataLayout');
|
||||||
|
if (lay) lay.setAttribute('data-layout', m);
|
||||||
|
const sel = $('#layoutSel');
|
||||||
|
if (sel && sel.value !== m) sel.value = m;
|
||||||
|
LS.set(LS_LAYOUT, m);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- 设置 ---------------- */
|
/* ---------------- 设置 ---------------- */
|
||||||
async function openSettings() {
|
async function openSettings() {
|
||||||
try {
|
try {
|
||||||
@@ -493,8 +650,6 @@
|
|||||||
/* ---------------- 事件绑定 ---------------- */
|
/* ---------------- 事件绑定 ---------------- */
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
$('#projectSel').addEventListener('change', () => { state.prevMap = {}; refresh(); });
|
|
||||||
$('#nameFieldSel').addEventListener('change', () => { state.prevMap = {}; refresh(); });
|
|
||||||
$('#btnRefresh').addEventListener('click', () => refresh(false));
|
$('#btnRefresh').addEventListener('click', () => refresh(false));
|
||||||
$('#btnSettings').addEventListener('click', openSettings);
|
$('#btnSettings').addEventListener('click', openSettings);
|
||||||
$('#btnRoster').addEventListener('click', openRoster);
|
$('#btnRoster').addEventListener('click', openRoster);
|
||||||
@@ -505,11 +660,32 @@
|
|||||||
}
|
}
|
||||||
$('#kw').addEventListener('input', renderTable);
|
$('#kw').addEventListener('input', renderTable);
|
||||||
$('#clsSel').addEventListener('change', () => {
|
$('#clsSel').addEventListener('change', () => {
|
||||||
|
populateGroups();
|
||||||
|
rememberFilters();
|
||||||
renderStats();
|
renderStats();
|
||||||
renderTable();
|
renderTable();
|
||||||
renderAbsent();
|
renderAbsent();
|
||||||
renderHints();
|
renderHints();
|
||||||
});
|
});
|
||||||
|
$('#grpSel').addEventListener('change', () => {
|
||||||
|
rememberFilters();
|
||||||
|
renderStats();
|
||||||
|
renderTable();
|
||||||
|
renderAbsent();
|
||||||
|
renderHints();
|
||||||
|
});
|
||||||
|
$('#layoutSel').addEventListener('change', e => applyLayout(e.target.value));
|
||||||
|
$('#nameFieldSel').addEventListener('change', () => {
|
||||||
|
state.prevMap = {}; state.prevSids = new Set(); state.hasPrev = false;
|
||||||
|
LS.set(LS_LASTQID, $('#nameFieldSel').value || '');
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
$('#projectSel').addEventListener('change', () => {
|
||||||
|
// 切换项目后清掉前一项目的 prevMap 与高亮缓存
|
||||||
|
state.prevMap = {}; state.prevSids = new Set(); state.hasPrev = false;
|
||||||
|
// 用缓存数据先快速渲染,再后台拉新数据
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
$('#intervalSel').addEventListener('change', schedule);
|
$('#intervalSel').addEventListener('change', schedule);
|
||||||
$('#autoRefresh').addEventListener('change', e => {
|
$('#autoRefresh').addEventListener('change', e => {
|
||||||
if (e.target.checked) { schedule(); refresh(true); }
|
if (e.target.checked) { schedule(); refresh(true); }
|
||||||
@@ -691,7 +867,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* 启动 */
|
/* 启动 */
|
||||||
// 支持 ?projectId=xxx&nameQid=yyy 直接打开某份问卷
|
// 1) 恢复布局偏好(仅样式,不涉及数据)
|
||||||
|
applyLayout(LS.get(LS_LAYOUT, 'default'));
|
||||||
|
|
||||||
|
// 2) 支持 ?projectId=xxx&nameQid=yyy 直接打开某份问卷
|
||||||
const qs = new URLSearchParams(location.search);
|
const qs = new URLSearchParams(location.search);
|
||||||
if (qs.get('interval')) $('#intervalSel').value = qs.get('interval');
|
if (qs.get('interval')) $('#intervalSel').value = qs.get('interval');
|
||||||
state.deeplinkProject = qs.get('projectId') || '';
|
state.deeplinkProject = qs.get('projectId') || '';
|
||||||
|
|||||||
+51
-6
@@ -212,6 +212,8 @@ select:focus, input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px
|
|||||||
border: 1px solid var(--border); border-radius: var(--radius);
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
box-shadow: var(--shadow); overflow: hidden;
|
box-shadow: var(--shadow); overflow: hidden;
|
||||||
}
|
}
|
||||||
|
/* 在 .layout 容器内由 gap 控制间距,去掉 panel 自身 margin-top */
|
||||||
|
.layout > .panel { margin-top: 0; }
|
||||||
.panel-head {
|
.panel-head {
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
padding: 13px 18px; border-bottom: 1px solid var(--border-2);
|
padding: 13px 18px; border-bottom: 1px solid var(--border-2);
|
||||||
@@ -234,6 +236,38 @@ select:focus, input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px
|
|||||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||||
@media (max-width: 900px) { .grid-2 { grid-template-columns: 1fr; } }
|
@media (max-width: 900px) { .grid-2 { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
|
/* ---------- 三 panel 布局容器(默认 / 成绩明细与缺考名单同行) ---------- */
|
||||||
|
.layout {
|
||||||
|
display: grid; gap: 18px;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
.layout > #tablePanel { grid-column: 1; grid-row: 1; }
|
||||||
|
.layout > #absentPanel { grid-column: 1; grid-row: 2; }
|
||||||
|
.layout > #unmatchedPanel { grid-column: 1; grid-row: 3; }
|
||||||
|
|
||||||
|
/* side:成绩明细 与 缺考名单 同行,未匹配答卷放下面整宽 */
|
||||||
|
.layout[data-layout="side"] {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
.layout[data-layout="side"] > #tablePanel { grid-column: 1; grid-row: 1; }
|
||||||
|
.layout[data-layout="side"] > #absentPanel { grid-column: 2; grid-row: 1; }
|
||||||
|
.layout[data-layout="side"] > #unmatchedPanel { grid-column: 1 / -1; grid-row: 2; }
|
||||||
|
|
||||||
|
/* 窄屏一律回落单列(grid-row 也要还原) */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.layout, .layout[data-layout="side"] { grid-template-columns: 1fr; }
|
||||||
|
.layout > #tablePanel, .layout > #absentPanel, .layout > #unmatchedPanel,
|
||||||
|
.layout[data-layout="side"] > #tablePanel,
|
||||||
|
.layout[data-layout="side"] > #absentPanel,
|
||||||
|
.layout[data-layout="side"] > #unmatchedPanel {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
.layout > #tablePanel, .layout[data-layout="side"] > #tablePanel { grid-row: 1; }
|
||||||
|
.layout > #absentPanel, .layout[data-layout="side"] > #absentPanel { grid-row: 2; }
|
||||||
|
.layout > #unmatchedPanel, .layout[data-layout="side"] > #unmatchedPanel { grid-row: 3; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- 表格 ---------- */
|
/* ---------- 表格 ---------- */
|
||||||
.table-scroll { overflow: auto; max-height: 62vh; }
|
.table-scroll { overflow: auto; max-height: 62vh; }
|
||||||
.table-scroll.short { max-height: 320px; }
|
.table-scroll.short { max-height: 320px; }
|
||||||
@@ -526,10 +560,21 @@ body.fs-on .topbar { padding-top: 8px; padding-bottom: 8px; }
|
|||||||
.gpanel, .class-card { overflow: hidden; }
|
.gpanel, .class-card { overflow: hidden; }
|
||||||
.member-list td.name { max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.member-list td.name { max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
|
||||||
/* 看板全屏适配 */
|
/* 看板全屏:整页不溢出屏幕(外层锁 100vh + overflow:hidden,内部区域自行滚动) */
|
||||||
body.fs-on .wrap { max-width: none; padding-top: 14px; }
|
body.dash.fs-on .wrap {
|
||||||
body.fs-on .member-list { max-height: 46vh; }
|
height: 100vh; display: flex; flex-direction: column; overflow: hidden;
|
||||||
body.fs-on .class-grid { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); }
|
max-width: none; padding: 12px 20px 14px;
|
||||||
body.fs-on .group-compare { grid-template-columns: 1fr 1fr; }
|
}
|
||||||
body.fs-on .dash-hero { border-radius: var(--radius); }
|
body.dash.fs-on .dash-hero { padding: 12px 18px; flex: 0 0 auto; border-radius: var(--radius); }
|
||||||
|
body.dash.fs-on .sec-title { margin: 12px 0 4px; font-size: 14px; }
|
||||||
|
body.dash.fs-on .cards { gap: 10px; margin-top: 10px; }
|
||||||
|
body.dash.fs-on .card { padding: 10px 12px; }
|
||||||
|
body.dash.fs-on .card b { font-size: 20px; }
|
||||||
|
body.dash.fs-on #overview, body.dash.fs-on #detail {
|
||||||
|
flex: 1 1 auto; min-height: 0; overflow: auto;
|
||||||
|
}
|
||||||
|
body.dash.fs-on .class-grid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
|
||||||
|
body.dash.fs-on .group-compare { gap: 12px; }
|
||||||
|
body.dash.fs-on .gpanel { padding: 12px 14px; }
|
||||||
|
body.dash.fs-on .member-list { max-height: 30vh; }
|
||||||
|
|
||||||
|
|||||||
+81
-9
@@ -10,7 +10,7 @@ require __DIR__ . '/bootstrap.php';
|
|||||||
<title>数据看板 · <?= htmlspecialchars(NY_APP_NAME) ?></title>
|
<title>数据看板 · <?= htmlspecialchars(NY_APP_NAME) ?></title>
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
<link rel="stylesheet" href="assets/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="dash">
|
||||||
|
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
|
|
||||||
@@ -87,6 +87,15 @@ function rateColor(r){
|
|||||||
return 'var(--danger)';
|
return 'var(--danger)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 本地存储(缓存问卷列表/看板数据,重开即恢复) */
|
||||||
|
const LS = {
|
||||||
|
get(k, def){ try { const v = localStorage.getItem(k); return v == null ? def : JSON.parse(v); } catch(e){ return def; } },
|
||||||
|
set(k, v){ try { localStorage.setItem(k, JSON.stringify(v)); } catch(e){} },
|
||||||
|
del(k){ try { localStorage.removeItem(k); } catch(e){} }
|
||||||
|
};
|
||||||
|
const LS_PROJECTS = 'ny_projects';
|
||||||
|
const LS_DASH = pid => 'ny_dashboard_' + pid;
|
||||||
|
|
||||||
let DASH = null; // 最近一次看板数据
|
let DASH = null; // 最近一次看板数据
|
||||||
let CUR = null; // 当前查看的班级名
|
let CUR = null; // 当前查看的班级名
|
||||||
|
|
||||||
@@ -102,17 +111,61 @@ async function api(action, opts){
|
|||||||
throw new Error((j && (j.msg || j.error)) || '请求失败');
|
throw new Error((j && (j.msg || j.error)) || '请求失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadProjects(){
|
/* ---------- 问卷列表:双策略识别文件夹 + 缓存优先 ---------- */
|
||||||
try {
|
function renderProjects(list){
|
||||||
const list = await api('projects');
|
|
||||||
const sel = $('#projectSel');
|
const sel = $('#projectSel');
|
||||||
const keep = sel.value;
|
const keep = sel.value;
|
||||||
sel.innerHTML = '<option value="">请选择问卷 / 考试</option>' +
|
sel.innerHTML = '<option value="">请选择问卷 / 考试</option>';
|
||||||
list.map(p => '<option value="' + esc(p.id) + '">' + esc(p.name) + '(' + esc(p.mode || '') + ')</option>').join('');
|
// 双策略识别文件夹:mode 含 folder,或被其他项目作为 parent_id 引用
|
||||||
if (keep) sel.value = keep;
|
const isFolder = p => (p.mode || '').toLowerCase().includes('folder');
|
||||||
|
const parentIds = new Set();
|
||||||
|
list.forEach(p => { const pid = (p.parent_id || '0'); if (pid && pid !== '0' && pid !== '') parentIds.add(pid); });
|
||||||
|
const folderSet = new Set();
|
||||||
|
list.forEach(p => { if (isFolder(p) || parentIds.has(p.id)) folderSet.add(p.id); });
|
||||||
|
const childOf = fid => list.filter(p => !folderSet.has(p.id) && (p.parent_id || '0') === fid);
|
||||||
|
|
||||||
|
// 根项
|
||||||
|
list.filter(p => !folderSet.has(p.id)).forEach(p => {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = p.id; o.textContent = (p.mode ? '[' + p.mode + '] ' : '') + p.name + '(' + (p.total || 0) + '份)';
|
||||||
|
sel.appendChild(o);
|
||||||
|
});
|
||||||
|
// 文件夹
|
||||||
|
list.filter(p => folderSet.has(p.id)).forEach(f => {
|
||||||
|
const kids = childOf(f.id);
|
||||||
|
if (!kids.length) return;
|
||||||
|
const og = document.createElement('optgroup');
|
||||||
|
og.label = '📁 ' + f.name;
|
||||||
|
kids.forEach(p => {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = p.id; o.textContent = (p.mode ? '[' + p.mode + '] ' : '') + p.name + '(' + (p.total || 0) + '份)';
|
||||||
|
og.appendChild(o);
|
||||||
|
});
|
||||||
|
sel.appendChild(og);
|
||||||
|
});
|
||||||
|
if (keep && list.some(p => p.id === keep)) sel.value = keep;
|
||||||
|
else {
|
||||||
|
const last = LS.get('ny_dash_last_pid', '');
|
||||||
|
if (last && list.some(p => p.id === last)) sel.value = last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProjects(){
|
||||||
|
// 1) 先读本地缓存
|
||||||
|
const cached = LS.get(LS_PROJECTS, []);
|
||||||
|
if (Array.isArray(cached) && cached.length) {
|
||||||
|
renderProjects(cached);
|
||||||
|
}
|
||||||
|
// 2) 后台异步刷新
|
||||||
|
try {
|
||||||
|
const list = await api('projects');
|
||||||
|
LS.set(LS_PROJECTS, list || []);
|
||||||
|
renderProjects(list || []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!cached || !cached.length) {
|
||||||
$('#classGrid').innerHTML = '<div class="empty">加载问卷列表失败:' + esc(e.message) + '</div>';
|
$('#classGrid').innerHTML = '<div class="empty">加载问卷列表失败:' + esc(e.message) + '</div>';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSummary(d){
|
function renderSummary(d){
|
||||||
@@ -241,21 +294,40 @@ async function loadDashboard(){
|
|||||||
$('#classGrid').innerHTML = '<div class="empty">请选择问卷 / 考试以查看各班数据</div>';
|
$('#classGrid').innerHTML = '<div class="empty">请选择问卷 / 考试以查看各班数据</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1) 缓存优先:先渲染上次保存的看板,避免空白
|
||||||
|
const cached = LS.get(LS_DASH(pid), null);
|
||||||
|
if (cached && cached.classes) {
|
||||||
|
DASH = cached;
|
||||||
|
renderSummary(DASH);
|
||||||
|
renderClasses(DASH);
|
||||||
|
$('#lastUpdate').textContent = (cached.updated_at || '—') + '(缓存)';
|
||||||
|
if (CUR) {
|
||||||
|
const still = (DASH.classes || []).some(x => x.class === CUR);
|
||||||
|
if (still) showClassDetail(CUR); else backToOverview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 后台拉真实数据
|
||||||
try {
|
try {
|
||||||
DASH = await api('dashboard', { params: { projectId: pid } });
|
const fresh = await api('dashboard', { params: { projectId: pid } });
|
||||||
|
DASH = fresh;
|
||||||
|
LS.set(LS_DASH(pid), fresh);
|
||||||
|
LS.set('ny_dash_last_pid', pid);
|
||||||
renderSummary(DASH);
|
renderSummary(DASH);
|
||||||
renderClasses(DASH);
|
renderClasses(DASH);
|
||||||
$('#lastUpdate').textContent = DASH.updated_at || '-';
|
$('#lastUpdate').textContent = DASH.updated_at || '-';
|
||||||
$('#liveDot').classList.add('on');
|
$('#liveDot').classList.add('on');
|
||||||
setTimeout(() => $('#liveDot').classList.remove('on'), 900);
|
setTimeout(() => $('#liveDot').classList.remove('on'), 900);
|
||||||
// 若正在看某班详情,刷新后同步更新
|
|
||||||
if (CUR) {
|
if (CUR) {
|
||||||
const still = (DASH.classes || []).some(x => x.class === CUR);
|
const still = (DASH.classes || []).some(x => x.class === CUR);
|
||||||
if (still) showClassDetail(CUR); else backToOverview();
|
if (still) showClassDetail(CUR); else backToOverview();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!cached) {
|
||||||
$('#classGrid').innerHTML = '<div class="empty">数据加载失败:' + esc(e.message) + '</div>';
|
$('#classGrid').innerHTML = '<div class="empty">数据加载失败:' + esc(e.message) + '</div>';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 全屏 ---------- */
|
/* ---------- 全屏 ---------- */
|
||||||
|
|||||||
@@ -71,6 +71,17 @@ $rosterCount = Roster::count();
|
|||||||
<label>班级</label>
|
<label>班级</label>
|
||||||
<select id="clsSel"><option value="">全部班级</option></select>
|
<select id="clsSel"><option value="">全部班级</option></select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>组别</label>
|
||||||
|
<select id="grpSel"><option value="">全部组别</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>布局</label>
|
||||||
|
<select id="layoutSel">
|
||||||
|
<option value="default">默认(缺考+未匹配同行)</option>
|
||||||
|
<option value="side">成绩明细与缺考名单同行</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>自动刷新</label>
|
<label>自动刷新</label>
|
||||||
<div class="inline">
|
<div class="inline">
|
||||||
@@ -104,7 +115,6 @@ $rosterCount = Roster::count();
|
|||||||
<div class="card accent-blue"><span>名单总人数</span><b id="sTotal">-</b></div>
|
<div class="card accent-blue"><span>名单总人数</span><b id="sTotal">-</b></div>
|
||||||
<div class="card ok accent-green"><span>已提交</span><b id="sSubmit">-</b></div>
|
<div class="card ok accent-green"><span>已提交</span><b id="sSubmit">-</b></div>
|
||||||
<div class="card warn accent-orange"><span>缺考</span><b id="sAbsent">-</b></div>
|
<div class="card warn accent-orange"><span>缺考</span><b id="sAbsent">-</b></div>
|
||||||
<div class="card accent-purple"><span>提交率</span><b id="sRate">-</b></div>
|
|
||||||
<div class="card accent-teal"><span>平均分</span><b id="sAvg">-</b></div>
|
<div class="card accent-teal"><span>平均分</span><b id="sAvg">-</b></div>
|
||||||
<div class="card accent-green"><span>最高分</span><b id="sMax">-</b></div>
|
<div class="card accent-green"><span>最高分</span><b id="sMax">-</b></div>
|
||||||
<div class="card accent-red"><span>最低分</span><b id="sMin">-</b></div>
|
<div class="card accent-red"><span>最低分</span><b id="sMin">-</b></div>
|
||||||
@@ -112,7 +122,8 @@ $rosterCount = Roster::count();
|
|||||||
<div class="card accent-gray"><span>平均用时</span><b id="sDur">-</b></div>
|
<div class="card accent-gray"><span>平均用时</span><b id="sDur">-</b></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<div class="layout" id="dataLayout" data-layout="default">
|
||||||
|
<section class="panel" id="tablePanel">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<h2>成绩明细 <small id="tableHint"></small></h2>
|
<h2>成绩明细 <small id="tableHint"></small></h2>
|
||||||
<div class="updated">最后更新:<span id="lastUpdate">—</span><i id="liveDot" class="dot"></i></div>
|
<div class="updated">最后更新:<span id="lastUpdate">—</span><i id="liveDot" class="dot"></i></div>
|
||||||
@@ -135,8 +146,7 @@ $rosterCount = Roster::count();
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="grid-2">
|
<section class="panel" id="absentPanel">
|
||||||
<section class="panel">
|
|
||||||
<div class="panel-head"><h2>缺考名单 <small id="absentHint"></small></h2></div>
|
<div class="panel-head"><h2>缺考名单 <small id="absentHint"></small></h2></div>
|
||||||
<div class="table-scroll short">
|
<div class="table-scroll short">
|
||||||
<table class="grid compact">
|
<table class="grid compact">
|
||||||
@@ -145,7 +155,8 @@ $rosterCount = Roster::count();
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="panel">
|
|
||||||
|
<section class="panel" id="unmatchedPanel">
|
||||||
<div class="panel-head"><h2>未匹配答卷 <small>姓名不在名单中</small></h2></div>
|
<div class="panel-head"><h2>未匹配答卷 <small>姓名不在名单中</small></h2></div>
|
||||||
<div class="table-scroll short">
|
<div class="table-scroll short">
|
||||||
<table class="grid compact">
|
<table class="grid compact">
|
||||||
|
|||||||
Reference in New Issue
Block a user