- 登录门:进入页面先输入系统网址/API路由/账号/密码,测试连接通过后才显示登录按钮,登录后才进入主界面;密码每次需输入,防止依赖一次配置直接查看数据 - 每套试卷独立满分配置(设置中按当前试卷单独设置,合格率按 ≥60% 满分计算) - 班级联动统计与面板计数(成绩明细/缺考名单标题计数随班级过滤) - 移除最高/最低分获得者显示 - 平均用时不换行(避免“秒”字换行) - 卷王地址默认 https://exam.nxxy335.top,默认勾选校验 HTTPS 证书 - 统计卡片含合格率(≥60% 满分) - .gitignore 排除 data/(账号密码/令牌/学生名单,禁止入库)
115 lines
4.1 KiB
JavaScript
115 lines
4.1 KiB
JavaScript
// 用 JS 复现 XlsxReader 的 ZIP/XML 解析逻辑,验证对真实 导入模板.xlsx 的处理
|
|
const fs = require('fs');
|
|
|
|
function u16(d, o) { const s = d.subarray(o, o + 2); return s.length < 2 ? 0 : s.readUInt16LE(0); }
|
|
function u32(d, o) { const s = d.subarray(o, o + 4); return s.length < 4 ? 0 : s.readUInt32LE(0); }
|
|
|
|
function parseZipRaw(data) {
|
|
let eocd = -1;
|
|
for (let i = data.length - 22; i >= 0; i--) {
|
|
if (data[i] === 0x50 && data[i + 1] === 0x4b && data[i + 2] === 0x05 && data[i + 3] === 0x06) { eocd = i; break; }
|
|
}
|
|
if (eocd < 0) throw new Error('no EOCD');
|
|
const count = u16(data, eocd + 10);
|
|
const offset = u32(data, eocd + 16);
|
|
const out = {};
|
|
let p = offset;
|
|
for (let i = 0; i < count; i++) {
|
|
if (!(data[p] === 0x50 && data[p + 1] === 0x4b && data[p + 2] === 0x01 && data[p + 3] === 0x02)) break;
|
|
const method = u16(data, p + 10);
|
|
const compSize = u32(data, p + 20);
|
|
const nameLen = u16(data, p + 28);
|
|
const extraLen = u16(data, p + 30);
|
|
const commentLen = u16(data, p + 32);
|
|
const localOff = u32(data, p + 42);
|
|
const name = data.subarray(p + 46, p + 46 + nameLen).toString('binary');
|
|
const lNameLen = u16(data, localOff + 26);
|
|
const lExtraLen = u16(data, localOff + 28);
|
|
const start = localOff + 30 + lNameLen + lExtraLen;
|
|
const raw = data.subarray(start, start + compSize);
|
|
let content;
|
|
if (method === 0) content = raw;
|
|
else content = require('zlib').inflateRawSync(raw);
|
|
out[name.replace(/\\/g, '/')] = content.toString('utf8');
|
|
p += 46 + nameLen + extraLen + commentLen;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function decodeEntities(s) {
|
|
return s.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
.replace(/'/g, "'").replace(/'/g, "'").replace(/&/g, '&');
|
|
}
|
|
|
|
function parseSharedStrings(xml) {
|
|
const out = [];
|
|
const re = /<si>([\s\S]*?)<\/si>/g;
|
|
let m;
|
|
while ((m = re.exec(xml))) {
|
|
const si = m[1];
|
|
const tre = /<t[^>]*>([\s\S]*?)<\/t>/g;
|
|
let t, parts = [];
|
|
while ((t = tre.exec(si))) parts.push(decodeEntities(t[1]));
|
|
out.push(parts.join(''));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function colToIndex(letters) {
|
|
let n = 0;
|
|
for (let i = 0; i < letters.length; i++) n = n * 26 + (letters.charCodeAt(i) - 64);
|
|
return n - 1;
|
|
}
|
|
|
|
function parseSheet(xml, shared) {
|
|
const rows = [];
|
|
const rowRe = /<row\b[^>]*>([\s\S]*?)<\/row>/g;
|
|
let rm;
|
|
while ((rm = rowRe.exec(xml))) {
|
|
const rowXml = rm[1];
|
|
const cells = {};
|
|
const cRe = /<c(?:\s([^>]*?))?(?:\/>|>([\s\S]*?)<\/c>)/g;
|
|
let cm, autoIdx = 0;
|
|
while ((cm = cRe.exec(rowXml))) {
|
|
const attrs = cm[1] || '';
|
|
const inner = cm[2] || '';
|
|
let idx = autoIdx;
|
|
const r = /\br="([A-Z]+)\d*"/.exec(attrs);
|
|
if (r) idx = colToIndex(r[1]);
|
|
autoIdx = idx + 1;
|
|
const tm = /\bt="([^"]+)"/.exec(attrs);
|
|
const type = tm ? tm[1] : '';
|
|
let val = '';
|
|
if (type === 'inlineStr') {
|
|
const tre = /<t[^>]*>([\s\S]*?)<\/t>/g;
|
|
let t, parts = [];
|
|
while ((t = tre.exec(inner))) parts.push(decodeEntities(t[1]));
|
|
val = parts.join('');
|
|
} else {
|
|
const vm = /<v>([\s\S]*?)<\/v>/.exec(inner);
|
|
val = vm ? decodeEntities(vm[1]) : '';
|
|
if (type === 's') val = shared[parseInt(val, 10)] || '';
|
|
else if (type === 'b') val = parseInt(val, 10) === 1 ? 'TRUE' : 'FALSE';
|
|
}
|
|
cells[idx] = val;
|
|
}
|
|
const keys = Object.keys(cells).map(Number);
|
|
if (!keys.length) continue;
|
|
const max = Math.max(...keys);
|
|
const line = [];
|
|
for (let i = 0; i <= max; i++) line.push(cells[i] || '');
|
|
rows.push(line);
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
const file = process.argv[2];
|
|
const entries = parseZipRaw(fs.readFileSync(file));
|
|
console.log('entries:', Object.keys(entries).join(', '));
|
|
const shared = entries['xl/sharedStrings.xml'] ? parseSharedStrings(entries['xl/sharedStrings.xml']) : [];
|
|
const sheet = entries['xl/worksheets/sheet1.xml'] || entries[Object.keys(entries).find(k => /^xl\/worksheets\/sheet\d+\.xml$/.test(k))];
|
|
const rows = parseSheet(sheet, shared);
|
|
console.log('rows:', rows.length);
|
|
console.log('first 3:', JSON.stringify(rows.slice(0, 3), null, 0));
|
|
console.log('last 2:', JSON.stringify(rows.slice(-2), null, 0));
|