[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>