feat: 全面打磨平台交互
- 名单导入容错:rowsToStudents 加去重/跳过统计、表头正则扩充、.0 尾巴清理;importFile 返回精确 count
- 看板:修复 .repeat(0) 空状态坏代码;新增实时点 stale 指示(手动/自动刷新失败变红 + 提示刷新中断);
手动刷新按钮加刷新中…反馈;记住自动刷新开关与间隔(localStorage + ?interval= 深链)
- 主统计页:后台静默刷新连续失败时连接指示转实时刷新中断告警;记住用户手动调整的刷新间隔
- 设置页:新增 config_test 端点 + 独立测试连接按钮(用弹窗内未保存凭据测连接,不落盘);
保存按钮加保存中…防重提交;登录失败不清旧 token
- 抽取页:重置结果加二次确认;新增复制名单按钮(两列制表符分隔,可粘到微信/Excel)
- 修复看板预先存在 JS 语法错误(少一个右括号导致整页 JS 失效)
This commit is contained in:
+39
-14
@@ -128,6 +128,10 @@ class Roster
|
||||
* 解析上传文件 -> 学生数组
|
||||
* @return array{count:int,students:array}
|
||||
*/
|
||||
/**
|
||||
* 解析上传文件 -> 学生数组
|
||||
* @return array{count:int,skipped:int,duplicates:int,headers:array,students:array}
|
||||
*/
|
||||
public static function importFile(string $file, string $ext = ''): array
|
||||
{
|
||||
if ($ext === '') {
|
||||
@@ -148,9 +152,17 @@ class Roster
|
||||
}
|
||||
|
||||
if ($rows === []) {
|
||||
throw new RuntimeException('文件为空或无法解析');
|
||||
throw new RuntimeException('文件为空或无法解析(请确认是 xlsx/csv 且包含数据行)');
|
||||
}
|
||||
return ['count' => count($rows), 'students' => self::rowsToStudents($rows)];
|
||||
$meta = [];
|
||||
$students = self::rowsToStudents($rows, $meta);
|
||||
return [
|
||||
'count' => count($students),
|
||||
'skipped' => $meta['skipped'],
|
||||
'duplicates' => $meta['duplicates'],
|
||||
'headers' => $meta['headers'],
|
||||
'students' => $students,
|
||||
];
|
||||
}
|
||||
|
||||
public static function parseCsv(string $file): array
|
||||
@@ -195,10 +207,13 @@ class Roster
|
||||
/**
|
||||
* 识别 学号/姓名/班级/组别/登陆账号 列
|
||||
* 登陆账号仅用于随机抽取页导出白名单,界面任何位置都不显示
|
||||
* @param array $rows
|
||||
* @param array $meta 输出:['skipped'=>int,'duplicates'=>int,'headers'=>array]
|
||||
* @return array<int,array{sid:string,name:string,cls:string,grp:string,login:string}>
|
||||
*/
|
||||
public static function rowsToStudents(array $rows): array
|
||||
public static function rowsToStudents(array $rows, array &$meta = []): array
|
||||
{
|
||||
$meta = ['skipped' => 0, 'duplicates' => 0, 'headers' => []];
|
||||
$sidCol = -1;
|
||||
$nameCol = -1;
|
||||
$clsCol = -1;
|
||||
@@ -206,11 +221,11 @@ class Roster
|
||||
$loginCol = -1;
|
||||
$headerIdx = -1;
|
||||
|
||||
// 找表头
|
||||
// 找表头:同一行需同时命中「学号类」与「姓名类」关键词(容错全角空格/间隔)
|
||||
foreach ($rows as $i => $row) {
|
||||
$joined = implode('|', array_map('strval', $row));
|
||||
if (preg_match('/学\s*号|编\s*号|序\s*号|no\.?|id/i', $joined)
|
||||
&& preg_match('/姓\s*名|名\s*字|学生|成员/i', $joined)) {
|
||||
if (preg_match('/学\s*号|编\s*号|序\s*号|学\s*员\s*号|号\s*码|no\.?|id/i', $joined)
|
||||
&& preg_match('/姓\s*名|名\s*字|学\s*生\s*姓\s*名|姓\s*氏|成\s*员/i', $joined)) {
|
||||
$headerIdx = $i;
|
||||
break;
|
||||
}
|
||||
@@ -220,22 +235,25 @@ class Roster
|
||||
$start = $headerIdx + 1;
|
||||
foreach ($rows[$headerIdx] as $j => $cell) {
|
||||
$c = (string)$cell;
|
||||
if ($sidCol < 0 && preg_match('/学\s*号|编\s*号|序\s*号|^\s*no\.?\s*$|^\s*id\s*$/i', $c)) {
|
||||
if ($sidCol < 0 && preg_match('/学\s*号|编\s*号|序\s*号|学\s*员\s*号|号\s*码|^\s*no\.?\s*$|^\s*id\s*$/i', $c)) {
|
||||
$sidCol = $j;
|
||||
}
|
||||
if ($nameCol < 0 && preg_match('/姓\s*名|名\s*字|学生姓名/i', $c)) {
|
||||
if ($nameCol < 0 && preg_match('/姓\s*名|名\s*字|学\s*生\s*姓\s*名|姓\s*氏/i', $c)) {
|
||||
$nameCol = $j;
|
||||
}
|
||||
if ($clsCol < 0 && preg_match('/班\s*级|班级|class/i', $c)) {
|
||||
$clsCol = $j;
|
||||
}
|
||||
if ($grpCol < 0 && preg_match('/组\s*别|分\s*组|^\s*组\s*$|group/i', $c)) {
|
||||
if ($grpCol < 0 && preg_match('/组\s*别|分\s*组|组\s*次|^\s*组\s*$|group/i', $c)) {
|
||||
$grpCol = $j;
|
||||
}
|
||||
if ($loginCol < 0 && preg_match('/登\s*[陆录]\s*账\s*号|登\s*[陆录]\s*账号|账\s*号|手\s*机\s*号|手\s*机/i', $c)) {
|
||||
$loginCol = $j;
|
||||
}
|
||||
}
|
||||
$meta['headers'] = [
|
||||
'sid' => $sidCol, 'name' => $nameCol, 'cls' => $clsCol, 'grp' => $grpCol, 'login' => $loginCol,
|
||||
];
|
||||
}
|
||||
if ($sidCol < 0) {
|
||||
$sidCol = 0;
|
||||
@@ -245,20 +263,27 @@ class Roster
|
||||
}
|
||||
|
||||
$out = [];
|
||||
$seen = [];
|
||||
for ($i = $start; $i < count($rows); $i++) {
|
||||
$row = $rows[$i];
|
||||
$sid = trim((string)($row[$sidCol] ?? ''));
|
||||
$name = trim((string)($row[$nameCol] ?? ''));
|
||||
if ($sid === '' && $name === '') {
|
||||
$meta['skipped']++;
|
||||
continue;
|
||||
}
|
||||
// 学号去掉 .0 尾巴(Excel 数字读取)
|
||||
$sid = preg_replace('/\.0+$/', '', $sid) ?? $sid;
|
||||
$cls = $clsCol >= 0 ? trim((string)($row[$clsCol] ?? '')) : '';
|
||||
$grp = $grpCol >= 0 ? trim((string)($row[$grpCol] ?? '')) : '';
|
||||
// 学号/登陆账号去掉 Excel 数字尾巴(如 2024012001.0 / 13912687810.0)
|
||||
$sid = preg_replace('/\.0+$/', '', $sid) ?? $sid;
|
||||
$cls = $clsCol >= 0 ? trim((string)($row[$clsCol] ?? '')) : '';
|
||||
$grp = $grpCol >= 0 ? trim((string)($row[$grpCol] ?? '')) : '';
|
||||
$login = $loginCol >= 0 ? trim((string)($row[$loginCol] ?? '')) : '';
|
||||
// 登陆账号同样去掉 Excel 数字尾巴(手机号被读成 13912687810.0)
|
||||
$login = preg_replace('/\.0+$/', '', $login) ?? $login;
|
||||
// 同一条学号多次出现:保留首次,其余记重复跳过(避免看板/统计被重复计入)
|
||||
if ($sid !== '' && isset($seen[$sid])) {
|
||||
$meta['duplicates']++;
|
||||
continue;
|
||||
}
|
||||
$seen[$sid] = true;
|
||||
$out[] = ['sid' => $sid, 'name' => $name, 'cls' => $cls, 'grp' => $grp, 'login' => $login];
|
||||
}
|
||||
return $out;
|
||||
|
||||
Reference in New Issue
Block a user