diff --git a/assets/app.js b/assets/app.js
index db0078d..54dcf25 100644
--- a/assets/app.js
+++ b/assets/app.js
@@ -24,6 +24,26 @@
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) {
opts = opts || {};
@@ -73,97 +93,155 @@
}
/* ---------------- 问卷列表 ---------------- */
+ // 把数组渲染到 #projectSel(抽出便于缓存复用)
+ function renderProjects() {
+ const sel = $('#projectSel');
+ const keep = sel.value;
+ sel.innerHTML = '';
+ if (!state.projects.length) {
+ sel.innerHTML = '';
+ } else {
+ const ph = document.createElement('option');
+ ph.value = ''; ph.textContent = '请选择问卷 / 考试';
+ 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 => {
+ 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');
+ o.value = p.id;
+ o.textContent = (p.mode ? '[' + p.mode + '] ' : '') + p.name + '(' + (p.total || 0) + '份)';
+ 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;
+ else if (state.deeplinkProject && state.projects.some(p => p.id === state.deeplinkProject)) sel.value = state.deeplinkProject;
+ 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('');
+ 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 || [];
- const sel = $('#projectSel');
- const keep = sel.value;
- sel.innerHTML = '';
- if (!state.projects.length) {
- sel.innerHTML = '';
- } else {
- const ph = document.createElement('option');
- ph.value = ''; ph.textContent = '请选择问卷 / 考试';
- sel.appendChild(ph);
-
- // 文件夹(mode 含 folder)以 optgroup 分组形式呈现,其子项以 parentId 关联
- const isFolder = p => (p.mode || '').toLowerCase().includes('folder');
- const folders = state.projects.filter(isFolder);
- const folderIds = new Set(folders.map(f => f.id));
- const childOfFolder = fid => state.projects.filter(p => !isFolder(p) && (p.parent_id || '0') === fid);
-
- const mkOpt = p => {
- const o = document.createElement('option');
- o.value = p.id;
- o.textContent = (p.mode ? '[' + p.mode + '] ' : '') + p.name + '(' + (p.total || 0) + '份)';
- return o;
- };
-
- // 根项(不含文件夹、且 parent_id 不指向任何文件夹)
- state.projects.filter(p => !isFolder(p) && !(p.parent_id && p.parent_id !== '0' && folderIds.has(p.parent_id)))
- .forEach(p => sel.appendChild(mkOpt(p)));
- // 文件夹及子项
- folders.forEach(f => {
- const kids = childOfFolder(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;
- else if (state.deeplinkProject && state.projects.some(p => p.id === state.deeplinkProject)) sel.value = state.deeplinkProject;
- const surveyCount = state.projects.filter(p => !(p.mode || '').toLowerCase().includes('folder')).length;
- setConn('conn-on', '已连接 · ' + surveyCount + ' 个问卷');
- showAlert('');
- if (sel.value) refresh();
+ state._fromCache = false;
+ LS.set(LS_PROJECTS, state.projects);
+ renderProjects();
} catch (e) {
- setConn('conn-err', '连接失败');
- showAlert('无法连接卷王系统:' + e.message + '\n请点击右上角【设置】检查系统地址、接口前缀与账号密码。');
+ if (!cached || !cached.length) {
+ setConn('conn-err', '连接失败');
+ showAlert('无法连接卷王系统:' + e.message + '\n请点击右上角【设置】检查系统地址、接口前缀与账号密码。');
+ } else {
+ showAlert('⚠️ 后台刷新失败,已显示本地缓存:' + e.message);
+ }
}
}
/* ---------------- 数据刷新 ---------------- */
+ function applyData(d, opts) {
+ opts = opts || {};
+ state.questions = d.questions || [];
+ state.rows = d.rows || [];
+ state.absent = d.absent || [];
+ state.unmatched = d.unmatched || [];
+ state.roster = d.roster || [];
+ state.fullScore = (d.config && d.config.full_score) ? (parseInt(d.config.full_score, 10) || 100) : 100;
+
+ 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)));
+ else state.newSids = new Set();
+ state.hasPrev = true;
+
+ renderNameFields(d);
+ renderStats();
+ renderTable();
+ state.prevSids = cur;
+ populateClasses();
+ populateGroups();
+ renderAbsent();
+ renderUnmatched();
+ renderHints();
+
+ $('#lastUpdate').textContent = d.updated_at || '—';
+ setConn('conn-on', (opts.fromCache ? '缓存 · ' : '') + '实时 · ' + (d.project && d.project.name || ''));
+ if (!opts.fromCache) showAlert('');
+ }
+
async function refresh(silent) {
const pid = $('#projectSel').value;
if (!pid) return;
- if (state.loading) 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 } });
- state.questions = d.questions || [];
- state.rows = d.rows || [];
- state.absent = d.absent || [];
- state.unmatched = d.unmatched || [];
- state.roster = d.roster || [];
- 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));
- if (state.hasPrev) state.newSids = new Set(Array.from(cur).filter(s => !state.prevSids.has(s)));
- else state.newSids = new Set();
- state.hasPrev = true;
-
- renderNameFields(d);
- renderStats();
- renderTable();
- state.prevSids = cur;
- populateClasses();
- populateGroups();
- renderAbsent();
- renderUnmatched();
- renderHints();
-
- $('#lastUpdate').textContent = d.updated_at;
- setConn('conn-on', '实时 · ' + d.project.name);
- showAlert('');
+ 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) {
- if (!silent) setConn('conn-err', '数据获取失败');
- showAlert(e.message);
+ if (!cached) {
+ if (!silent) setConn('conn-err', '数据获取失败');
+ showAlert(e.message);
+ } else {
+ showAlert('⚠️ 后台刷新失败,已显示本地缓存:' + e.message);
+ }
} finally {
state.loading = false;
$('#liveDot').classList.add('on');
@@ -189,8 +267,10 @@
o.textContent = (q.title || q.id) + '(' + (q.type || '') + ')';
sel.appendChild(o);
});
+ const lastQid = LS.get(LS_LASTQID, '');
if (keep) sel.value = keep;
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;
}
@@ -202,7 +282,11 @@
const vals = Array.from(set).sort((a, b) => a.localeCompare(b, 'zh', { numeric: true }));
sel.innerHTML = '' +
vals.map(v => '').join('');
+ // 优先保留用户当前已选项;否则按本地缓存恢复(跟随 projectId)
+ const pid = $('#projectSel').value;
+ const cache = pid ? (LS.get(LS_FILTERS, {})[pid] || {}) : {};
if (vals.indexOf(cur) >= 0) sel.value = cur;
+ else if (cache.cls && vals.indexOf(cache.cls) >= 0) sel.value = cache.cls;
else sel.value = '';
}
@@ -220,10 +304,21 @@
const vals = Array.from(set).sort();
grpSel.innerHTML = '' +
vals.map(v => '').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 || [];
@@ -268,11 +363,10 @@
const cls = $('#clsSel').value;
const grp = $('#grpSel').value;
const st = computeDisplayStats(cls, grp);
- const set = (id, v) => { $(id).textContent = v; };
+ const set = (id, v) => { const el = $(id); if (el) el.textContent = v; };
set('#sTotal', st.total);
set('#sSubmit', st.submitted);
set('#sAbsent', st.absent);
- set('#sRate', st.rate + '%');
set('#sAvg', st.avg == null ? '-' : st.avg);
set('#sMax', st.maxScore == null ? '-' : st.maxScore);
set('#sMin', st.minScore == null ? '-' : st.minScore);
@@ -423,6 +517,16 @@
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() {
try {
@@ -546,8 +650,6 @@
/* ---------------- 事件绑定 ---------------- */
document.addEventListener('DOMContentLoaded', function () {
- $('#projectSel').addEventListener('change', () => { state.prevMap = {}; refresh(); });
- $('#nameFieldSel').addEventListener('change', () => { state.prevMap = {}; refresh(); });
$('#btnRefresh').addEventListener('click', () => refresh(false));
$('#btnSettings').addEventListener('click', openSettings);
$('#btnRoster').addEventListener('click', openRoster);
@@ -559,17 +661,31 @@
$('#kw').addEventListener('input', renderTable);
$('#clsSel').addEventListener('change', () => {
populateGroups();
+ rememberFilters();
renderStats();
renderTable();
renderAbsent();
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);
$('#autoRefresh').addEventListener('change', e => {
if (e.target.checked) { schedule(); refresh(true); }
@@ -751,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);
if (qs.get('interval')) $('#intervalSel').value = qs.get('interval');
state.deeplinkProject = qs.get('projectId') || '';
diff --git a/assets/style.css b/assets/style.css
index 9e7b96d..b0ae4ee 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -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);
box-shadow: var(--shadow); overflow: hidden;
}
+/* 在 .layout 容器内由 gap 控制间距,去掉 panel 自身 margin-top */
+.layout > .panel { margin-top: 0; }
.panel-head {
display: flex; align-items: center; justify-content: space-between;
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; }
@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.short { max-height: 320px; }
diff --git a/dashboard.php b/dashboard.php
index 0cfb7a9..72760de 100644
--- a/dashboard.php
+++ b/dashboard.php
@@ -87,6 +87,15 @@ function rateColor(r){
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 CUR = null; // 当前查看的班级名
@@ -102,16 +111,60 @@ async function api(action, opts){
throw new Error((j && (j.msg || j.error)) || '请求失败');
}
+/* ---------- 问卷列表:双策略识别文件夹 + 缓存优先 ---------- */
+function renderProjects(list){
+ const sel = $('#projectSel');
+ const keep = sel.value;
+ sel.innerHTML = '';
+ // 双策略识别文件夹:mode 含 folder,或被其他项目作为 parent_id 引用
+ 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');
- const sel = $('#projectSel');
- const keep = sel.value;
- sel.innerHTML = '' +
- list.map(p => '').join('');
- if (keep) sel.value = keep;
+ LS.set(LS_PROJECTS, list || []);
+ renderProjects(list || []);
} catch (e) {
- $('#classGrid').innerHTML = '
加载问卷列表失败:' + esc(e.message) + '
';
+ if (!cached || !cached.length) {
+ $('#classGrid').innerHTML = '加载问卷列表失败:' + esc(e.message) + '
';
+ }
}
}
@@ -241,20 +294,39 @@ async function loadDashboard(){
$('#classGrid').innerHTML = '请选择问卷 / 考试以查看各班数据
';
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 {
- 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);
renderClasses(DASH);
$('#lastUpdate').textContent = DASH.updated_at || '-';
$('#liveDot').classList.add('on');
setTimeout(() => $('#liveDot').classList.remove('on'), 900);
- // 若正在看某班详情,刷新后同步更新
if (CUR) {
const still = (DASH.classes || []).some(x => x.class === CUR);
if (still) showClassDetail(CUR); else backToOverview();
}
} catch (e) {
- $('#classGrid').innerHTML = '数据加载失败:' + esc(e.message) + '
';
+ if (!cached) {
+ $('#classGrid').innerHTML = '数据加载失败:' + esc(e.message) + '
';
+ }
}
}
diff --git a/index.php b/index.php
index 1ee3fbc..b364158 100644
--- a/index.php
+++ b/index.php
@@ -75,6 +75,13 @@ $rosterCount = Roster::count();
+
+
+
+
@@ -108,7 +115,6 @@ $rosterCount = Roster::count();
名单总人数-
已提交-
缺考-
-
提交率-
平均分-
最高分-
最低分-
@@ -116,31 +122,31 @@ $rosterCount = Roster::count();
平均用时-
-
+