吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 49|回复: 2
上一主题 下一主题
收起左侧

[其他原创] 2026级学生均衡分班系统(分段+性别双均衡) 1. 导入学生模板Excel

[复制链接]
跳转到指定楼层
楼主
zhubaihui 发表于 2026-7-13 11:06 回帖奖励
均衡分班系统1.0.zip (8.09 KB, 下载次数: 2)

中考结束,高一分班马上开始了,请大家帮忙测试一下这个分班好用不!
使用很简单,填好模板中的内容,设置好同名、双胞胎、固定班级等参数,导入。
设置好分班参数,均衡分班。还有缺点就是不能随机分班。希望有高手能改一下。
设置分班开始班号、和开始结束序号就是方便分层分班用的。如不需要可以默认就行了
设置好分班固定人数的和班数即可。
代码如下:
[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">&#128229; 首次使用请先下载标准模板,按模板格式填写后上传;支持在固定列后任意添加扩展列</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">&#9888;&#65039; 系统严格满足「班级数固定」+「单班人数不超上限」双约束,同时保证各班分段与男女生人数基本一致</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">&#9989; 读取文件成功,共 ${workbook.SheetNames.length} 个工作表</span>`;
            previewBox.classList.add('hidden');
        } catch (err) {
            uploadTip.innerHTML = `<span class="error">&#10060; 文件读取失败:${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">&#9989; 预览成功,显示前20行数据,请确认表头行和数据起始行是否正确</span>';
    } catch (err) {
        uploadTip.innerHTML = `<span class="error">&#10060; 预览失败:${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">&#10060; 请先选择工作表</span>'; return; }
    if (isNaN(headerRowNum) || isNaN(dataStartNum) || headerRowNum >= dataStartNum) { uploadTip.innerHTML = '<span class="error">&#10060; 表头行号必须小于数据起始行号</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">&#10060; 未找到「序号」或「姓名」列,请检查表头是否包含对应关键词</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">&#10060; 未读取到有效学生数据,请检查行号设置</span>'; return; }
        uploadTip.innerHTML = `<span class="success">&#9989; 成功导入 ${allStudent.length} 名学生数据,识别到 ${extraColumns.length} 个扩展列:${extraColumns.map(c=>c.name).join('、') || '无'}</span>`;
        endIdx.value = allStudent.length;
    } catch (err) {
        uploadTip.innerHTML = `<span class="error">&#10060; 数据解析失败:${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(`&#9888;&#65039; 待分班人数(${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">&#9989; 分班完成!共 ${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>

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
SimonXQM 发表于 2026-7-13 14:46
这个挺好的,谢谢分享。拿来试试
3#
xiaochanchan 发表于 2026-7-13 14:54
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-13 15:06

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表