feat: 导出按班级/组别筛选 + 随机抽取页 + 零依赖 XLSX 导出
导出跟随分类筛选 - api.php export / export_absent 新增 cls、grp 参数,按当前筛选过滤 rows - 缺考口径与页面完全一致:筛选后名单 − 筛选后已提交(按学号匹配) - 导出文件名附带班级 / 组别(如 成绩明细_高一数学月考_高一(1)班_A组_时间戳.csv) - app.js 两个导出按钮追加 cls/grp 参数并提示导出范围 随机抽取页 draw.php - 来源:学号名单 / 某试卷已提交人员;支持班级、组别筛选与抽取人数设置 - 「排除已抽中」累计去重,抽取结果存入 localStorage,刷新后仍可下载 - 抽取动画(滚动定格),结果表格含序号 / 学号 / 姓名 / 班级 / 组别 - 下载「白名单导入模板.xlsx」「白名单列表.xlsx」:两列 姓名 / 登陆账号 XLSX 导出能力 - 新增 lib/XlsxWriter.php:零依赖生成 xlsx,有 zip 扩展走 ZipArchive, 无则回退手写 ZIP(stored);单元格按共享字符串 + 文本格式(numFmtId=49) 写入,长学号不会变科学计数法 - 新增 api.php action=draw_export,输出与「白名单列表.xlsx」模板一致的结构 其他 - index.php 顶栏新增「随机抽取」入口;支持 index.php#roster 直接打开名单管理 - tools/test-xlsxwriter.php 自测:18 项全通过(含无 ZipArchive 的手写打包路径)
This commit is contained in:
@@ -326,7 +326,7 @@ try {
|
||||
Roster::clear();
|
||||
ny_ok(['students' => [], 'meta' => Roster::meta()]);
|
||||
|
||||
// ---------------- 导出 ----------------
|
||||
// ---------------- 导出(支持按班级 / 组别筛选,与页面当前筛选保持一致) ----------------
|
||||
case 'export':
|
||||
case 'export_absent':
|
||||
$projectId = (string)($_GET['projectId'] ?? '');
|
||||
@@ -334,34 +334,80 @@ try {
|
||||
ny_fail('请先选择问卷/考试');
|
||||
}
|
||||
$nameQid = (string)($_GET['nameQid'] ?? '');
|
||||
$clsFilter = trim((string)($_GET['cls'] ?? ''));
|
||||
$grpFilter = trim((string)($_GET['grp'] ?? ''));
|
||||
|
||||
// 命中筛选:未选班级/组别时全部通过
|
||||
$hit = static function (array $r) use ($clsFilter, $grpFilter): bool {
|
||||
if ($clsFilter !== '' && trim((string)($r['cls'] ?? '')) !== $clsFilter) {
|
||||
return false;
|
||||
}
|
||||
if ($grpFilter !== '' && trim((string)($r['grp'] ?? '')) !== $grpFilter) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
$c = cfg();
|
||||
$result = DataService::build($c, $projectId, $nameQid);
|
||||
|
||||
$rows = array_values(array_filter((array)($result['rows'] ?? []), $hit));
|
||||
|
||||
// 缺考口径与页面完全保持一致:筛选后名单 − 筛选后已提交(按学号匹配)
|
||||
$roster = array_values(array_filter((array)($result['roster'] ?? []), $hit));
|
||||
if ($roster !== []) {
|
||||
$submitted = [];
|
||||
foreach ($rows as $r) {
|
||||
$sid = trim((string)($r['sid'] ?? ''));
|
||||
if ($sid !== '') {
|
||||
$submitted[$sid] = true;
|
||||
}
|
||||
}
|
||||
$absent = array_values(array_filter(
|
||||
$roster,
|
||||
static fn(array $s): bool => !isset($submitted[trim((string)($s['sid'] ?? ''))])
|
||||
));
|
||||
} else {
|
||||
// 未导入名单时退回后端自带的缺考数组(同样按筛选过滤)
|
||||
$absent = array_values(array_filter((array)($result['absent'] ?? []), $hit));
|
||||
}
|
||||
|
||||
// 文件名附带班级 / 组别,便于分辨
|
||||
$safe = static fn(string $s): string => preg_replace('/[\\\\\/:*?"<>|]/u', '_', $s) ?? '';
|
||||
$clsLabel = $clsFilter === '' ? ''
|
||||
: (mb_strpos($clsFilter, '班') === false ? $clsFilter . '班' : $clsFilter);
|
||||
$grpLabel = $grpFilter === '' ? ''
|
||||
: (mb_strpos($grpFilter, '组') === false ? $grpFilter . '组' : $grpFilter);
|
||||
$tag = ($clsLabel !== '' ? '_' . $safe($clsLabel) : '')
|
||||
. ($grpLabel !== '' ? '_' . $safe($grpLabel) : '');
|
||||
|
||||
$filename = ($action === 'export_absent' ? '缺考名单' : '成绩明细')
|
||||
. '_' . preg_replace('/[\\\\\/:*?"<>|]/u', '_', (string)$result['project']['name'])
|
||||
. '_' . $safe((string)$result['project']['name'])
|
||||
. $tag
|
||||
. '_' . date('Ymd_His') . '.csv';
|
||||
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"; filename*=UTF-8\'\'' . rawurlencode($filename));
|
||||
header('Cache-Control: no-store');
|
||||
echo "\xEF\xBB\xBF";
|
||||
$out = fopen('php://output', 'w');
|
||||
|
||||
if ($action === 'export_absent') {
|
||||
fputcsv($out, ['学号', '姓名', '班级', '组别']);
|
||||
foreach ($result['absent'] as $r) {
|
||||
fputcsv($out, [$r['sid'], $r['name'], $r['cls'] ?? '', $r['grp'] ?? '']);
|
||||
foreach ($absent as $r) {
|
||||
fputcsv($out, [$r['sid'] ?? '', $r['name'] ?? '', $r['cls'] ?? '', $r['grp'] ?? '']);
|
||||
}
|
||||
} else {
|
||||
fputcsv($out, ['学号', '姓名', '班级', '组别', '成绩', '提交时间', '考试时长', '考试时长(秒)']);
|
||||
foreach ($result['rows'] as $r) {
|
||||
foreach ($rows as $r) {
|
||||
fputcsv($out, [
|
||||
$r['sid'],
|
||||
$r['name'],
|
||||
$r['sid'] ?? '',
|
||||
$r['name'] ?? '',
|
||||
$r['cls'] ?? '',
|
||||
$r['grp'] ?? '',
|
||||
$r['score'] === null ? '' : $r['score'],
|
||||
$r['submit_at'],
|
||||
$r['duration_txt'],
|
||||
$r['submit_at'] ?? '',
|
||||
$r['duration_txt'] ?? '',
|
||||
$r['duration'] === null ? '' : $r['duration'],
|
||||
]);
|
||||
}
|
||||
@@ -369,6 +415,39 @@ try {
|
||||
fclose($out);
|
||||
exit;
|
||||
|
||||
// ---------------- 随机抽取:导出白名单(xlsx,列:姓名 / 登陆账号) ----------------
|
||||
case 'draw_export':
|
||||
$raw = (string)file_get_contents('php://input');
|
||||
$in = json_decode($raw, true);
|
||||
if (!is_array($in)) {
|
||||
$in = $_POST + $_GET;
|
||||
}
|
||||
// type=import → 白名单导入模板.xlsx;type=list → 白名单列表.xlsx
|
||||
$type = (($in['type'] ?? '') === 'import') ? 'import' : 'list';
|
||||
$members = [];
|
||||
foreach ((array)($in['members'] ?? []) as $m) {
|
||||
if (!is_array($m)) {
|
||||
continue;
|
||||
}
|
||||
$name = trim((string)($m['name'] ?? ''));
|
||||
$sid = trim((string)($m['sid'] ?? ''));
|
||||
if ($name === '' && $sid === '') {
|
||||
continue;
|
||||
}
|
||||
$members[] = [$name, $sid];
|
||||
}
|
||||
$bin = XlsxWriter::render(
|
||||
array_merge([['姓名', '登陆账号']], $members),
|
||||
['widths' => [16, 24], 'title' => '白名单']
|
||||
);
|
||||
$filename = $type === 'import' ? '白名单导入模板.xlsx' : '白名单列表.xlsx';
|
||||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
header('Content-Disposition: attachment; filename="whitelist.xlsx"; filename*=UTF-8\'\'' . rawurlencode($filename));
|
||||
header('Content-Length: ' . strlen($bin));
|
||||
header('Cache-Control: no-store');
|
||||
echo $bin;
|
||||
exit;
|
||||
|
||||
default:
|
||||
ny_fail('未知操作:' . $action);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user