fix: 文件夹试卷可见(用optgroup) + 班级后组别筛选 + 看板全屏锁屏不溢出
This commit is contained in:
@@ -94,6 +94,7 @@ try {
|
|||||||
'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);
|
||||||
|
|||||||
+74
-14
@@ -87,16 +87,37 @@
|
|||||||
const ph = document.createElement('option');
|
const ph = document.createElement('option');
|
||||||
ph.value = ''; ph.textContent = '请选择问卷 / 考试';
|
ph.value = ''; ph.textContent = '请选择问卷 / 考试';
|
||||||
sel.appendChild(ph);
|
sel.appendChild(ph);
|
||||||
state.projects.forEach(p => {
|
|
||||||
|
// 文件夹(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');
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根项(不含文件夹、且 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;
|
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 + ' 个问卷');
|
const surveyCount = state.projects.filter(p => !(p.mode || '').toLowerCase().includes('folder')).length;
|
||||||
|
setConn('conn-on', '已连接 · ' + surveyCount + ' 个问卷');
|
||||||
showAlert('');
|
showAlert('');
|
||||||
if (sel.value) refresh();
|
if (sel.value) refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -132,6 +153,7 @@
|
|||||||
renderTable();
|
renderTable();
|
||||||
state.prevSids = cur;
|
state.prevSids = cur;
|
||||||
populateClasses();
|
populateClasses();
|
||||||
|
populateGroups();
|
||||||
renderAbsent();
|
renderAbsent();
|
||||||
renderUnmatched();
|
renderUnmatched();
|
||||||
renderHints();
|
renderHints();
|
||||||
@@ -184,11 +206,33 @@
|
|||||||
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('');
|
||||||
|
if (vals.indexOf(cur) >= 0) grpSel.value = cur;
|
||||||
|
else grpSel.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按当前所选班级 + 组别(或无)客户端重算统计卡片
|
||||||
|
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,7 +266,8 @@
|
|||||||
|
|
||||||
function renderStats() {
|
function renderStats() {
|
||||||
const cls = $('#clsSel').value;
|
const cls = $('#clsSel').value;
|
||||||
const st = computeDisplayStats(cls);
|
const grp = $('#grpSel').value;
|
||||||
|
const st = computeDisplayStats(cls, grp);
|
||||||
const set = (id, v) => { $(id).textContent = v; };
|
const set = (id, v) => { $(id).textContent = v; };
|
||||||
set('#sTotal', st.total);
|
set('#sTotal', st.total);
|
||||||
set('#sSubmit', st.submitted);
|
set('#sSubmit', st.submitted);
|
||||||
@@ -235,15 +280,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 +313,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 +369,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>';
|
||||||
@@ -505,6 +558,13 @@
|
|||||||
}
|
}
|
||||||
$('#kw').addEventListener('input', renderTable);
|
$('#kw').addEventListener('input', renderTable);
|
||||||
$('#clsSel').addEventListener('change', () => {
|
$('#clsSel').addEventListener('change', () => {
|
||||||
|
populateGroups();
|
||||||
|
renderStats();
|
||||||
|
renderTable();
|
||||||
|
renderAbsent();
|
||||||
|
renderHints();
|
||||||
|
});
|
||||||
|
$('#grpSel').addEventListener('change', () => {
|
||||||
renderStats();
|
renderStats();
|
||||||
renderTable();
|
renderTable();
|
||||||
renderAbsent();
|
renderAbsent();
|
||||||
|
|||||||
+17
-6
@@ -526,10 +526,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; }
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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">
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ $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">
|
<div class="field">
|
||||||
<label>自动刷新</label>
|
<label>自动刷新</label>
|
||||||
<div class="inline">
|
<div class="inline">
|
||||||
|
|||||||
Reference in New Issue
Block a user