好友
阅读权限10
听众
最后登录1970-1-1
|
本帖最后由 zhubaihui 于 2026-7-16 08:46 编辑
均衡分班系统1.0.zip
(8.09 KB, 下载次数: 63)
中考结束,高一分班马上开始了,请大家帮忙测试一下这个分班好用不!
使用很简单,填好模板中的内容,设置好同名、双胞胎、固定班级等参数,导入。
设置好分班参数,均衡分班。还有缺点就是不能随机分班。希望有高手能改一下。
设置分班开始班号、和开始结束序号就是方便分层分班用的。如不需要可以默认就行了
设置好分班固定人数的和班数即可。
代码如下:[HTML] 纯文本查看 复制代码 <!DOCTYPE html><html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>2026级均衡分班系统</title>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* {box-sizing: border-box;margin:0;padding:0;font-family:"Microsoft Yahei",sans-serif}
body {background:#f5f7fa;padding:20px}
.container {max-width:1400px;margin:0 auto;background:#fff;padding:24px;border-radius:12px;box-shadow:0 2px 12px #e0e5ec}
h1 {text-align:center;color:#222;margin-bottom:20px;font-size:22px}
.row {display:flex;gap:16px;margin-bottom:16px;flex-wrap:wrap}
.card {border:1px solid #e5e7eb;padding:16px;border-radius:8px;flex:1;min-width:280px}
.card h3 {font-size:16px;margin-bottom:12px;color:#333}
input,select,button {padding:8px 12px;font-size:14px;border:1px solid #ccc;border-radius:6px}
input[type="file"] {border:none;padding:0}
button {background:#1677ff;color:#fff;border:none;cursor:pointer;margin-right:8px;transition:opacity .2s}
button:hover {opacity:0.9}
button.gray {background:#888}
button.green {background:#00b42a}
button.orange {background:#ff7d00}
button:disabled {background:#ccc;cursor:not-allowed}
.table-box {margin-top:20px;overflow:auto;max-height:400px;border:1px solid #eee;border-radius:8px}
table {width:100%;border-collapse:collapse;min-width:900px}
th,td {border:1px solid #eee;padding:8px 10px;font-size:13px;text-align:center}
th {background:#f0f7ff;position:sticky;top:0;z-index:1}
.tip {color:#666;font-size:13px;margin:8px 0}
.tip.error {color:#f53f3f}
.tip.success {color:#00b42a}
.tip.warn {color:#ff7d00}
.stat-box {margin-top:16px;padding:16px;background:#f8fafc;border-radius:8px;border:1px solid #e5e7eb}
.hidden {display:none!important}
.preview-table {max-height:300px}
.constraint-box {margin:8px 0;padding:12px;background:#fffbeb;border-radius:6px;border:1px solid #fff3cd}
#chartContainer {display:flex;gap:16px;margin-top:16px;flex-wrap:wrap;width:100%;min-height:350px}
.chart-wrapper {flex:1;min-width:400px;height:350px !important;width:auto !important;border:1px solid #eee;border-radius:8px;background:#fff}
</style>
</head>
<body>
<div class="container">
<h1>2026级学生均衡分班系统(分段+性别双均衡)</h1>
<!-- 1. 文件上传区 -->
<div class="card">
<h3>1. 导入学生模板Excel</h3>
<div style="margin-bottom:12px;padding:10px;background:#f0f7ff;border-radius:6px;border:1px dashed #91caff;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px">
<span class="tip" style="margin:0;color:#1677ff">📥 首次使用请先下载标准模板,按模板格式填写后上传;支持在固定列后任意添加扩展列</span>
<button id="downloadTemplateBtn" style="background:#1677ff;font-size:13px;padding:6px 16px">下载导入模板</button>
</div>
<input type="file" id="fileInput" accept=".xlsx,.xls">
<div id="uploadControls" class="hidden" style="margin-top:12px">
<div style="margin:8px 0;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<label>选择工作表:</label>
<select id="sheetSelect" style="min-width:150px"></select>
<button id="previewSheetBtn">预览选中工作表</button>
</div>
<div style="margin:8px 0;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<label>表头行号:</label>
<input type="number" id="headerRow" value="1" min="1" style="width:80px">
<label>数据起始行号:</label>
<input type="number" id="dataStartRow" value="2" min="1" style="width:80px">
<button id="confirmImportBtn" class="green">确认导入数据</button>
</div>
</div>
<div id="uploadTip" class="tip"></div>
<div id="previewBox" class="hidden table-box preview-table"></div>
</div>
<!-- 2. 参数配置与操作 -->
<div class="row">
<div class="card">
<h3>2. 分班参数配置</h3>
<div class="tip">分班序号范围:仅对该区间内无固定班级的学生进行分配</div>
<div style="margin:8px 0;display:flex;align-items:center;gap:8px">
<label>起始序号:</label>
<input type="number" id="startIdx" value="1" min="1" style="width:100px">
<label>结束序号:</label>
<input type="number" id="endIdx" value="1884" min="1" style="width:100px">
</div>
<div class="constraint-box">
<div style="margin:6px 0;display:flex;align-items:center;gap:8px">
<label>目标班级数量:</label>
<input type="number" id="classNum" value="6" min="2" max="50" style="width:100px">
<span class="tip">(最终班级数严格按此设置)</span>
</div>
<div style="margin:6px 0;display:flex;align-items:center;gap:8px">
<label>每班固定人数上限:</label>
<input type="number" id="fixedClassSize" value="32" min="10" max="200" style="width:100px">
<span class="tip">(单班人数绝不超过此值)</span>
</div>
<div style="margin:6px 0;display:flex;align-items:center;gap:8px">
<label>导出起始班号:</label>
<input type="number" id="exportStartClassNum" value="1" min="1" max="99" style="width:100px">
<span class="tip">(导出Excel中班级Sheet及统计表的起始编号)</span>
</div>
<div class="tip warn">⚠️ 系统严格满足「班级数固定」+「单班人数不超上限」双约束,同时保证各班分段与男女生人数基本一致</div>
</div>
<div style="margin-top:12px">
<button id="startSplit" class="orange">执行均衡分班</button>
<button class="gray" id="resetBtn">重置数据</button>
</div>
</div>
<div class="card">
<h3>3. 导出结果</h3>
<p class="tip">导出包含:①各班统计表 ②按班级命名的名单表 ③全量明细表(含所有扩展列)</p>
<button class="green" id="exportBtn" disabled>导出分班Excel</button>
</div>
</div>
<!-- 3. 统计与可视化面板 -->
<div class="stat-box hidden" id="statPanel">
<h3>分班统计概览</h3>
<div id="statContent" class="tip"></div>
<div id="chartContainer"></div>
</div>
<!-- 4. 分班结果表格 -->
<div class="table-box hidden" id="resultBox">
<table id="resultTable">
<thead id="resultThead">
<tr>
<th>序号</th><th>姓名</th><th>性别</th><th>身份证号码</th><th>总分</th>
<th>同名或近音</th><th>多胞胎</th><th>多胞胎同班</th>
<th>固定班级</th><th>分配班级</th>
</tr>
</thead>
<tbody id="resultTbody"></tbody>
</table>
</div>
</div>
<script>
// ==================== 全局变量 ====================
let allStudent = [];
let splitResult = [];
let workbook = null;
let chartInstances = [];
// 【新增】存储扩展列的表头名称和对应索引
let extraColumns = [];
const fileInput = document.getElementById('fileInput');
const uploadTip = document.getElementById('uploadTip');
const uploadControls = document.getElementById('uploadControls');
const sheetSelect = document.getElementById('sheetSelect');
const previewSheetBtn = document.getElementById('previewSheetBtn');
const headerRow = document.getElementById('headerRow');
const dataStartRow = document.getElementById('dataStartRow');
const confirmImportBtn = document.getElementById('confirmImportBtn');
const previewBox = document.getElementById('previewBox');
const startIdx = document.getElementById('startIdx');
const endIdx = document.getElementById('endIdx');
const classNumInput = document.getElementById('classNum');
const fixedClassSizeInput = document.getElementById('fixedClassSize');
const exportStartClassNumInput = document.getElementById('exportStartClassNum');
const startSplitBtn = document.getElementById('startSplit');
const resetBtn = document.getElementById('resetBtn');
const exportBtn = document.getElementById('exportBtn');
const statPanel = document.getElementById('statPanel');
const statContent = document.getElementById('statContent');
const resultBox = document.getElementById('resultBox');
const resultThead = document.getElementById('resultThead');
const resultTbody = document.getElementById('resultTbody');
const chartContainer = document.getElementById('chartContainer');
// 固定列的表头关键词映射(用于自动识别列位置)
const FIXED_FIELD_KEYS = {
serial: '序号',
name: '姓名',
gender: '性别',
idCard: '身份证',
score: '总分',
sameNameGroup: '同名',
twinGroup: '多胞胎组',
twinSameClass: '多胞胎同班',
fixedClass: '固定班级'
};
// ==================== 下载标准导入模板(含身份证+扩展列示例) ====================
document.getElementById('downloadTemplateBtn').addEventListener('click', function() {
const headers = ['序号', '姓名', '性别', '身份证号码', '总分', '同名或近音', '多胞胎组', '多胞胎同班', '固定班级', '备注', '毕业小学'];
const sampleData = [
[1, '张三', '男', '110101201401010011', 580, '', '', '否', '', '特长生', '实验小学'],
[2, '李四', '女', '110101201402020022', 575, '', '', '否', '', '', '育才小学'],
[3, '王五', '男', '110101201403030033', 570, 'A组', '双胞胎1', '是', '3班', '随迁子女', '新华小学']
];
const wsData = [headers, ...sampleData];
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(wsData);
ws['!cols'] = [
{ wch: 6 }, { wch: 10 }, { wch: 6 }, { wch: 20 }, { wch: 8 },
{ wch: 12 }, { wch: 12 }, { wch: 12 }, { wch: 10 }, { wch: 12 }, { wch: 12 }
];
XLSX.utils.book_append_sheet(wb, ws, '学生导入模板');
XLSX.writeFile(wb, '2026级学生导入模板.xlsx');
});
// ==================== 文件上传与解析 ====================
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(ev) {
try {
const data = new Uint8Array(ev.target.result);
workbook = XLSX.read(data, { type: 'array' });
sheetSelect.innerHTML = '';
workbook.SheetNames.forEach(name => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
sheetSelect.appendChild(option);
});
uploadControls.classList.remove('hidden');
uploadTip.innerHTML = `<span class="success">✅ 读取文件成功,共 ${workbook.SheetNames.length} 个工作表</span>`;
previewBox.classList.add('hidden');
} catch (err) {
uploadTip.innerHTML = `<span class="error">❌ 文件读取失败:${err.message}</span>`;
}
};
reader.readAsArrayBuffer(file);
});
// ==================== 预览工作表 ====================
previewSheetBtn.addEventListener('click', function() {
const sheetName = sheetSelect.value;
if (!sheetName || !workbook) return;
try {
const sheet = workbook.Sheets[sheetName];
const previewData = XLSX.utils.sheet_to_json(sheet, { header: 1, range: 0, raw: false });
previewBox.classList.remove('hidden');
let html = '<table><thead><tr>';
if (previewData[0]) previewData[0].forEach((cell, idx) => { html += `<th>${cell || '列'+(idx+1)}</th>`; });
html += '</tr></thead><tbody>';
previewData.slice(1, 21).forEach(row => {
html += '<tr>' + row.map(cell => `<td>${cell || ''}</td>`).join('') + '</tr>';
});
html += '</tbody></table>';
previewBox.innerHTML = html;
uploadTip.innerHTML = '<span class="success">✅ 预览成功,显示前20行数据,请确认表头行和数据起始行是否正确</span>';
} catch (err) {
uploadTip.innerHTML = `<span class="error">❌ 预览失败:${err.message}</span>`;
}
});
// ==================== 确认导入数据(支持动态列映射) ====================
confirmImportBtn.addEventListener('click', function() {
const sheetName = sheetSelect.value;
const headerRowNum = parseInt(headerRow.value) - 1;
const dataStartNum = parseInt(dataStartRow.value) - 1;
if (!sheetName || !workbook) { uploadTip.innerHTML = '<span class="error">❌ 请先选择工作表</span>'; return; }
if (isNaN(headerRowNum) || isNaN(dataStartNum) || headerRowNum >= dataStartNum) { uploadTip.innerHTML = '<span class="error">❌ 表头行号必须小于数据起始行号</span>'; return; }
try {
const sheet = workbook.Sheets[sheetName];
const allRows = XLSX.utils.sheet_to_json(sheet, { header: 1, raw: true });
const headerCells = allRows[headerRowNum] || [];
const dataRows = allRows.slice(dataStartNum);
const validRows = dataRows.filter(row => row.some(cell => cell !== null && cell !== undefined && cell !== ''));
// 自动识别固定列索引
const fieldIndexMap = {};
Object.entries(FIXED_FIELD_KEYS).forEach(([field, keyword]) => {
const idx = headerCells.findIndex(cell => String(cell || '').includes(keyword));
if (idx !== -1) fieldIndexMap[field] = idx;
});
// 校验必填列
if (fieldIndexMap.serial === undefined || fieldIndexMap.name === undefined) {
uploadTip.innerHTML = '<span class="error">❌ 未找到「序号」或「姓名」列,请检查表头是否包含对应关键词</span>';
return;
}
// 提取扩展列(不在固定列关键词范围内的列)
extraColumns = [];
headerCells.forEach((cell, idx) => {
const cellStr = String(cell || '');
const isFixed = Object.values(FIXED_FIELD_KEYS).some(kw => cellStr.includes(kw));
if (!isFixed && cellStr.trim() !== '') {
extraColumns.push({ name: cellStr, index: idx });
}
});
allStudent = validRows.map(row => {
const stu = {
serial: Number(row[fieldIndexMap.serial]) || 0,
name: row[fieldIndexMap.name] || '',
gender: String(row[fieldIndexMap.gender] || '').trim(),
idCard: String(row[fieldIndexMap.idCard] || '').trim(),
score: Number(row[fieldIndexMap.score]) || 0,
sameNameGroup: row[fieldIndexMap.sameNameGroup] || '',
twinGroup: row[fieldIndexMap.twinGroup] || '',
twinSameClass: String(row[fieldIndexMap.twinSameClass]).trim() === '是',
fixedClass: String(row[fieldIndexMap.fixedClass] || '').trim(),
targetClass: String(row[fieldIndexMap.fixedClass] || '').trim(),
segment: null,
extraData: {}
};
// 填充扩展列数据
extraColumns.forEach(col => {
stu.extraData[col.name] = row[col.index] ?? '';
});
return stu;
});
if (allStudent.length === 0) { uploadTip.innerHTML = '<span class="error">❌ 未读取到有效学生数据,请检查行号设置</span>'; return; }
uploadTip.innerHTML = `<span class="success">✅ 成功导入 ${allStudent.length} 名学生数据,识别到 ${extraColumns.length} 个扩展列:${extraColumns.map(c=>c.name).join('、') || '无'}</span>`;
endIdx.value = allStudent.length;
} catch (err) {
uploadTip.innerHTML = `<span class="error">❌ 数据解析失败:${err.message}</span>`;
}
});
// ==================== 核心均衡分班算法 ====================
startSplitBtn.addEventListener('click', function() {
const cNum = parseInt(classNumInput.value);
const fSize = parseInt(fixedClassSizeInput.value);
const sIdx = parseInt(startIdx.value) - 1;
const eIdx = parseInt(endIdx.value) - 1;
if (!allStudent.length) { alert('请先导入数据'); return; }
if (cNum < 2 || fSize < 1) { alert('班级数和固定人数必须大于0'); return; }
if (sIdx < 0 || eIdx >= allStudent.length || sIdx > eIdx) { alert('序号范围超出数据边界,请检查'); return; }
const toSplit = [], fixedList = [];
for (let i = 0; i < allStudent.length; i++) {
const stu = allStudent[i];
if (i >= sIdx && i <= eIdx && !stu.fixedClass) toSplit.push(stu);
else fixedList.push(stu);
}
const maxCapacity = cNum * fSize;
if (toSplit.length > maxCapacity) { alert(`⚠️ 待分班人数(${toSplit.length})超过最大容量(${cNum}×${fSize}=${maxCapacity})`); return; }
toSplit.sort((a, b) => b.score - a.score);
const segSize = Math.ceil(toSplit.length / 5);
toSplit.forEach((stu, idx) => { stu.segment = Math.min(Math.floor(idx / segSize), 4); });
const segGroups = {};
toSplit.forEach(stu => { if (!segGroups[stu.segment]) segGroups[stu.segment] = []; segGroups[stu.segment].push(stu); });
const classes = Array.from({ length: cNum }, () => []);
Object.keys(segGroups).sort((a, b) => a - b).forEach(seg => {
const group = segGroups[seg];
const males = group.filter(s => s.gender === '男');
const females = group.filter(s => s.gender !== '男');
const snakeAssign = (list) => {
for (let i = 0; i < list.length; i++) {
const cycle = Math.floor(i / cNum);
let clsIdx = i % cNum;
if (cycle % 2 === 1) clsIdx = cNum - 1 - clsIdx;
let tryCount = 0;
while (classes[clsIdx].length >= fSize && tryCount < cNum) { clsIdx = (clsIdx + 1) % cNum; tryCount++; }
if (tryCount >= cNum) break;
classes[clsIdx].push(list[i]);
}
};
snakeAssign(males);
snakeAssign(females);
});
classes.forEach((cls, idx) => { cls.forEach(stu => { stu.targetClass = `${idx + 1}班`; }); });
fixedList.forEach(stu => { stu.targetClass = stu.fixedClass; });
splitResult = [...fixedList, ...toSplit].sort((a, b) => a.serial - b.serial);
renderResultTable();
renderCharts(classes, cNum);
statPanel.classList.remove('hidden');
resultBox.classList.remove('hidden');
exportBtn.disabled = false;
statContent.innerHTML = `<span class="success">✅ 分班完成!共 ${cNum} 个班,待分班 ${toSplit.length} 人,固定班级 ${fixedList.length} 人</span>`;
});
// ==================== 渲染结果表格(动态包含扩展列) ====================
function renderResultTable() {
// 动态生成表头
let theadHtml = '<tr><th>序号</th><th>姓名</th><th>性别</th><th>身份证号码</th><th>总分</th>';
extraColumns.forEach(col => { theadHtml += `<th>${col.name}</th>`; });
theadHtml += '<th>同名或近音</th><th>多胞胎</th><th>多胞胎同班</th><th>固定班级</th><th>分配班级</th></tr>';
resultThead.innerHTML = theadHtml;
// 动态生成表格内容
resultTbody.innerHTML = splitResult.map(s => {
let rowHtml = `<tr>
<td>${s.serial}</td><td>${s.name}</td><td>${s.gender}</td><td>${s.idCard}</td><td>${s.score}</td>`;
extraColumns.forEach(col => { rowHtml += `<td>${s.extraData[col.name] ?? ''}</td>`; });
rowHtml += `<td>${s.sameNameGroup || ''}</td><td>${s.twinGroup || ''}</td>
<td>${s.twinSameClass ? '是' : '否'}</td><td>${s.fixedClass || ''}</td>
<td style="font-weight:bold;color:#1677ff">${s.targetClass}</td></tr>`;
return rowHtml;
}).join('');
}
// ==================== ECharts 可视化 ====================
function renderCharts(classes, cNum) {
chartInstances.forEach(inst => inst.dispose());
chartInstances = [];
chartContainer.innerHTML = `<div id="segChart" class="chart-wrapper"></div><div id="genderChart" class="chart-wrapper"></div>`;
requestAnimationFrame(() => {
const segDom = document.getElementById('segChart');
const genderDom = document.getElementById('genderChart');
if (!segDom || !genderDom || segDom.offsetWidth === 0) return;
const xLabels = Array.from({ length: cNum }, (_, i) => `${i + 1}班`);
const segChart = echarts.init(segDom);
const segColors = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de'];
segChart.setOption({
title: { text: '各班分段人数分布', left: 'center', textStyle: { fontSize: 14 } },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { bottom: 0, itemWidth: 12, itemHeight: 12, textStyle: { fontSize: 12 } },
grid: { top: 45, bottom: 40, left: 50, right: 20 },
xAxis: { type: 'category', data: xLabels, axisLabel: { fontSize: 12 } },
yAxis: { type: 'value', name: '人数', axisLabel: { fontSize: 12 } },
series: [0, 1, 2, 3, 4].map((seg, idx) => ({
name: `${seg + 1}段`, type: 'bar', stack: 'total',
itemStyle: { color: segColors[idx] }, emphasis: { focus: 'series' },
data: classes.map(cls => cls.filter(s => s.segment === seg).length)
}))
});
chartInstances.push(segChart);
const genderChart = echarts.init(genderDom);
genderChart.setOption({
title: { text: '各班男女生人数对比', left: 'center', textStyle: { fontSize: 14 } },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { bottom: 0, itemWidth: 12, itemHeight: 12, textStyle: { fontSize: 12 } },
grid: { top: 45, bottom: 40, left: 50, right: 20 },
xAxis: { type: 'category', data: xLabels, axisLabel: { fontSize: 12 } },
yAxis: { type: 'value', name: '人数', axisLabel: { fontSize: 12 } },
series: [
{ name: '男生', type: 'bar', itemStyle: { color: '#5470c6' }, data: classes.map(c => c.filter(s => s.gender === '男').length) },
{ name: '女生', type: 'bar', itemStyle: { color: '#ee6666' }, data: classes.map(c => c.filter(s => s.gender !== '男').length) }
]
});
chartInstances.push(genderChart);
});
}
window.addEventListener('resize', () => { chartInstances.forEach(inst => inst.resize()); });
// ==================== 导出Excel(含扩展列+身份证+起始班号) ====================
exportBtn.addEventListener('click', function() {
if (!splitResult.length) { alert('暂无分班结果'); return; }
const exportStartNum = parseInt(exportStartClassNumInput.value) || 1;
const wb = XLSX.utils.book_new();
// 构建内部班号 → 导出班号的映射关系
const classMap = {};
const internalClassNames = [];
splitResult.forEach(s => {
const cls = s.targetClass || '未分配';
if (!classMap[cls]) {
classMap[cls] = [];
internalClassNames.push(cls);
}
classMap[cls].push(s);
});
internalClassNames.sort((a, b) => {
const numA = parseInt(a.replace(/[^0-9]/g, '')) || 0;
const numB = parseInt(b.replace(/[^0-9]/g, '')) || 0;
return numA - numB;
});
const classLabelMap = {};
internalClassNames.forEach((internalName, idx) => {
const exportNum = exportStartNum + idx;
classLabelMap[internalName] = `${exportNum}班`;
});
// --- Sheet 1: 各班人数统计表 ---
const statHeaders = ['班级', '男生人数', '女生人数', '总人数', '平均分'];
const statData = [statHeaders];
internalClassNames.forEach(internalCls => {
const students = classMap[internalCls];
const maleCount = students.filter(s => s.gender === '男').length;
const femaleCount = students.length - maleCount;
const totalScore = students.reduce((sum, s) => sum + (Number(s.score) || 0), 0);
const avgScore = students.length > 0 ? (totalScore / students.length).toFixed(2) : '0.00';
statData.push([classLabelMap[internalCls], maleCount, femaleCount, students.length, Number(avgScore)]);
});
const totalMale = statData.slice(1).reduce((s, r) => s + r[1], 0);
const totalFemale = statData.slice(1).reduce((s, r) => s + r[2], 0);
const totalCount = statData.slice(1).reduce((s, r) => s + r[3], 0);
const totalAvg = totalCount > 0
? (splitResult.reduce((s, st) => s + (Number(st.score) || 0), 0) / totalCount).toFixed(2)
: '0.00';
statData.push(['合计', totalMale, totalFemale, totalCount, Number(totalAvg)]);
const statWs = XLSX.utils.aoa_to_sheet(statData);
statWs['!cols'] = [{ wch: 10 }, { wch: 10 }, { wch: 10 }, { wch: 10 }, { wch: 10 }];
XLSX.utils.book_append_sheet(wb, statWs, '各班统计表');
// --- Sheet 2~N: 按导出班号命名的分班名单表(含扩展列+身份证) ---
internalClassNames.forEach(internalCls => {
const students = classMap[internalCls];
const clsHeaders = ['序号', '姓名', '性别', '身份证号码', '总分'];
extraColumns.forEach(col => clsHeaders.push(col.name));
clsHeaders.push('同名或近音', '多胞胎组', '多胞胎同班', '固定班级');
const clsData = [clsHeaders];
students.forEach(s => {
const rowData = [s.serial, s.name, s.gender, s.idCard, s.score];
extraColumns.forEach(col => rowData.push(s.extraData[col.name] ?? ''));
rowData.push(s.sameNameGroup || '', s.twinGroup || '', s.twinSameClass ? '是' : '否', s.fixedClass || '');
clsData.push(rowData);
});
const clsWs = XLSX.utils.aoa_to_sheet(clsData);
const colWidths = [{ wch: 6 }, { wch: 10 }, { wch: 6 }, { wch: 20 }, { wch: 8 }];
extraColumns.forEach(() => colWidths.push({ wch: 12 }));
colWidths.push({ wch: 12 }, { wch: 12 }, { wch: 12 }, { wch: 10 });
clsWs['!cols'] = colWidths;
let sheetName = classLabelMap[internalCls].substring(0, 31).replace(/[:\\/?*\[\]]/g, '_');
XLSX.utils.book_append_sheet(wb, clsWs, sheetName);
});
// --- Sheet N+1: 全量分班明细表(含扩展列+身份证+导出班号) ---
const detailHeaders = ['序号', '姓名', '性别', '身份证号码', '总分'];
extraColumns.forEach(col => detailHeaders.push(col.name));
detailHeaders.push('同名组', '多胞胎组', '多胞胎同班', '固定班级', '分配班级');
const detailData = [detailHeaders];
splitResult.forEach(s => {
const displayClass = classLabelMap[s.targetClass] || s.targetClass;
const rowData = [s.serial, s.name, s.gender, s.idCard, s.score];
extraColumns.forEach(col => rowData.push(s.extraData[col.name] ?? ''));
rowData.push(s.sameNameGroup || '', s.twinGroup || '', s.twinSameClass ? '是' : '否', s.fixedClass || '', displayClass);
detailData.push(rowData);
});
const detailWs = XLSX.utils.aoa_to_sheet(detailData);
const detailColWidths = [{ wch: 6 }, { wch: 10 }, { wch: 6 }, { wch: 20 }, { wch: 8 }];
extraColumns.forEach(() => detailColWidths.push({ wch: 12 }));
detailColWidths.push({ wch: 10 }, { wch: 10 }, { wch: 10 }, { wch: 10 }, { wch: 10 });
detailWs['!cols'] = detailColWidths;
XLSX.utils.book_append_sheet(wb, detailWs, '全量分班明细');
XLSX.writeFile(wb, '2026级均衡分班结果.xlsx');
});
// ==================== 重置 ====================
resetBtn.addEventListener('click', function() {
allStudent = []; splitResult = []; workbook = null; extraColumns = [];
fileInput.value = ''; uploadTip.innerHTML = '';
uploadControls.classList.add('hidden'); previewBox.classList.add('hidden');
statPanel.classList.add('hidden'); resultBox.classList.add('hidden');
exportBtn.disabled = true;
chartInstances.forEach(inst => inst.dispose());
chartInstances = []; chartContainer.innerHTML = '';
// 重置表头为默认状态
resultThead.innerHTML = '<tr><th>序号</th><th>姓名</th><th>性别</th><th>身份证号码</th><th>总分</th><th>同名或近音</th><th>多胞胎</th><th>多胞胎同班</th><th>固定班级</th><th>分配班级</th></tr>';
});
</script>
</body>
</html>
全量分班明细没有按照参数开始班号生成分配班级,模板增加分配班级列,分配后班级号填充到此列中。更新了其他错误。欢迎大家试用。
更正后代码如下:[HTML] 纯文本查看 复制代码 <!DOCTYPE html><html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>2026级均衡分班系统(修正导出班号映射)</title>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* {box-sizing: border-box;margin:0;padding:0;font-family:"Microsoft Yahei",sans-serif}
body {background:#f5f7fa;padding:20px}
.container {max-width:1400px;margin:0 auto;background:#fff;padding:24px;border-radius:12px;box-shadow:0 2px 12px #e0e5ec}
h1 {text-align:center;color:#222;margin-bottom:20px;font-size:22px}
.row {display:flex;gap:16px;margin-bottom:16px;flex-wrap:wrap}
.card {border:1px solid #e5e7eb;padding:16px;border-radius:8px;flex:1;min-width:280px}
.card h3 {font-size:16px;margin-bottom:12px;color:#333}
input,select,button {padding:8px 12px;font-size:14px;border:1px solid #ccc;border-radius:6px}
input[type="file"] {border:none;padding:0}
button {background:#1677ff;color:#fff;border:none;cursor:pointer;margin-right:8px;transition:opacity .2s}
button:hover {opacity:0.9}
button.gray {background:#888}
button.green {background:#00b42a}
button.orange {background:#ff7d00}
button:disabled {background:#ccc;cursor:not-allowed}
.table-box {margin-top:20px;overflow:auto;max-height:400px;border:1px solid #eee;border-radius:8px}
table {width:100%;border-collapse:collapse;min-width:900px}
th,td {border:1px solid #eee;padding:8px 10px;font-size:13px;text-align:center}
th {background:#f0f7ff;position:sticky;top:0;z-index:1}
.tip {color:#666;font-size:13px;margin:8px 0}
.tip.error {color:#f53f3f}
.tip.success {color:#00b42a}
.tip.warn {color:#ff7d00}
.stat-box {margin-top:16px;padding:16px;background:#f8fafc;border-radius:8px;border:1px solid #e5e7eb}
.hidden {display:none!important}
.preview-table {max-height:300px}
.constraint-box {margin:8px 0;padding:12px;background:#fffbeb;border-radius:6px;border:1px solid #fff3cd}
#chartContainer {display:flex;gap:16px;margin-top:16px;flex-wrap:wrap;width:100%;min-height:350px}
.chart-wrapper {flex:1;min-width:400px;height:350px !important;width:auto !important;border:1px solid #eee;border-radius:8px;background:#fff}
</style>
</head>
<body>
<div class="container">
<h1>2026级学生均衡分班系统(✅ 修正导出起始班号映射)</h1>
<!-- 1. 文件上传区 -->
<div class="card">
<h3>1. 导入学生模板Excel</h3>
<div style="margin-bottom:12px;padding:10px;background:#f0f7ff;border-radius:6px;border:1px dashed #91caff;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px">
<span class="tip" style="margin:0;color:#1677ff">📥 模板含固定班级+分配班级列;支持任意扩展列</span>
<button id="downloadTemplateBtn" style="background:#1677ff;font-size:13px;padding:6px 16px">下载导入模板</button>
</div>
<input type="file" id="fileInput" accept=".xlsx,.xls">
<div id="uploadControls" class="hidden" style="margin-top:12px">
<div style="margin:8px 0;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<label>选择工作表:</label>
<select id="sheetSelect" style="min-width:150px"></select>
<button id="previewSheetBtn">预览选中工作表</button>
</div>
<div style="margin:8px 0;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<label>表头行号:</label>
<input type="number" id="headerRow" value="1" min="1" style="width:80px">
<label>数据起始行号:</label>
<input type="number" id="dataStartRow" value="2" min="1" style="width:80px">
<button id="confirmImportBtn" class="green">确认导入数据</button>
</div>
</div>
<div id="uploadTip" class="tip"></div>
<div id="previewBox" class="hidden table-box preview-table"></div>
</div>
<!-- 2. 参数配置与操作 -->
<div class="row">
<div class="card">
<h3>2. 分班参数配置</h3>
<div class="tip">⚠️ 仅对下方序号范围内、且「固定班级」为空的学生执行分班</div>
<div style="margin:8px 0;display:flex;align-items:center;gap:8px">
<label>起始序号:</label>
<input type="number" id="startIdx" value="1" min="1" style="width:100px">
<label>结束序号:</label>
<input type="number" id="endIdx" value="300" min="1" style="width:100px">
</div>
<div class="constraint-box">
<div style="margin:6px 0;display:flex;align-items:center;gap:8px">
<label>目标班级数量:</label>
<input type="number" id="classNum" value="6" min="2" max="50" style="width:100px">
<span class="tip">(仅对范围内学生生效)</span>
</div>
<div style="margin:6px 0;display:flex;align-items:center;gap:8px">
<label>每班固定人数上限:</label>
<input type="number" id="fixedClassSize" value="50" min="10" max="200" style="width:100px">
<span class="tip">(单班人数绝不超过此值)</span>
</div>
<div style="margin:6px 0;display:flex;align-items:center;gap:8px">
<label>导出起始班号:</label>
<input type="number" id="exportStartClassNum" value="7" min="1" max="99" style="width:100px">
<span class="tip">(✅ 已修正:全量明细表同步应用此起始号)</span>
</div>
<div class="tip warn">✅ 修复:当起始班号设为7时,全量明细表中分配班级从7班开始,而非1班</div>
</div>
<div style="margin-top:12px">
<button id="startSplit" class="orange">执行均衡分班</button>
<button class="gray" id="resetBtn">重置数据</button>
</div>
</div>
<div class="card">
<h3>3. 导出结果</h3>
<p class="tip">导出包含:<br>① 各班统计表(仅统计范围内学生)<br>② 各班名单Sheet(仅含范围内学生)<br>③ 全量明细表(含全部学生,班号已映射)</p>
<button class="green" id="exportBtn" disabled>导出分班Excel</button>
</div>
</div>
<!-- 3. 统计与可视化面板 -->
<div class="stat-box hidden" id="statPanel">
<h3>分班统计概览(仅统计分班范围内学生)</h3>
<div id="statContent" class="tip"></div>
<div id="chartContainer"></div>
</div>
<!-- 4. 分班结果表格 -->
<div class="table-box hidden" id="resultBox">
<table id="resultTable">
<thead id="resultThead"></thead>
<tbody id="resultTbody"></tbody>
</table>
</div>
</div>
<script>
// ==================== 全局变量 ====================
let allStudent = [];
let splitResult = [];
let rangedStudents = [];
let workbook = null;
let chartInstances = [];
let extraColumns = [];
const fileInput = document.getElementById('fileInput');
const uploadTip = document.getElementById('uploadTip');
const uploadControls = document.getElementById('uploadControls');
const sheetSelect = document.getElementById('sheetSelect');
const previewSheetBtn = document.getElementById('previewSheetBtn');
const headerRow = document.getElementById('headerRow');
const dataStartRow = document.getElementById('dataStartRow');
const confirmImportBtn = document.getElementById('confirmImportBtn');
const previewBox = document.getElementById('previewBox');
const startIdx = document.getElementById('startIdx');
const endIdx = document.getElementById('endIdx');
const classNumInput = document.getElementById('classNum');
const fixedClassSizeInput = document.getElementById('fixedClassSize');
const exportStartClassNumInput = document.getElementById('exportStartClassNum');
const startSplitBtn = document.getElementById('startSplit');
const resetBtn = document.getElementById('resetBtn');
const exportBtn = document.getElementById('exportBtn');
const statPanel = document.getElementById('statPanel');
const statContent = document.getElementById('statContent');
const resultBox = document.getElementById('resultBox');
const resultThead = document.getElementById('resultThead');
const resultTbody = document.getElementById('resultTbody');
const chartContainer = document.getElementById('chartContainer');
const FIXED_FIELD_KEYS = {
serial: '序号', name: '姓名', gender: '性别', idCard: '身份证',
score: '总分', sameNameGroup: '同名', twinGroup: '多胞胎组',
twinSameClass: '多胞胎同班', fixedClass: '固定班级', assignedClass: '分配班级'
};
// ==================== 下载模板 ====================
document.getElementById('downloadTemplateBtn').addEventListener('click', function() {
const headers = ['序号','姓名','性别','身份证号码','总分','同名或近音','多胞胎组','多胞胎同班','固定班级','分配班级'];
const sampleData = [
[1,'张三','男','110101201401010011',580,'','','否','',''],
[2,'李四','女','110101201402020022',575,'','','否','',''],
[3,'王五','男','110101201403030033',570,'A组','双胞胎1','是','3班','']
];
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet([headers, ...sampleData]);
ws['!cols'] = [{wch:6},{wch:10},{wch:6},{wch:20},{wch:8},{wch:12},{wch:12},{wch:12},{wch:10},{wch:10}];
XLSX.utils.book_append_sheet(wb, ws, '学生导入模板');
XLSX.writeFile(wb, '2026级学生导入模板.xlsx');
});
// ==================== 文件读取 & 预览 ====================
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0]; if (!file) return;
const reader = new FileReader();
reader.onload = function(ev) {
try {
workbook = XLSX.read(new Uint8Array(ev.target.result), {type:'array'});
sheetSelect.innerHTML = '';
workbook.SheetNames.forEach(n => { const o=document.createElement('option'); o.value=n; o.textContent=n; sheetSelect.appendChild(o); });
uploadControls.classList.remove('hidden');
uploadTip.innerHTML = `<span class="success">✅ 读取成功,共 ${workbook.SheetNames.length} 个工作表</span>`;
previewBox.classList.add('hidden');
} catch(err) { uploadTip.innerHTML = `<span class="error">❌ ${err.message}</span>`; }
};
reader.readAsArrayBuffer(file);
});
previewSheetBtn.addEventListener('click', function() {
const sn = sheetSelect.value; if(!sn||!workbook) return;
try {
const d = XLSX.utils.sheet_to_json(workbook.Sheets[sn],{header:1,range:0,raw:false});
previewBox.classList.remove('hidden');
let h='<table><thead><tr>'; if(d[0]) d[0].forEach((c,i)=>{h+=`<th>${c||'列'+(i+1)}</th>`;}); h+='</tr></thead><tbody>';
d.slice(1,21).forEach(r=>{h+='<tr>'+r.map(c=>`<td>${c||''}</td>`).join('')+'</tr>';}); h+='</tbody></table>';
previewBox.innerHTML=h; uploadTip.innerHTML='<span class="success">✅ 预览成功</span>';
} catch(err) { uploadTip.innerHTML=`<span class="error">❌ ${err.message}</span>`; }
});
// ==================== 确认导入 ====================
confirmImportBtn.addEventListener('click', function() {
const sn=sheetSelect.value, hr=parseInt(headerRow.value)-1, dr=parseInt(dataStartRow.value)-1;
if(!sn||!workbook){uploadTip.innerHTML='<span class="error">❌ 请先选择工作表</span>';return;}
if(isNaN(hr)||isNaN(dr)||hr>=dr){uploadTip.innerHTML='<span class="error">❌ 行号设置无效</span>';return;}
try {
const allRows=XLSX.utils.sheet_to_json(workbook.Sheets[sn],{header:1,raw:true});
const hc=allRows[hr]||[], validRows=allRows.slice(dr).filter(r=>r.some(c=>c!=null&&c!==undefined&&c!==''));
const fim={}; Object.entries(FIXED_FIELD_KEYS).forEach(([f,k])=>{const i=hc.findIndex(c=>String(c||'').includes(k));if(i!==-1)fim[f]=i;});
if(fim.serial===undefined||fim.name===undefined){uploadTip.innerHTML='<span class="error">❌ 未找到「序号」或「姓名」列</span>';return;}
extraColumns=[]; hc.forEach((c,i)=>{const s=String(c||'');if(!Object.values(FIXED_FIELD_KEYS).some(k=>s.includes(k))&&s.trim())extraColumns.push({name:s,index:i});});
allStudent=validRows.map(r=>{
const stu={serial:Number(r[fim.serial])||0,name:r[fim.name]||'',gender:String(r[fim.gender]||'').trim(),
idCard:String(r[fim.idCard]||'').trim(),score:Number(r[fim.score])||0,
sameNameGroup:r[fim.sameNameGroup]||'',twinGroup:r[fim.twinGroup]||'',
twinSameClass:String(r[fim.twinSameClass]).trim()==='是',
fixedClass:String(r[fim.fixedClass]||'').trim(),
assignedClass:'', segment:null, extraData:{}, originalIndex:0};
extraColumns.forEach(col=>{stu.extraData[col.name]=r[col.index]??'';});
return stu;
});
if(!allStudent.length){uploadTip.innerHTML='<span class="error">❌ 无有效数据</span>';return;}
uploadTip.innerHTML=`<span class="success">✅ 导入 ${allStudent.length} 人,${extraColumns.length} 个扩展列</span>`;
endIdx.value=allStudent.length;
} catch(err) { uploadTip.innerHTML=`<span class="error">❌ ${err.message}</span>`; }
});
// ==================== 核心分班算法 ====================
startSplitBtn.addEventListener('click', function() {
const cNum=parseInt(classNumInput.value), fSize=parseInt(fixedClassSizeInput.value);
const sIdx=parseInt(startIdx.value), eIdx=parseInt(endIdx.value);
if(!allStudent.length){alert('请先导入数据');return;}
if(cNum<2||fSize<1){alert('参数无效');return;}
const toSplit=[], outOfRange=[];
allStudent.forEach((stu,i)=>{
stu.originalIndex=i;
stu.assignedClass='';
const inRange = stu.serial>=sIdx && stu.serial<=eIdx;
if(inRange && !stu.fixedClass) toSplit.push(stu);
else outOfRange.push(stu);
});
if(!toSplit.length){alert(`序号 ${sIdx}-${eIdx} 范围内没有待分班学生`);return;}
if(toSplit.length>cNum*fSize){alert(`⚠️ 范围内${toSplit.length}人超过容量${cNum}×${fSize}`);return;}
toSplit.sort((a,b)=>b.score-a.score);
const segSize=Math.ceil(toSplit.length/5);
toSplit.forEach((s,i)=>{s.segment=Math.min(Math.floor(i/segSize),4);});
const segGroups={}; toSplit.forEach(s=>{if(!segGroups[s.segment])segGroups[s.segment]=[];segGroups[s.segment].push(s);});
const classes=Array.from({length:cNum},()=>[]);
Object.keys(segGroups).sort((a,b)=>a-b).forEach(seg=>{
const g=segGroups[seg], m=g.filter(s=>s.gender==='男'), f=g.filter(s=>s.gender!=='男');
const snake=(list)=>{for(let i=0;i<list.length;i++){const cy=Math.floor(i/cNum);let ci=i%cNum;if(cy%2===1)ci=cNum-1-ci;let tc=0;while(classes[ci].length>=fSize&&tc<cNum){ci=(ci+1)%cNum;tc++;}if(tc>=cNum)break;classes[ci].push(list[i]);}};
snake(m); snake(f);
});
classes.forEach((cls,idx)=>{cls.forEach(s=>{s.assignedClass=`${idx+1}班`;});});
rangedStudents = [...toSplit];
splitResult = [...allStudent].sort((a,b)=>a.originalIndex-b.originalIndex);
renderResultTable();
renderCharts(classes, cNum);
statPanel.classList.remove('hidden');
resultBox.classList.remove('hidden');
exportBtn.disabled=false;
statContent.innerHTML=`<span class="success">✅ 分班完成!范围 ${sIdx}-${eIdx},待分班 ${toSplit.length} 人,范围外 ${outOfRange.length} 人</span>`;
});
// ==================== 渲染结果表格 ====================
function renderResultTable() {
let th='<tr><th>序号</th><th>姓名</th><th>性别</th><th>身份证</th><th>总分</th>';
extraColumns.forEach(c=>{th+=`<th>${c.name}</th>`;});
th+='<th>同名组</th><th>多胞胎组</th><th>同班</th><th>固定班级</th><th>分配班级</th></tr>';
resultThead.innerHTML=th;
resultTbody.innerHTML=splitResult.map(s=>{
let r=`<tr><td>${s.serial}</td><td>${s.name}</td><td>${s.gender}</td><td>${s.idCard}</td><td>${s.score}</td>`;
extraColumns.forEach(c=>{r+=`<td>${s.extraData[c.name]??''}</td>`;});
const ac=s.assignedClass||(s.fixedClass?`<span style="color:#888">${s.fixedClass}(固定)</span>`:'');
r+=`<td>${s.sameNameGroup||''}</td><td>${s.twinGroup||''}</td><td>${s.twinSameClass?'是':'否'}</td><td>${s.fixedClass||''}</td><td style="font-weight:bold;color:#1677ff">${ac}</td></tr>`;
return r;
}).join('');
}
// ==================== ECharts ====================
function renderCharts(classes,cNum){
chartInstances.forEach(i=>i.dispose()); chartInstances=[];
chartContainer.innerHTML='<div id="segChart" class="chart-wrapper"></div><div id="genderChart" class="chart-wrapper"></div>';
requestAnimationFrame(()=>{
const sd=document.getElementById('segChart'),gd=document.getElementById('genderChart');
if(!sd||!gd||sd.offsetWidth===0)return;
const xl=Array.from({length:cNum},(_,i)=>`${i+1}班`);
const sc=['#5470c6','#91cc75','#fac858','#ee6666','#73c0de'];
const c1=echarts.init(sd);
c1.setOption({title:{text:'各班分段人数分布',left:'center',textStyle:{fontSize:14}},tooltip:{trigger:'axis',axisPointer:{type:'shadow'}},legend:{bottom:0,itemWidth:12,itemHeight:12,textStyle:{fontSize:12}},grid:{top:45,bottom:40,left:50,right:20},xAxis:{type:'category',data:xl,axisLabel:{fontSize:12}},yAxis:{type:'value',name:'人数',axisLabel:{fontSize:12}},series:[0,1,2,3,4].map((seg,idx)=>({name:`${seg+1}段`,type:'bar',stack:'total',itemStyle:{color:sc[idx]},emphasis:{focus:'series'},data:classes.map(cls=>cls.filter(s=>s.segment===seg).length)}))});
chartInstances.push(c1);
const c2=echarts.init(gd);
c2.setOption({title:{text:'各班男女生人数对比',left:'center',textStyle:{fontSize:14}},tooltip:{trigger:'axis',axisPointer:{type:'shadow'}},legend:{bottom:0,itemWidth:12,itemHeight:12,textStyle:{fontSize:12}},grid:{top:45,bottom:40,left:50,right:20},xAxis:{type:'category',data:xl,axisLabel:{fontSize:12}},yAxis:{type:'value',name:'人数',axisLabel:{fontSize:12}},series:[{name:'男生',type:'bar',itemStyle:{color:'#5470c6'},data:classes.map(c=>c.filter(s=>s.gender==='男').length)},{name:'女生',type:'bar',itemStyle:{color:'#ee6666'},data:classes.map(c=>c.filter(s=>s.gender!=='男').length)}]});
chartInstances.push(c2);
});
}
window.addEventListener('resize',()=>{chartInstances.forEach(i=>i.resize());});
// ==================== ✅ 导出Excel(修正全量明细班号映射) ====================
exportBtn.addEventListener('click', function() {
if(!splitResult.length){alert('暂无分班结果');return;}
const exportStartNum=parseInt(exportStartClassNumInput.value)||1;
const wb=XLSX.utils.book_new();
// 🔑 关键修复:将班号映射提升到导出函数的全局作用域
// 确保统计表、班级名单、全量明细三处使用同一套映射关系
const classMap={}, internalNames=[];
rangedStudents.forEach(s=>{
const cls=s.assignedClass; if(!cls)return;
if(!classMap[cls]){classMap[cls]=[];internalNames.push(cls);}
classMap[cls].push(s);
});
internalNames.sort((a,b)=>(parseInt(a.replace(/[^0-9]/g,''))||0)-(parseInt(b.replace(/[^0-9]/g,''))||0));
// 构建全局映射表:内部班号(1班) -> 导出班号(7班)
const globalClassLabelMap = {};
internalNames.forEach((n,i)=>{
globalClassLabelMap[n] = `${exportStartNum + i}班`;
});
// --- Sheet1: 统计表 ---
const sh=['班级','男生','女生','总人数','平均分'], sd=[sh];
internalNames.forEach(n=>{
const st=classMap[n], mc=st.filter(s=>s.gender==='男').length, fc=st.length-mc;
const ts=st.reduce((s,x)=>s+(Number(x.score)||0),0), avg=st.length?(ts/st.length).toFixed(2):'0.00';
sd.push([globalClassLabelMap[n],mc,fc,st.length,Number(avg)]);
});
const tm=sd.slice(1).reduce((s,r)=>s+r[1],0), tf=sd.slice(1).reduce((s,r)=>s+r[2],0), tc=sd.slice(1).reduce((s,r)=>s+r[3],0);
const ta=tc?(rangedStudents.reduce((s,x)=>s+(Number(x.score)||0),0)/tc).toFixed(2):'0.00';
sd.push(['合计',tm,tf,tc,Number(ta)]);
const sw=XLSX.utils.aoa_to_sheet(sd); sw['!cols']=[{wch:10},{wch:8},{wch:8},{wch:8},{wch:10}];
XLSX.utils.book_append_sheet(wb,sw,'各班统计表');
// --- Sheet2~N: 各班名单 ---
internalNames.forEach(n=>{
const st=classMap[n];
const ch=['序号','姓名','性别','身份证号码','总分'];
extraColumns.forEach(c=>ch.push(c.name));
ch.push('同名或近音','多胞胎组','多胞胎同班','固定班级');
const cd=[ch];
st.forEach(s=>{
const row=[s.serial,s.name,s.gender,s.idCard,s.score];
extraColumns.forEach(c=>row.push(s.extraData[c.name]??''));
row.push(s.sameNameGroup||'',s.twinGroup||'',s.twinSameClass?'是':'否',s.fixedClass||'');
cd.push(row);
});
const cw=XLSX.utils.aoa_to_sheet(cd);
const widths=[{wch:6},{wch:10},{wch:6},{wch:20},{wch:8}];
extraColumns.forEach(()=>widths.push({wch:12}));
widths.push({wch:12},{wch:12},{wch:12},{wch:10});
cw['!cols']=widths;
// 使用全局映射表获取正确的Sheet名称
let sn = (globalClassLabelMap[n] || n).substring(0,31).replace(/[:\\/?*\[\]]/g,'_');
XLSX.utils.book_append_sheet(wb,cw,sn);
});
// --- Sheet N+1: 全量明细(🔑 使用 globalClassLabelMap 替换原始班号) ---
const dh=['序号','姓名','性别','身份证号码','总分'];
extraColumns.forEach(c=>dh.push(c.name));
dh.push('同名组','多胞胎组','多胞胎同班','固定班级','分配班级');
const dd=[dh];
splitResult.forEach(s=>{
const row=[s.serial,s.name,s.gender,s.idCard,s.score];
extraColumns.forEach(c=>row.push(s.extraData[c.name]??''));
// 🔑 核心修正:通过全局映射表转换班号,而非直接使用 s.assignedClass
const displayAssignedClass = s.assignedClass ? (globalClassLabelMap[s.assignedClass] || s.assignedClass) : '';
row.push(s.sameNameGroup||'',s.twinGroup||'',s.twinSameClass?'是':'否',s.fixedClass||'', displayAssignedClass);
dd.push(row);
});
const dw=XLSX.utils.aoa_to_sheet(dd);
const dWidths=[{wch:6},{wch:10},{wch:6},{wch:20},{wch:8}];
extraColumns.forEach(()=>dWidths.push({wch:12}));
dWidths.push({wch:10},{wch:10},{wch:10},{wch:10},{wch:10});
dw['!cols']=dWidths;
XLSX.utils.book_append_sheet(wb,dw,'全量分班明细');
XLSX.writeFile(wb,'2026级均衡分班结果.xlsx');
});
// ==================== 重置 ====================
resetBtn.addEventListener('click',function(){
allStudent=[];splitResult=[];rangedStudents=[];workbook=null;extraColumns=[];
fileInput.value='';uploadTip.innerHTML='';
uploadControls.classList.add('hidden');previewBox.classList.add('hidden');
statPanel.classList.add('hidden');resultBox.classList.add('hidden');
exportBtn.disabled=true;
chartInstances.forEach(i=>i.dispose());chartInstances=[];chartContainer.innerHTML='';
});
</script>
</body>
</html>
更新功能,增加打印分班名单。纠正了同名及双胞胎同班或强制拆分的细节。
代码就不放了,都放附件里了,欢迎大家试用。 |
免费评分
-
查看全部评分
|