feat: 成绩明细打印 + SQLite + 每卷独立配置(满分/合格率/免考) + 导航精简
- 成绩明细打印:默认成绩降序,可选按班级/组别分组对比(段首小计) - 修复看板退出全屏无反应(exitFullscreen 挂在 document 而非 documentElement) - 导航栏精简:三页统一为单一主标题,移除 Logo 与副标题 - 优化三端布局:重排断点、竖排隐藏 spacer、触摸设备热区 - 全面启用 SQLite(data/ny.db):配置/名单/每卷配置入库,无 pdo_sqlite 自动降级文件 - 新增独立合格率配置(全局 + 每卷),合格率不再写死 60% - 新增免考配置,仅对当前试卷生效,试卷间隔离 - 试卷独立满分从设置移出,改到主页面「试卷设置」 - 测试:新增 4 条每卷配置隔离断言,全套 113 项通过
This commit is contained in:
+185
-48
@@ -261,6 +261,7 @@
|
||||
state.roster = d.roster || [];
|
||||
state.anomalies = d.anomalies || { fast_high: [], frequent: [], identical: [] };
|
||||
state.fullScore = (d.config && d.config.full_score) ? (parseInt(d.config.full_score, 10) || 100) : 100;
|
||||
state.passRate = (d.config && d.config.pass_rate != null) ? (parseFloat(d.config.pass_rate) || 60) : 60;
|
||||
|
||||
// 变更检测:学号/成绩/提交时间/考试次数/最近提交时间 任一变化即视为"有新数据"
|
||||
// (纳入 attempts 与 last_submit_ts 才能发现"重考但分数更低"这种提交)
|
||||
@@ -456,7 +457,8 @@
|
||||
const avg = scores.length ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length * 100) / 100 : null;
|
||||
|
||||
const full = state.fullScore || 100;
|
||||
const passCount = rows.filter(r => r.score !== null && r.score !== undefined && r.score >= 0.6 * full).length;
|
||||
const pr = (state.passRate == null ? 60 : state.passRate); // 合格率阈值可配
|
||||
const passCount = rows.filter(r => r.score !== null && r.score !== undefined && r.score >= (pr / 100) * full).length;
|
||||
const passRate = submitted > 0 ? Math.round(passCount / submitted * 1000) / 10 : 0;
|
||||
|
||||
let maxScore = null, minScore = null;
|
||||
@@ -490,6 +492,9 @@
|
||||
set('#sMax', st.maxScore == null ? '-' : st.maxScore);
|
||||
set('#sMin', st.minScore == null ? '-' : st.minScore);
|
||||
set('#sPass', st.passRate + '%');
|
||||
// 合格率阈值可配(全局或本卷独立),卡片标题实时反映当前生效值
|
||||
const lbl = $('#sPassLabel');
|
||||
if (lbl) lbl.textContent = '(≥' + (state.passRate == null ? 60 : state.passRate) + '%满分)';
|
||||
set('#sDur', st.avgDur == null ? '-' : fmtDur(st.avgDur));
|
||||
}
|
||||
|
||||
@@ -854,10 +859,20 @@
|
||||
const el = document.documentElement;
|
||||
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
|
||||
const req = el.requestFullscreen || el.webkitRequestFullscreen;
|
||||
if (req) req.call(el);
|
||||
if (req) {
|
||||
const p = req.call(el);
|
||||
// 部分浏览器返回 Promise,被拒绝时静默吞掉会让用户以为"点了没反应"
|
||||
if (p && typeof p.catch === 'function') {
|
||||
p.catch(() => { showAlert('当前浏览器/环境不允许进入全屏,请按 F11 手动切换', 'warn'); });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// exitFullscreen 挂在 document 上(documentElement.exitFullscreen 恒为 undefined)
|
||||
const exit = document.exitFullscreen || document.webkitExitFullscreen;
|
||||
if (exit) exit.call(document);
|
||||
if (exit) {
|
||||
const p = exit.call(document);
|
||||
if (p && typeof p.catch === 'function') p.catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
function onFsChange() {
|
||||
@@ -890,9 +905,9 @@
|
||||
// 布局
|
||||
const layout = c.layout || LS.get(LS_LAYOUT, 'default');
|
||||
(document.querySelector('input[name=cfgLayout][value="' + layout + '"]') || {}).checked = true;
|
||||
// 满分:默认 + 各试卷独立
|
||||
// 全局默认满分 / 合格率(每卷的独立配置移到主页面「试卷设置」)
|
||||
$('#cfgFull').value = c.full_score || 100;
|
||||
renderFullScores((c.full_scores && typeof c.full_scores === 'object') ? c.full_scores : {});
|
||||
if ($('#cfgPassRate')) $('#cfgPassRate').value = (c.pass_rate == null ? 60 : c.pass_rate);
|
||||
// 历史成绩展示方式 + 导出排序
|
||||
state.historyDisplay = (c.history_display === 'inline') ? 'inline' : 'click';
|
||||
state.exportSort = (c.export_sort === 'sid') ? 'sid' : ((c.export_sort === 'score_desc') ? 'score_desc' : ((c.export_sort === 'score_asc') ? 'score_asc' : 'time'));
|
||||
@@ -914,39 +929,6 @@
|
||||
$('#settingsModal').classList.add('open');
|
||||
}
|
||||
|
||||
// 渲染"各试卷独立满分"列表
|
||||
// 注意:#fullScoresHint 必须位于 #fullScoresBox 之外(见 index.php),
|
||||
// 否则 box.innerHTML 覆盖会销毁它,下面的 textContent 赋值会抛 "Cannot set properties of null"。
|
||||
// 这里仍保留 null 守卫,避免任何 DOM 结构变动导致整个设置弹窗打不开。
|
||||
function setFullScoresHint(text) {
|
||||
const hint = $('#fullScoresHint');
|
||||
if (hint) hint.textContent = text;
|
||||
}
|
||||
function renderFullScores(scores) {
|
||||
const box = $('#fullScoresBox');
|
||||
if (!box) return;
|
||||
if (!state.projects.length) {
|
||||
box.innerHTML = '<span class="hint">暂无试卷,请先连接卷王系统并选择问卷</span>';
|
||||
setFullScoresHint('给某卷填入数字即可单独覆盖默认满分;留空=用默认');
|
||||
return;
|
||||
}
|
||||
// 排除文件夹项:模式为文件夹的无可答卷
|
||||
const surveys = state.projects.filter(p => !((p.mode || '').toLowerCase().includes('folder')));
|
||||
if (!surveys.length) {
|
||||
box.innerHTML = '<span class="hint">尚未获取到可答卷</span>';
|
||||
setFullScoresHint('给某卷填入数字即可单独覆盖默认满分;留空=用默认');
|
||||
return;
|
||||
}
|
||||
box.innerHTML = surveys.map(p => {
|
||||
const val = scores[p.id] != null ? scores[p.id] : '';
|
||||
return '<div class="row">' +
|
||||
'<label title="' + esc(p.name) + '">' + esc(p.name) + '</label>' +
|
||||
'<input type="number" min="1" max="1000" data-pid="' + esc(p.id) + '" value="' + esc(val) + '" placeholder="默认">' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
setFullScoresHint('默认分值为空时使用上方"默认满分";填写即单独覆盖该卷');
|
||||
}
|
||||
|
||||
/* ---------------- 名单 ---------------- */
|
||||
let rosterDraft = [];
|
||||
const rosterSel = new Set(); // 勾选待删除的行下标(对应 rosterDraft 索引)
|
||||
@@ -1163,8 +1145,11 @@
|
||||
include_temp_save: $('#cfgTemp').checked,
|
||||
verify_ssl: $('#cfgSsl').checked,
|
||||
full_score: parseInt($('#cfgFull').value, 10) || 100,
|
||||
pass_rate: (function () {
|
||||
const v = $('#cfgPassRate') ? parseFloat($('#cfgPassRate').value) : NaN;
|
||||
return (isNaN(v) || v < 1 || v > 100) ? 60 : v;
|
||||
})(),
|
||||
layout: (document.querySelector('input[name=cfgLayout]:checked') || {}).value || 'default',
|
||||
full_scores: collectFullScores(),
|
||||
username: $('#cfgUser').value.trim(),
|
||||
history_display: $('#cfgHistoryDisplay').value,
|
||||
export_sort: $('#cfgExportSort').value,
|
||||
@@ -1245,15 +1230,6 @@
|
||||
$('#btnTestConn').addEventListener('click', testConnection);
|
||||
|
||||
// 收集"各试卷独立满分" -> {projectId: score}
|
||||
function collectFullScores() {
|
||||
const out = {};
|
||||
$$('#fullScoresBox input[data-pid]').forEach(inp => {
|
||||
const v = parseInt(inp.value, 10);
|
||||
if (inp.dataset.pid && !isNaN(v) && v > 0) out[inp.dataset.pid] = v;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/* 名单上传 */
|
||||
const zone = $('#dropZone'), fileInput = $('#rosterFile');
|
||||
zone.addEventListener('click', () => fileInput.click());
|
||||
@@ -1503,6 +1479,167 @@
|
||||
$('#trendModal').classList.add('open');
|
||||
});
|
||||
|
||||
/* 成绩明细打印:默认按成绩降序,可选按班级/组别分组对比 */
|
||||
function printTable() {
|
||||
const rows = (state._sorted || []).slice();
|
||||
if (!rows.length) return showAlert('当前没有可打印的成绩', 'warn');
|
||||
|
||||
const sortMode = $('#ptSort').value;
|
||||
const groupBy = $('#ptGroup').value; // none | cls | grp
|
||||
const showRank = $('#ptRank').checked;
|
||||
const showAtt = $('#ptAttempts').checked;
|
||||
const full = state.fullScore || 100;
|
||||
const passRate = (state.passRate == null ? 60 : state.passRate);
|
||||
|
||||
// 排序(打印独立于页面排序,默认成绩降序;null 成绩恒排末尾)
|
||||
const cmp = (a, b) => {
|
||||
const an = (a.score == null), bn = (b.score == null);
|
||||
if (an && bn) return 0;
|
||||
if (an) return 1;
|
||||
if (bn) return -1;
|
||||
if (sortMode === 'score_desc') return (b.score - a.score) || ((b.submit_ts || 0) - (a.submit_ts || 0));
|
||||
if (sortMode === 'score_asc') return (a.score - b.score) || ((b.submit_ts || 0) - (a.submit_ts || 0));
|
||||
if (sortMode === 'sid') return String(a.sid).localeCompare(String(b.sid), 'zh', { numeric: true });
|
||||
return ((b.submit_ts || 0) - (a.submit_ts || 0)); // time
|
||||
};
|
||||
rows.sort(cmp);
|
||||
|
||||
const passOf = r => (r.score != null && r.score >= (passRate / 100) * full);
|
||||
const statOf = list => {
|
||||
const sc = list.map(r => r.score).filter(v => v != null);
|
||||
const avg = sc.length ? (sc.reduce((x, y) => x + y, 0) / sc.length) : null;
|
||||
const pass = list.filter(passOf).length;
|
||||
return {
|
||||
n: list.length,
|
||||
avg: avg == null ? '—' : avg.toFixed(1),
|
||||
passRate: list.length ? Math.round(pass / list.length * 1000) / 10 : 0,
|
||||
};
|
||||
};
|
||||
|
||||
const head = '<tr><th>' + (showRank ? '名次' : '') + '</th><th>学号</th><th>姓名</th>' +
|
||||
(groupBy === 'none' ? '<th>班级</th><th>组别</th>' : '') +
|
||||
'<th>成绩</th>' + (showAtt ? '<th>考试次数</th>' : '') + '<th>提交时间</th></tr>';
|
||||
|
||||
let html = '';
|
||||
let rank = 0;
|
||||
const renderRows = (list, offsetRank) => list.map((r, i) => {
|
||||
const rk = (r.score == null) ? '' : (offsetRank + i + 1);
|
||||
return '<tr>' + (showRank ? '<td class="c">' + rk + '</td>' : '') +
|
||||
'<td>' + esc(r.sid || '') + '</td>' +
|
||||
'<td>' + esc(r.name || '') + '</td>' +
|
||||
(groupBy === 'none' ? '<td>' + esc(r.cls || '') + '</td><td>' + esc(r.grp || '') + '</td>' : '') +
|
||||
'<td class="c"><b>' + (r.score == null ? '—' : r.score) + '</b></td>' +
|
||||
(showAtt ? '<td class="c">' + (r.attempts || 1) + '</td>' : '') +
|
||||
'<td>' + esc(r.submit_at || '') + '</td></tr>';
|
||||
}).join('');
|
||||
|
||||
if (groupBy === 'none') {
|
||||
html = '<table><thead>' + head + '</thead><tbody>' + renderRows(rows, 0) + '</tbody></table>';
|
||||
} else {
|
||||
const keyOf = (groupBy === 'cls') ? (r => (r.cls || '未分班')) : (r => (r.grp || '未分组'));
|
||||
const groups = {};
|
||||
rows.forEach(r => { const k = keyOf(r); (groups[k] = groups[k] || []).push(r); });
|
||||
Object.keys(groups).sort((a, b) => String(a).localeCompare(String(b), 'zh', { numeric: true }))
|
||||
.forEach(k => {
|
||||
const list = groups[k];
|
||||
const st = statOf(list);
|
||||
html += '<h3 class="grp">' + esc(k) +
|
||||
' <small>(' + st.n + ' 人 · 平均 ' + st.avg + ' · 合格率 ' + st.passRate + '%)</small></h3>';
|
||||
html += '<table><thead>' + head + '</thead><tbody>' + renderRows(list, 0) + '</tbody></table>';
|
||||
});
|
||||
}
|
||||
|
||||
const all = statOf(rows);
|
||||
const title = (state.projectName ? state.projectName + ' · ' : '') + '成绩明细';
|
||||
const cls = $('#clsSel').value, grp = $('#grpSel').value;
|
||||
const scope = (cls ? cls + '班 ' : '') + (grp ? grp + '组' : '');
|
||||
|
||||
const w = window.open('', '_blank');
|
||||
if (!w) return showAlert('浏览器拦截了弹窗,请允许后重试', 'error');
|
||||
w.document.write('<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><title>' + esc(title) + '</title>' +
|
||||
'<style>' +
|
||||
'body{font-family:"Microsoft YaHei",sans-serif;margin:20px;color:#000}' +
|
||||
'h2{margin:0 0 4px;font-size:18px}' +
|
||||
'.meta{font-size:12px;color:#555;margin-bottom:14px}' +
|
||||
'h3.grp{font-size:14px;margin:18px 0 6px;padding:6px 8px;background:#f0f0f0;border-left:4px solid #333}' +
|
||||
'h3.grp small{font-weight:400;color:#555}' +
|
||||
'table{border-collapse:collapse;width:100%;margin-bottom:10px;font-size:12px}' +
|
||||
'th,td{border:1px solid #666;padding:5px 8px;text-align:left}' +
|
||||
'th{background:#e8e8e8;font-weight:700}' +
|
||||
'td.c{text-align:center}' +
|
||||
'@media print{body{margin:0}h3.grp{page-break-after:avoid}table{page-break-inside:auto}tr{page-break-inside:avoid}}' +
|
||||
'</style></head><body>' +
|
||||
'<h2>' + esc(title) + '</h2>' +
|
||||
'<div class="meta">' + esc(scope) + ' 共 ' + all.n + ' 人 · 平均 ' + all.avg +
|
||||
' · 合格率 ' + all.passRate + '%(合格线:满分的 ' + passRate + '%,即 ' +
|
||||
(Math.round(passRate / 100 * full * 10) / 10) + ' 分) · 打印时间:' + new Date().toLocaleString() + '</div>' +
|
||||
html +
|
||||
'<scr' + 'ipt>window.onload=function(){setTimeout(function(){window.print()},300)}</scr' + 'ipt>' +
|
||||
'</body></html>');
|
||||
w.document.close();
|
||||
}
|
||||
|
||||
$('#btnPrintTable').addEventListener('click', () => {
|
||||
if (!curPid()) return showAlert('请先选择问卷 / 考试');
|
||||
const n = (state._sorted || []).length;
|
||||
$('#ptHint').textContent = n ? ('当前将打印 ' + n + ' 条记录') : '暂无数据';
|
||||
$('#printTableModal').classList.add('open');
|
||||
});
|
||||
$('#btnPtPrint').addEventListener('click', () => {
|
||||
$('#printTableModal').classList.remove('open');
|
||||
printTable();
|
||||
});
|
||||
|
||||
/* 试卷独立设置(满分 / 合格率 / 免考名单)——仅对当前选中试卷生效 */
|
||||
async function openProjCfg() {
|
||||
const pid = curPid();
|
||||
if (!pid) return showAlert('请先选择问卷 / 考试');
|
||||
const pname = (($('#projectSel').selectedOptions && $('#projectSel').selectedOptions[0]) || {}).textContent || '';
|
||||
$('#projCfgSub').textContent = pname ? ('· ' + pname) : '';
|
||||
try {
|
||||
const d = await api('project_config', { params: { projectId: pid } });
|
||||
const cfg = d.cfg || {}, eff = d.effective || {}, g = d.global || {};
|
||||
$('#pcFull').value = (cfg.full_score == null ? '' : cfg.full_score);
|
||||
$('#pcPassRate').value = (cfg.pass_rate == null ? '' : cfg.pass_rate);
|
||||
$('#pcExempt').value = (cfg.exempt || []).join('\n');
|
||||
$('#pcFullHint').textContent = '当前生效:' + (eff.full_score || '-') +
|
||||
'(全局默认 ' + (g.full_score || '-') + ')';
|
||||
$('#pcPassHint').textContent = '当前生效:' + (eff.pass_rate || '-') +
|
||||
'%(全局默认 ' + (g.pass_rate || '-') + '%)';
|
||||
} catch (e) { showAlert(e.message); return; }
|
||||
$('#projCfgModal').classList.add('open');
|
||||
}
|
||||
$('#btnProjCfg').addEventListener('click', openProjCfg);
|
||||
|
||||
$('#btnPcSave').addEventListener('click', async () => {
|
||||
const pid = curPid();
|
||||
if (!pid) return showAlert('请先选择问卷 / 考试');
|
||||
try {
|
||||
await api('project_config_save', { method: 'POST', params: { projectId: pid }, body: {
|
||||
full_score: $('#pcFull').value.trim(),
|
||||
pass_rate: $('#pcPassRate').value.trim(),
|
||||
exempt: $('#pcExempt').value,
|
||||
}});
|
||||
$('#projCfgModal').classList.remove('open');
|
||||
showAlert('本卷设置已保存(仅对当前试卷生效)', 'info');
|
||||
refresh(false, true); // 免考会改变名单/缺考口径,需强制重拉
|
||||
} catch (e) { showAlert(e.message); }
|
||||
});
|
||||
|
||||
$('#btnPcReset').addEventListener('click', async () => {
|
||||
const pid = curPid();
|
||||
if (!pid) return;
|
||||
if (!confirm('确定清除本卷的全部独立设置(恢复跟随全局默认)?')) return;
|
||||
try {
|
||||
await api('project_config_save', { method: 'POST', params: { projectId: pid }, body: {
|
||||
full_score: '', pass_rate: '', exempt: '',
|
||||
}});
|
||||
$('#projCfgModal').classList.remove('open');
|
||||
showAlert('本卷设置已清除', 'info');
|
||||
refresh(false, true);
|
||||
} catch (e) { showAlert(e.message); }
|
||||
});
|
||||
|
||||
/* 异常检测线索 */
|
||||
function renderAnomaly() {
|
||||
const a = state.anomalies || { fast_high: [], frequent: [], identical: [] };
|
||||
|
||||
+46
-10
@@ -53,7 +53,12 @@ body {
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: 0 2px 12px rgba(20, 30, 60, .05);
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 12px; }
|
||||
.brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||
/* 导航栏主标题(已移除 Logo 与副标题,只保留单一主标题) */
|
||||
.brand-title {
|
||||
margin: 0; font-size: 16px; font-weight: 700; letter-spacing: .3px;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.logo {
|
||||
width: 38px; height: 38px;
|
||||
border-radius: 10px;
|
||||
@@ -469,32 +474,47 @@ body.fs-on .card b { font-size: 28px; }
|
||||
body.fs-on .topbar { padding-top: 8px; padding-bottom: 8px; }
|
||||
|
||||
/* ---------- 三端自适应(桌面 / 平板 / 手机) ---------- */
|
||||
|
||||
/* 大屏笔记本:略收边距,统计卡片保持一行 */
|
||||
@media (max-width: 1180px) {
|
||||
.wrap { padding: 16px 16px 40px; }
|
||||
.topbar { padding: 10px 16px; }
|
||||
}
|
||||
|
||||
/* 平板横屏(769~1024):卡片改 4 列、工具栏字段占满整行 */
|
||||
@media (max-width: 1024px) {
|
||||
.grid-2 { grid-template-columns: 1fr; }
|
||||
.field.grow, .field.grow-2 { flex: 1 1 100%; min-width: 0; }
|
||||
.cards { grid-template-columns: repeat(4, 1fr); }
|
||||
.toolbar { gap: 14px; }
|
||||
}
|
||||
|
||||
/* 平板竖屏 / 大手机(<=768):整体收紧,工具栏与按钮自动换行 */
|
||||
@media (max-width: 768px) {
|
||||
body { font-size: 13px; }
|
||||
.topbar { flex-wrap: wrap; gap: 10px; }
|
||||
.brand-text small { display: none; }
|
||||
.brand { flex: 1 1 100%; }
|
||||
.brand-title { font-size: 15px; }
|
||||
.top-right { flex-wrap: wrap; gap: 8px; }
|
||||
.top-right .btn { padding: 6px 10px; font-size: 12px; }
|
||||
.btn b { font-size: 10px; }
|
||||
.wrap { padding: 12px 12px 36px; }
|
||||
.toolbar { padding: 12px; gap: 10px; }
|
||||
.toolbar { padding: 12px; gap: 10px 12px; }
|
||||
.field { flex: 1 1 100%; min-width: 0; }
|
||||
.field.btns { flex: 1 1 100%; }
|
||||
.field.btns { flex: 1 1 100%; flex-wrap: wrap; row-gap: 8px; }
|
||||
.field.btns select { width: 100%; }
|
||||
/* 导出区的复选标签在小屏独占一行,避免和按钮挤成一团 */
|
||||
.field.btns > label[style*="cursor:pointer"] { flex: 1 1 auto; }
|
||||
.cards { grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||
.card { padding: 10px 12px; }
|
||||
.card b { font-size: 20px; }
|
||||
.panel-head { flex-direction: column; align-items: flex-start; gap: 6px; }
|
||||
.card span { font-size: 11px; }
|
||||
/* 面板头部(含按钮)竖排,按钮自动换行 */
|
||||
.panel-head { flex-direction: column; align-items: flex-start; gap: 8px; }
|
||||
/* 竖排时 .spacer(flex:1)会把内容上下撑开,必须隐藏 */
|
||||
.panel-head > .spacer { display: none; }
|
||||
.panel-head .updated { flex-wrap: wrap; width: 100%; }
|
||||
.panel-head .btn, .panel-head .updated .btn { flex: 0 0 auto; }
|
||||
.table-scroll { max-height: 56vh; }
|
||||
table.grid { font-size: 12px; }
|
||||
table.grid th { padding: 8px 10px; white-space: nowrap; }
|
||||
@@ -503,19 +523,36 @@ body.fs-on .topbar { padding-top: 8px; padding-bottom: 8px; }
|
||||
.modal-box, .modal-box.wide { width: 100%; max-height: 92vh; }
|
||||
.form-grid { grid-template-columns: 1fr; }
|
||||
.modal-body { padding: 14px; }
|
||||
/* 弹窗底栏在小屏换行,按钮可点满宽 */
|
||||
.modal-foot { flex-wrap: wrap; padding: 10px 14px; }
|
||||
.modal-foot > .spacer { display: none; }
|
||||
.modal-foot .btn { flex: 1 1 auto; }
|
||||
.roster-bar { flex-wrap: wrap; }
|
||||
.roster-bar .chk { font-size: 11px; }
|
||||
/* 名单表在窄屏可横向滚动,避免 6 列挤爆 */
|
||||
#rosterTable { min-width: 560px; }
|
||||
}
|
||||
|
||||
/* 手机(<=480):卡片两列,进一步压缩字号与内边距 */
|
||||
@media (max-width: 480px) {
|
||||
.cards { grid-template-columns: repeat(2, 1fr); }
|
||||
.brand-text strong { font-size: 14px; }
|
||||
.topbar .logo { width: 32px; height: 32px; font-size: 13px; }
|
||||
.cards { grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
||||
.brand-title { font-size: 14px; }
|
||||
.btn { padding: 6px 9px; font-size: 12px; }
|
||||
.field label { font-size: 11px; }
|
||||
.roster-bar { gap: 8px; }
|
||||
.roster-bar input[type=text] { width: 120px; }
|
||||
.panel-head h2 { font-size: 14px; }
|
||||
#rosterBody input { font-size: 12px; }
|
||||
.modal-box { max-height: 94vh; }
|
||||
.modal-body { padding: 12px; }
|
||||
/* 工具条按钮在小屏铺满,便于点按 */
|
||||
.field.btns .btn { flex: 1 1 auto; }
|
||||
}
|
||||
|
||||
/* 触摸设备:增大点按热区,去掉 hover 残留高亮 */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.btn { min-height: 34px; }
|
||||
table.grid tbody tr:hover td { background: inherit; }
|
||||
}
|
||||
|
||||
/* ---------- 数据看板 ---------- */
|
||||
@@ -525,8 +562,7 @@ body.fs-on .topbar { padding-top: 8px; padding-bottom: 8px; }
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.dash-hero .brand strong { color: #fff; font-size: 17px; }
|
||||
.dash-hero .brand small { color: rgba(255, 255, 255, .72); }
|
||||
.dash-hero .brand-title { color: #fff; font-size: 17px; }
|
||||
.dash-hero a.back { color: #fff; opacity: .9; font-size: 13px; text-decoration: none; font-weight: 600; }
|
||||
.dash-hero a.back:hover { opacity: 1; text-decoration: underline; }
|
||||
.dash-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
|
||||
Reference in New Issue
Block a user