Files
ny-exam-platform/index.php
T
nxxy335 6cd17b9cb2 fix: 修复保存满分后清登录;配置双写;设置显示账号/密码;抽取比例改按人数加权
- 修复:仅当连接信息(base_url/api_prefix/username/password/verify_ssl)真变化时才清 token + 重连;
  保存满分/布局等不应触发登录卷王(bug:之前一律 unlink token.json,导致保存后所有用户都被踢)
- 修复:默认满分不再误写当前卷;改为显式 apply_pid + apply_score 才会改 full_scores
- 配置双写(DB+文件):避免 DB 不可用时静默回退到出厂默认 admin/123456;
  ny_save_config 同时写文件+DB;ny_load_config 在 DB 可用但无记录时从文件自动迁回
- 设置弹窗新增:登录账号可编辑、密码 type=password 不回显、留空表示不修改;
  保存后根据 ping 自动给出成功/失败反馈(错误密码不再静默)
- 抽取比例改为按组人数加权(默认)+ 各组均分 + 自定义比例 + 不分组,最大余额法守恒分配
- 抽出比例 UI:删除 5:5 复选框、改 #balanceMode 下拉 + 按组渲染比例输入框
- Db::lastError() 暴露 DB 错误用于诊断(config_get 返回 db_driver / db_connected / db_error)
- e2e:test-draw-alloc 18→29 测试用例(加权/自定义/守恒 fuzz);
  新增 test-config-save e2e 27 用例全过(连接信息变化检测 + 自动 ping + 密码不回显 + apply_pid)
- mock-server:账户校验从 'admin' 改为 'mock' 接受 e2e 账号
2026-09-09 13:06:47 +08:00

280 lines
12 KiB
PHP

<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
if (!ny_is_installed()) {
header('Location: install.php');
exit;
}
if (!ny_is_auth()) {
header('Location: login.php');
exit;
}
$cfg = ny_load_config();
$rosterCount = Roster::count();
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars(NY_APP_NAME) ?></title>
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<header class="topbar">
<div class="brand">
<span class="logo">NY</span>
<div class="brand-text">
<strong>NY问卷考试系统数据平台</strong>
<small>卷王问卷系统 · 实时数据统计</small>
</div>
</div>
<div class="top-right">
<div id="conn" class="conn conn-off"><i></i><span>未连接</span></div>
<a class="btn" id="btnDashboard" href="dashboard.php" target="_blank" rel="noopener" title="在新标签页打开数据看板">📊 数据看板</a>
<a class="btn" id="btnDraw" href="draw.php" target="_blank" rel="noopener" title="在新标签页打开随机抽取">🎲 随机抽取</a>
<button class="btn" id="btnFullscreen" title="切换全屏">⛶ 全屏</button>
<button class="btn" id="btnRoster">名单管理 <b id="rosterBadge"><?= (int)$rosterCount ?></b></button>
<button class="btn btn-primary" id="btnSettings">设置</button>
<button class="btn" id="btnLogout" title="退出登录">退出</button>
</div>
</header>
<main class="wrap">
<section class="toolbar">
<div class="field grow">
<label>问卷 / 考试</label>
<select id="projectSel"><option value="">加载中…</option></select>
</div>
<div class="field">
<label>姓名字段</label>
<select id="nameFieldSel"><option value="">自动识别</option></select>
</div>
<div class="field">
<label>班级</label>
<select id="clsSel"><option value="">全部班级</option></select>
</div>
<div class="field">
<label>组别</label>
<select id="grpSel"><option value="">全部组别</option></select>
</div>
<div class="field">
<label>自动刷新</label>
<div class="inline">
<label class="switch"><input type="checkbox" id="autoRefresh" checked><span></span></label>
<select id="intervalSel">
<option value="5">5 秒</option>
<option value="10">10 秒</option>
<option value="15" selected>15 秒</option>
<option value="30">30 秒</option>
<option value="60">60 秒</option>
<option value="120">2 分钟</option>
</select>
</div>
</div>
<div class="field btns">
<button class="btn btn-primary" id="btnRefresh">立即刷新</button>
</div>
<div class="field grow-2">
<label>搜索</label>
<input type="text" id="kw" placeholder="输入学号或姓名筛选">
</div>
<div class="field btns">
<button class="btn" id="btnExport">导出成绩</button>
<button class="btn" id="btnExportAbsent">导出缺考</button>
</div>
</section>
<div id="alert" class="alert hidden"></div>
<section class="cards" id="statCards">
<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 warn accent-orange"><span>缺考</span><b id="sAbsent">-</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-red"><span>最低分</span><b id="sMin">-</b></div>
<div class="card accent-indigo"><span>合格率(≥60%满分)</span><b id="sPass">-</b></div>
<div class="card accent-gray"><span>平均用时</span><b id="sDur">-</b></div>
</section>
<div class="layout" id="dataLayout" data-layout="default">
<section class="panel" id="tablePanel">
<div class="panel-head">
<h2>成绩明细 <small id="tableHint"></small></h2>
<div class="updated">最后更新:<span id="lastUpdate">—</span><i id="liveDot" class="dot"></i></div>
</div>
<div class="table-scroll">
<table class="grid" id="dataTable">
<thead>
<tr>
<th class="sortable" data-k="sid">学号</th>
<th class="sortable" data-k="name">姓名</th>
<th class="sortable" data-k="cls">班级</th>
<th class="sortable" data-k="grp">组别</th>
<th class="sortable" data-k="score">成绩</th>
<th class="sortable" data-k="submit_ts">提交时间</th>
<th class="sortable" data-k="duration">考试时长</th>
</tr>
</thead>
<tbody id="tbody"><tr><td colspan="7" class="empty">请选择问卷 / 考试</td></tr></tbody>
</table>
</div>
</section>
<section class="panel" id="absentPanel">
<div class="panel-head"><h2>缺考名单 <small id="absentHint"></small></h2></div>
<div class="table-scroll short">
<table class="grid compact">
<thead><tr><th>学号</th><th>姓名</th><th>班级</th><th>组别</th></tr></thead>
<tbody id="absentBody"><tr><td colspan="4" class="empty">—</td></tr></tbody>
</table>
</div>
</section>
<section class="panel" id="unmatchedPanel">
<div class="panel-head"><h2>未匹配答卷 <small>姓名不在名单中</small></h2></div>
<div class="table-scroll short">
<table class="grid compact">
<thead><tr><th>姓名</th><th>成绩</th><th>提交时间</th></tr></thead>
<tbody id="unmatchedBody"><tr><td colspan="3" class="empty">—</td></tr></tbody>
</table>
</div>
</section>
</div>
</main>
<!-- ================= 设置 ================= -->
<div class="modal" id="settingsModal">
<div class="modal-box">
<div class="modal-head"><h3>系统设置</h3><button class="x" data-close>&times;</button></div>
<div class="modal-body">
<div class="form-grid">
<div class="fi full">
<label>布局</label>
<div class="fi-inline">
<label class="chk"><input type="radio" name="cfgLayout" value="default" id="layoutDef"> 默认(缺考+未匹配同行)</label>
<label class="chk"><input type="radio" name="cfgLayout" value="side" id="layoutSide"> 成绩明细与缺考名单同行</label>
</div>
<p class="hint">主界面三个面板(成绩明细 / 缺考名单 / 未匹配答卷)的排版方式</p>
</div>
<div class="fi">
<label>默认刷新间隔(秒)</label>
<input type="number" id="cfgInterval" min="3" max="600">
</div>
<div class="fi">
<label>请求超时(秒)</label>
<input type="number" id="cfgTimeout" min="3" max="120">
</div>
<div class="fi">
<label>每页拉取条数</label>
<input type="number" id="cfgPageSize" min="20" max="1000">
</div>
<div class="fi">
<label>默认满分(用于合格率)</label>
<input type="number" id="cfgFull" min="1" max="1000">
<p class="hint" id="cfgFullHint">未选中试卷时的默认值</p>
</div>
<div class="fi full">
<label>姓名题关键词</label>
<input type="text" id="cfgKeywords">
<p class="hint">英文逗号分隔,用于自动识别"姓名"题目</p>
</div>
<div class="fi full">
<label>各试卷独立满分</label>
<div id="fullScoresBox" class="fullscores">
<span class="hint" id="fullScoresHint">未选中试卷时,此处为空;选中某套试卷后保存即在设置中为该卷单独设定满分</span>
</div>
<p class="hint">给某卷填入数字即可单独覆盖默认满分;留空=用默认</p>
</div>
<div class="fi full checks">
<label><input type="checkbox" id="cfgDedupe"> 同一学号只保留最新一份答卷</label>
<label><input type="checkbox" id="cfgTemp"> 包含"暂存"未提交的答卷</label>
<label><input type="checkbox" id="cfgSsl"> 校验 HTTPS 证书</label>
</div>
<div class="fi full" style="margin-top:6px">
<div style="display:flex;align-items:center;gap:8px">
<label style="margin:0">🔌 卷王系统连接</label>
<span class="hint" id="cfgConnInfo" style="font-size:12px"></span>
</div>
</div>
<div class="fi full">
<label>系统地址(不可在此修改)</label>
<input type="text" id="cfgBase" readonly>
<p class="hint">如需修改系统地址请重新打开 <a href="install.php?force=1" target="_blank">install.php</a></p>
</div>
<div class="fi">
<label>登录账号</label>
<input type="text" id="cfgUser" autocomplete="username" placeholder="登录卷王的账号">
</div>
<div class="fi">
<label>登录密码</label>
<input type="password" id="cfgPass" autocomplete="new-password" placeholder="已保存 · 留空表示不修改">
<p class="hint">为安全,已保存的密码不回显;如需修改请直接输入新密码后保存</p>
</div>
</div>
<div id="pingResult" class="ping-result hidden"></div>
</div>
<div class="modal-foot">
<span class="hint" style="align-self:center">只有修改账号/密码才会触发重连与测连接</span>
<span class="spacer"></span>
<button class="btn" data-close>取消</button>
<button class="btn btn-primary" id="btnSaveCfg">保存</button>
</div>
</div>
</div>
<!-- ================= 名单管理 ================= -->
<div class="modal" id="rosterModal">
<div class="modal-box wide">
<div class="modal-head"><h3>学号名单管理</h3><button class="x" data-close>&times;</button></div>
<div class="modal-body">
<div class="upload-zone" id="dropZone">
<input type="file" id="rosterFile" accept=".xlsx,.xlsm,.csv,.txt" hidden>
<div class="uz-inner">
<strong>点击或拖拽文件到此上传</strong>
<span>支持 .xlsx / .csv,需包含「学号」「姓名」两列(「班级」「组别」可选,组别如 A/B)</span>
</div>
</div>
<div class="roster-bar">
<div class="inline">
<label class="chk"><input type="radio" name="impMode" value="replace" checked> 覆盖</label>
<label class="chk"><input type="radio" name="impMode" value="append"> 追加</label>
</div>
<input type="text" id="rosterSearch" placeholder="搜索学号 / 姓名">
<span class="spacer"></span>
<button class="btn sm" id="btnAddRow">+ 新增</button>
<button class="btn sm danger" id="btnClearRoster">清空</button>
<button class="btn sm btn-primary" id="btnSaveRoster">保存名单</button>
</div>
<div class="table-scroll mid">
<table class="grid compact" id="rosterTable">
<thead><tr><th style="width:28%">学号</th><th style="width:22%">姓名</th><th>班级</th><th>组别</th><th style="width:70px">操作</th></tr></thead>
<tbody id="rosterBody"></tbody>
</table>
</div>
<p class="hint" id="rosterMeta">共 0 人</p>
</div>
<div class="modal-foot">
<span class="spacer"></span>
<button class="btn" data-close>关闭</button>
</div>
</div>
</div>
<script>
window.NY = {
appName: <?= json_encode(NY_APP_NAME, JSON_UNESCAPED_UNICODE) ?>,
version: <?= json_encode(NY_VERSION, JSON_UNESCAPED_UNICODE) ?>,
interval: <?= (int)$cfg['refresh_interval'] ?>
};
</script>
<script src="assets/app.js"></script>
</body>
</html>