[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XX学校新生智能分班系统 v2.0</title>
<style>
:root {
--primary-color: #4b86b4;
--primary-hover: #3a6a8a;
--bg-color: #f0f8ff;
--text-color: #2a4d69;
--error-color: #ff6b6b;
--female-color: #ff9999;
--male-color: #99ccff;
--border-color: #4b86b4;
--light-border: #e0e0e0;
--header-bg: #e6f2ff;
}
body {
font-family: "Microsoft YaHei", sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
}
.container {
display: flex;
min-height: 100vh;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: white;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.header-title {
font-size: 24px;
font-weight: bold;
color: var(--text-color);
}
.status-bar {
flex-grow: 1;
text-align: right;
padding-right: 20px;
}
/* Control Panel */
.control-panel {
width: 300px;
background-color: white;
padding: 15px;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
overflow-y: auto;
}
.panel-section {
margin-bottom: 15px;
border: 1px solid var(--light-border);
border-radius: 5px;
padding: 10px;
}
.panel-title {
font-weight: bold;
margin-bottom: 10px;
color: var(--text-color);
}
.btn {
display: block;
width: 100%;
padding: 8px;
margin: 5px 0;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: "Microsoft YaHei", sans-serif;
font-size: 14px;
transition: background-color 0.3s;
}
.btn:hover {
background-color: var(--primary-hover);
}
.btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.form-group {
margin-bottom: 10px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="number"], select {
width: 100%;
padding: 6px;
border: 1px solid var(--light-border);
border-radius: 4px;
}
.checkbox-group {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.checkbox-group input {
margin-right: 8px;
}
/* Main Content */
.main-content {
flex-grow: 1;
padding: 15px;
overflow: hidden;
}
/* Tabs */
.tabs {
display: flex;
border-bottom: 1px solid var(--light-border);
margin-bottom: 10px;
}
.tab {
padding: 10px 15px;
cursor: pointer;
border: 1px solid transparent;
border-bottom: none;
border-radius: 5px 5px 0 0;
margin-right: 5px;
font-weight: bold;
}
.tab.active {
border-color: var(--light-border);
border-bottom-color: white;
background-color: white;
}
.tab-content {
display: none;
background-color: white;
padding: 15px;
border-radius: 0 5px 5px 5px;
height: calc(100vh - 150px);
overflow: auto;
}
.tab-content.active {
display: block;
}
/* Data Table */
.data-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
}
.data-table th, .data-table td {
border: 1px solid var(--light-border);
padding: 8px;
text-align: center;
}
.data-table th {
background-color: var(--header-bg);
font-weight: bold;
}
.data-table tr.error {
background-color: var(--error-color);
}
/* Stats Table */
.stats-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
}
.stats-table th, .stats-table td {
border: 1px solid var(--light-border);
padding: 8px;
text-align: center;
}
.stats-table th {
background-color: var(--header-bg);
font-weight: bold;
}
/* Animation Canvas */
.animation-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: white;
z-index: 1000;
display: none;
flex-direction: column;
}
.animation-header {
padding: 10px;
background-color: var(--primary-color);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.animation-canvas {
flex-grow: 1;
background-color: white;
position: relative;
overflow: auto;
}
.class-area {
position: absolute;
border: 2px solid var(--border-color);
background-color: var(--header-bg);
border-radius: 5px;
}
.student-icon {
position: absolute;
width: 35px;
height: 30px;
border: 1px solid #666;
display: flex;
justify-content: center;
align-items: center;
font-size: 8px;
cursor: pointer;
}
.student-icon.female {
background-color: var(--female-color);
}
.student-icon.male {
background-color: var(--male-color);
}
/* Log */
.log-container {
height: 100%;
border: 1px solid var(--light-border);
padding: 10px;
overflow-y: auto;
font-family: monospace;
white-space: pre-wrap;
}
/* Stats Differences */
.diff-container {
display: flex;
flex-wrap: wrap;
margin-top: 15px;
}
.diff-item {
display: flex;
align-items: center;
margin-right: 20px;
margin-bottom: 10px;
}
.diff-label {
margin-right: 5px;
font-weight: bold;
}
.diff-value {
font-weight: bold;
}
.diff-value.good {
color: green;
}
.diff-value.bad {
color: red;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 2000;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 5px;
max-width: 500px;
width: 90%;
}
.modal-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
}
.modal-message {
margin-bottom: 20px;
}
.modal-buttons {
display: flex;
justify-content: flex-end;
}
.modal-btn {
padding: 8px 15px;
margin-left: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.modal-btn-primary {
background-color: var(--primary-color);
color: white;
}
.modal-btn-secondary {
background-color: #cccccc;
}
</style>
</head>
<body>
<div class="container">
<!-- Control Panel -->
<div class="control-panel">
<div class="panel-section">
<div class="panel-title">数据导入</div>
<button id="importBtn" class="btn">导入Excel数据</button>
<input type="file" id="fileInput" style="display: none;" accept=".xlsx,.xls">
</div>
<div class="panel-section">
<div class="panel-title">分班设置</div>
<div class="form-group">
<label for="classNum">班级数量:</label>
<input type="number" id="classNum" min="2" max="20" value="2">
</div>
<div class="form-group">
<label for="animationSpeed">动画速度:</label>
<input type="range" id="animationSpeed" min="0.1" max="2" step="0.1" value="1">
<span id="speedValue">1.0</span>
</div>
<div class="form-group">
<label>平衡因素:</label>
<div class="checkbox-group">
<input type="checkbox" id="genderBalance" checked>
<label for="genderBalance">性别均衡</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="scoreBalance" checked>
<label for="scoreBalance">成绩均衡</label>
</div>
</div>
</div>
<div class="panel-section">
<div class="panel-title">操作</div>
<button id="startBtn" class="btn">开始分班</button>
<button id="exportBtn" class="btn" disabled>导出结果</button>
<button id="statsBtn" class="btn" disabled>查看统计</button>
<button id="resetBtn" class="btn">重置系统</button>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<div class="tabs">
<div class="tab active" data-tab="data">学生数据</div>
<div class="tab" data-tab="stats">分班统计</div>
<div class="tab" data-tab="log">分班日志</div>
</div>
<!-- Data Tab -->
<div class="tab-content active" id="dataTab">
<table class="data-table">
<thead>
<tr>
<th>序号</th>
<th>姓名</th>
<th>性别</th>
<th>语文</th>
<th>数学</th>
<th>英语</th>
<th>科学</th>
<th>总分</th>
<th>同名或近音</th>
<th>多胞胎</th>
<th>多胞胎同班</th>
</tr>
</thead>
<tbody id="dataTableBody">
<!-- Data will be populated here -->
</tbody>
</table>
</div>
<!-- Stats Tab -->
<div class="tab-content" id="statsTab">
<table class="stats-table">
<thead>
<tr>
<th>班级</th>
<th>人数</th>
<th>男生</th>
<th>女生</th>
<th>语文均分</th>
<th>数学均分</th>
<th>英语均分</th>
<th>科学均分</th>
<th>总分均分</th>
</tr>
</thead>
<tbody id="statsTableBody">
<!-- Stats will be populated here -->
</tbody>
</table>
<div class="diff-container">
<div class="panel-title">各科最大分差</div>
<div class="diff-item">
<span class="diff-label">语文:</span>
<span class="diff-value" id="chineseDiff">0.00</span>
</div>
<div class="diff-item">
<span class="diff-label">数学:</span>
<span class="diff-value" id="mathDiff">0.00</span>
</div>
<div class="diff-item">
<span class="diff-label">英语:</span>
<span class="diff-value" id="englishDiff">0.00</span>
</div>
<div class="diff-item">
<span class="diff-label">科学:</span>
<span class="diff-value" id="scienceDiff">0.00</span>
</div>
<div class="diff-item">
<span class="diff-label">总分:</span>
<span class="diff-value" id="totalDiff">0.00</span>
</div>
</div>
</div>
<!-- Log Tab -->
<div class="tab-content" id="logTab">
<div class="log-container" id="logContent">
<!-- Log messages will be displayed here -->
</div>
</div>
</div>
</div>
<!-- Animation Window -->
<div class="animation-container" id="animationWindow">
<div class="animation-header">
<div>分班过程</div>
<button id="closeAnimationBtn">关闭</button>
</div>
<div class="animation-canvas" id="animationCanvas">
<!-- Animation elements will be added here -->
</div>
</div>
<!-- Modal Dialog -->
<div class="modal" id="modalDialog">
<div class="modal-content">
<div class="modal-title" id="modalTitle">提示</div>
<div class="modal-message" id="modalMessage"></div>
<div class="modal-buttons">
<button class="modal-btn modal-btn-secondary" id="modalCancelBtn">取消</button>
<button class="modal-btn modal-btn-primary" id="modalConfirmBtn">确定</button>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
// Global variables
let studentData = null;
let classes = [];
let classNum = 2;
let animationSpeed = 1.0;
const balanceThreshold = 0.05;
const maxIterations = 300;
let animationRunning = false;
let studentIcons = {};
let classSizes = [];
// DOM elements
const importBtn = document.getElementById('importBtn');
const fileInput = document.getElementById('fileInput');
const classNumInput = document.getElementById('classNum');
const animationSpeedInput = document.getElementById('animationSpeed');
const speedValue = document.getElementById('speedValue');
const genderBalanceCheckbox = document.getElementById('genderBalance');
const scoreBalanceCheckbox = document.getElementById('scoreBalance');
const startBtn = document.getElementById('startBtn');
const exportBtn = document.getElementById('exportBtn');
const statsBtn = document.getElementById('statsBtn');
const resetBtn = document.getElementById('resetBtn');
const dataTableBody = document.getElementById('dataTableBody');
const statsTableBody = document.getElementById('statsTableBody');
const logContent = document.getElementById('logContent');
const animationWindow = document.getElementById('animationWindow');
const animationCanvas = document.getElementById('animationCanvas');
const closeAnimationBtn = document.getElementById('closeAnimationBtn');
const modalDialog = document.getElementById('modalDialog');
const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage');
const modalCancelBtn = document.getElementById('modalCancelBtn');
const modalConfirmBtn = document.getElementById('modalConfirmBtn');
const tabs = document.querySelectorAll('.tab');
const tabContents = document.querySelectorAll('.tab-content');
// Tab switching
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(`${tab.dataset.tab}Tab`).classList.add('active');
});
});
// Event listeners
importBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileImport);
classNumInput.addEventListener('change', updateClassNum);
animationSpeedInput.addEventListener('input', updateAnimationSpeed);
startBtn.addEventListener('click', startAssignment);
exportBtn.addEventListener('click', exportResults);
statsBtn.addEventListener('click', showStatistics);
resetBtn.addEventListener('click', resetSystem);
closeAnimationBtn.addEventListener('click', closeAnimationWindow);
modalCancelBtn.addEventListener('click', () => hideModal());
modalConfirmBtn.addEventListener('click', confirmModalAction);
// Initialize
updateAnimationSpeed();
// Functions
function handleFileImport(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { defval: "" });
// 检查必要的基本列
const requiredBasicColumns = ["序号", "姓名", "性别", "语文", "数学", "英语", "科学", "总分"];
const missingBasicColumns = requiredBasicColumns.filter(col => !jsonData[0] || !(col in jsonData[0]));
if (missingBasicColumns.length > 0) {
showError(`数据文件中缺少以下必要列: ${missingBasicColumns.join(', ')}`);
return;
}
// 处理可选列(同名或近音、多胞胎、多胞胎同班)
const optionalColumns = ["同名或近音", "多胞胎", "多胞胎同班"];
// 处理数据
studentData = jsonData.map((row, index) => {
const student = {
id: index + 1,
serial: parseInt(row['序号']) || 0,
name: String(row['姓名'] || '').trim(),
gender: String(row['性别'] || '').trim(),
chinese: parseFloat(row['语文']) || 0,
math: parseFloat(row['数学']) || 0,
english: parseFloat(row['英语']) || 0,
science: parseFloat(row['科学']) || 0,
total: parseFloat(row['总分']) || 0
};
// 处理可选列
optionalColumns.forEach(col => {
if (col in row) {
student[col === "同名或近音" ? "sameName" :
col === "多胞胎" ? "twins" : "twinsSameClass"] =
row[col] !== undefined && row[col] !== null ? String(row[col]) : '';
} else {
student[col === "同名或近音" ? "sameName" :
col === "多胞胎" ? "twins" : "twinsSameClass"] = '';
}
});
return student;
});
updateDataTable();
logMessage(`导入数据: ${file.name}`);
logMessage(`学生总数: ${studentData.length}`);
updateStatus(`成功导入数据: ${studentData.length} 名学生`);
exportBtn.disabled = false;
statsBtn.disabled = false;
} catch (error) {
showError(`导入数据时出错: ${error.message}`);
}
};
reader.readAsArrayBuffer(file);
}
function updateDataTable() {
if (!studentData) return;
dataTableBody.innerHTML = '';
studentData.forEach(student => {
const row = document.createElement('tr');
if (student.error) row.classList.add('error');
row.innerHTML = `
<td>${student.serial}</td>
<td>${student.name}</td>
<td>${student.gender}</td>
<td>${student.chinese.toFixed(1)}</td>
<td>${student.math.toFixed(1)}</td>
<td>${student.english.toFixed(1)}</td>
<td>${student.science.toFixed(1)}</td>
<td>${student.total.toFixed(1)}</td>
<td>${student.sameName}</td>
<td>${student.twins}</td>
<td>${student.twinsSameClass}</td>
`;
dataTableBody.appendChild(row);
});
}
function updateClassNum() {
classNum = parseInt(classNumInput.value) || 2;
if (classNum < 2) classNum = 2;
if (classNum > 20) classNum = 20;
classNumInput.value = classNum;
}
function updateAnimationSpeed() {
animationSpeed = parseFloat(animationSpeedInput.value);
speedValue.textContent = animationSpeed.toFixed(1);
}
function logMessage(message) {
const line = document.createElement('div');
line.textContent = message;
logContent.appendChild(line);
logContent.scrollTop = logContent.scrollHeight;
}
function updateStatus(message) {
// In a real implementation, you would update a status bar element
console.log(`Status: ${message}`);
}
function showError(message) {
showModal('错误', message);
logMessage(`错误: ${message}`);
}
function showModal(title, message, confirmCallback = null) {
modalTitle.textContent = title;
modalMessage.textContent = message;
modalDialog.style.display = 'flex';
if (confirmCallback) {
currentConfirmCallback = confirmCallback;
modalCancelBtn.style.display = 'inline-block';
} else {
modalCancelBtn.style.display = 'none';
}
}
function hideModal() {
modalDialog.style.display = 'none';
}
let currentConfirmCallback = null;
function confirmModalAction() {
hideModal();
if (currentConfirmCallback) {
currentConfirmCallback();
currentConfirmCallback = null;
}
}
function startAssignment() {
if (!studentData || studentData.length === 0) {
showError('没有可用的学生数据');
return;
}
classNum = parseInt(classNumInput.value) || 2;
animationSpeed = parseFloat(animationSpeedInput.value);
// Disable buttons
[importBtn, startBtn, exportBtn, statsBtn, resetBtn].forEach(btn => {
btn.disabled = true;
});
// Clear classes
classes = Array.from({ length: classNum }, () => []);
// Start assignment in a timeout to allow UI to update
setTimeout(() => {
performAssignment();
}, 100);
}
function performAssignment() {
try {
logMessage('\n===== 开始分班处理 =====');
updateStatus('开始分班处理...');
// Clear log
logContent.innerHTML = '';
// Sort students by total score descending
const students = [...studentData].sort((a, b) => b.total - a.total);
// Calculate target class sizes
const totalStudents = students.length;
const baseSize = Math.floor(totalStudents / classNum);
const extra = totalStudents % classNum;
classSizes = Array.from({ length: classNum }, (_, i) => baseSize + (i < extra ? 1 : 0));
logMessage(`班级目标人数: ${classSizes.join(', ')}`);
// Show animation window
showAnimationWindow();
drawClassAreas();
// Initial assignment - snake pattern
let classIdx = 0;
let direction = 1;
const assignNextStudent = (index) => {
if (index >= students.length) {
// Initial assignment complete
logMessage('\n初始分班完成!');
calculateAndLogStats();
// Process constraints
handleNameAndTwinConstraints();
logMessage('\n处理同名或近音和多胞胎约束完成!');
calculateAndLogStats();
// Start optimization
logMessage('\n--- 优化分班 ---');
logMessage('开始优化分班结果...');
const optimized = optimizeAssignment();
if (optimized) {
logMessage('优化成功! 各科均分差异均小于0.05分');
} else {
logMessage('优化完成! 达到最大迭代次数');
}
// Update animation with final positions
updateAnimation();
// Show completion message
const canvas = document.getElementById('animationCanvas');
const completionMsg1 = document.createElement('div');
completionMsg1.textContent = '分班完成!';
completionMsg1.style.position = 'absolute';
completionMsg1.style.left = '50%';
completionMsg1.style.bottom = '60px';
completionMsg1.style.transform = 'translateX(-50%)';
completionMsg1.style.fontSize = '16px';
completionMsg1.style.fontWeight = 'bold';
completionMsg1.style.color = '#2a4d69';
canvas.appendChild(completionMsg1);
const completionMsg2 = document.createElement('div');
completionMsg2.textContent = '各科均分差异均小于0.05分';
completionMsg2.style.position = 'absolute';
completionMsg2.style.left = '50%';
completionMsg2.style.bottom = '30px';
completionMsg2.style.transform = 'translateX(-50%)';
completionMsg2.style.fontSize = '12px';
completionMsg2.style.color = '#2a4d69';
canvas.appendChild(completionMsg2);
// Enable buttons
[importBtn, startBtn, exportBtn, statsBtn, resetBtn].forEach(btn => {
btn.disabled = false;
});
updateStatus(`分班完成! 共分配 ${students.length} 名学生到 ${classNum} 个班级`);
logMessage('\n===== 分班完成! =====');
return;
}
const student = students[index];
updateStatus(`分配学生: ${student.name} (总分: ${student.total.toFixed(1)})`);
// Find next available class
while (classes[classIdx].length >= classSizes[classIdx]) {
classIdx += direction;
if (classIdx >= classNum) {
classIdx = classNum - 1;
direction = -1;
} else if (classIdx < 0) {
classIdx = 0;
direction = 1;
}
}
// Add to class
classes[classIdx].push(student);
// Create student icon
createStudentIcon(student, classIdx, classes[classIdx].length);
// Update class label
updateClassLabel(classIdx);
// Move to next student after delay
setTimeout(() => {
if (classIdx === 0 && direction === -1) {
direction = 1;
} else if (classIdx === classNum - 1 && direction === 1) {
direction = -1;
} else {
classIdx += direction;
}
assignNextStudent(index + 1);
}, 300 / animationSpeed);
};
// Start assignment
assignNextStudent(0);
} catch (error) {
logMessage(`错误: ${error.message}`);
showError(`分班过程中出错: ${error.message}`);
// Enable buttons
[importBtn, startBtn, exportBtn, statsBtn, resetBtn].forEach(btn => {
btn.disabled = false;
});
}
}
function showAnimationWindow() {
animationWindow.style.display = 'flex';
animationCanvas.innerHTML = '';
// Add total students label
const totalLabel = document.createElement('div');
totalLabel.textContent = `学生总数: ${studentData.length}`;
totalLabel.style.position = 'absolute';
totalLabel.style.top = '10px';
totalLabel.style.left = '10px';
totalLabel.style.fontFamily = '"Microsoft YaHei", sans-serif';
totalLabel.style.fontSize = '12px';
totalLabel.style.fontWeight = 'bold';
animationCanvas.appendChild(totalLabel);
}
function closeAnimationWindow() {
animationWindow.style.display = 'none';
}
function drawClassAreas() {
const canvas = document.getElementById('animationCanvas');
const canvasWidth = canvas.clientWidth;
const canvasHeight = canvas.clientHeight;
const marginX = 20;
const marginY = 50;
const availableWidth = Math.max(100, canvasWidth - 2 * marginX - (classNum - 1) * 15);
const classWidth = Math.max(100, availableWidth / classNum);
const classHeight = Math.max(100, canvasHeight - 2 * marginY - 100);
const classStartX = marginX;
const classStartY = marginY;
// Clear previous class areas
const existingAreas = document.querySelectorAll('.class-area');
existingAreas.forEach(area => area.remove());
// Create new class areas
for (let i = 0; i < classNum; i++) {
const classArea = document.createElement('div');
classArea.className = 'class-area';
classArea.style.left = `${classStartX + i * (classWidth + 15)}px`;
classArea.style.top = `${classStartY}px`;
classArea.style.width = `${classWidth}px`;
classArea.style.height = `${classHeight}px`;
// Add class label
const classLabel = document.createElement('div');
classLabel.textContent = `${i+1}班(0/${classSizes[i]})`;
classLabel.style.position = 'absolute';
classLabel.style.top = '20px';
classLabel.style.left = '50%';
classLabel.style.transform = 'translateX(-50%)';
classLabel.style.fontFamily = '"Microsoft YaHei", sans-serif';
classLabel.style.fontSize = '10px';
classLabel.style.fontWeight = 'bold';
classArea.appendChild(classLabel);
classArea.dataset.classIndex = i;
classArea.dataset.classLabel = classLabel;
canvas.appendChild(classArea);
}
}
function createStudentIcon(student, classIdx, studentIdxInClass) {
const canvas = document.getElementById('animationCanvas');
const classAreas = document.querySelectorAll('.class-area');
const classArea = classAreas[classIdx];
if (!classArea) return;
const classRect = classArea.getBoundingClientRect();
const canvasRect = canvas.getBoundingClientRect();
const classWidth = classRect.width;
const classHeight = classRect.height;
const classLeft = classRect.left - canvasRect.left;
const classTop = classRect.top - canvasRect.top;
const maxPerCol = Math.max(1, Math.floor(classHeight / 30));
const col = Math.floor((studentIdxInClass - 1) / maxPerCol);
const row = (studentIdxInClass - 1) % maxPerCol;
const x = classLeft + 15 + col * 40;
const y = classTop + 60 + row * 30;
const studentIcon = document.createElement('div');
studentIcon.className = `student-icon ${student.gender === '女' ? 'female' : 'male'}`;
studentIcon.style.left = `${x}px`;
studentIcon.style.top = `${y}px`;
const nameText = document.createElement('div');
nameText.textContent = student.name;
nameText.style.fontSize = '8px';
nameText.style.overflow = 'hidden';
nameText.style.textOverflow = 'ellipsis';
nameText.style.whiteSpace = 'nowrap';
studentIcon.appendChild(nameText);
canvas.appendChild(studentIcon);
// Store reference for later updates
if (!studentIcons[student.id]) {
studentIcons[student.id] = [];
}
studentIcons[student.id].push({
element: studentIcon,
classIdx: classIdx
});
}
function updateClassLabel(classIdx) {
const classAreas = document.querySelectorAll('.class-area');
const classArea = classAreas[classIdx];
if (classArea && classArea.dataset.classLabel) {
const classLabel = classArea.dataset.classLabel;
classLabel.textContent = `${classIdx+1}班(${classes[classIdx].length}/${classSizes[classIdx]})`;
}
}
function updateAnimation() {
// In a real implementation, this would update the animation
// to reflect any student movements during optimization
}
function handleNameAndTwinConstraints() {
// Process same name constraints
if (studentData.some(s => s.sameName)) {
logMessage('开始处理同名或近音学生约束 (基于 "同名或近音" 列数字)...');
// Group students by sameName value
const sameNameGroups = {};
studentData.forEach(student => {
if (student.sameName) {
if (!sameNameGroups[student.sameName]) {
sameNameGroups[student.sameName] = [];
}
sameNameGroups[student.sameName].push(student);
}
});
// Process each group
Object.entries(sameNameGroups).forEach(([groupKey, groupStudents]) => {
if (groupStudents.length < 2) {
logMessage(`同名或近音组 ${groupKey} 只有 ${groupStudents.length} 个成员,跳过。`);
return;
}
logMessage(`处理同名或近音组 ${groupKey}, 成员数: ${groupStudents.length}`);
// Find current class assignments
const memberInfo = [];
groupStudents.forEach(student => {
for (let classIdx = 0; classIdx < classes.length; classIdx++) {
const studentIdx = classes[classIdx].findIndex(s => s.id === student.id);
if (studentIdx !== -1) {
memberInfo.push({
student,
classIdx,
studentIdx
});
break;
}
}
});
if (memberInfo.length !== groupStudents.length) {
logMessage(`警告: 同名或近音组 ${groupKey} 成员定位不完整 (${memberInfo.length}/${groupStudents.length})`);
}
if (memberInfo.length < 2) {
logMessage(`信息: 同名或近音组 ${groupKey} 找到的有效成员少于2人,无需处理。`);
return;
}
// Check for students in same class
const classCounts = {};
memberInfo.forEach(info => {
classCounts[info.classIdx] = (classCounts[info.classIdx] || 0) + 1;
});
const classesWithMultiple = Object.entries(classCounts)
.filter(([_, count]) => count > 1)
.map(([classIdx]) => parseInt(classIdx));
if (classesWithMultiple.length === 0) {
logMessage(`该组同名或近音学生已满足"不同班"要求。`);
return;
}
// Move extra students from overfilled classes
classesWithMultiple.forEach(classIdx => {
const studentsInClass = memberInfo.filter(info => info.classIdx === classIdx);
const numToMove = studentsInClass.length - 1;
if (numToMove <= 0) return;
logMessage(`班级 ${classIdx+1} 有 ${studentsInClass.length} 个该组同名或近音成员,需移动 ${numToMove} 个。`);
// Select students to move (skip the first one)
const studentsToMove = studentsInClass.slice(1, numToMove + 1);
// Remove from current class
studentsToMove.forEach(info => {
classes[info.classIdx].splice(info.studentIdx, 1);
});
// Find available classes
const availableClasses = Array.from({ length: classNum }, (_, i) => i)
.filter(i => i !== classIdx);
if (availableClasses.length === 0) {
logMessage('警告: 只有一个班级,无法分开同名或近音学生。');
// Add back to original class
studentsToMove.forEach(info => {
classes[classIdx].push(info.student);
});
return;
}
// Distribute to other classes
studentsToMove.forEach((info, i) => {
const targetClass = availableClasses[i % availableClasses.length];
classes[targetClass].push(info.student);
logMessage(`将同名或近音学生 ${info.student.name} 从班级 ${classIdx+1} 移动到班级 ${targetClass+1}`);
});
});
});
logMessage('同名或近音学生约束处理完成。');
}
// Process twins constraints
if (studentData.some(s => s.twins)) {
logMessage('开始处理多胞胎约束...');
// Group students by twins value
const twinsGroups = {};
studentData.forEach(student => {
if (student.twins) {
if (!twinsGroups[student.twins]) {
twinsGroups[student.twins] = [];
}
twinsGroups[student.twins].push(student);
}
});
// Process each group
Object.entries(twinsGroups).forEach(([groupKey, groupStudents]) => {
if (groupStudents.length < 2) {
logMessage(`多胞胎组 ${groupKey} 只有 ${groupStudents.length} 个成员,跳过。`);
return;
}
logMessage(`处理多胞胎组 ${groupKey}, 成员数: ${groupStudents.length}`);
// Determine constraint (default is separate classes)
let sameClass = false;
const firstStudent = groupStudents[0];
if (firstStudent.twinsSameClass &&
['是', 'yes', 'true', '1'].includes(firstStudent.twinsSameClass.toLowerCase())) {
sameClass = true;
}
logMessage(`组 ${groupKey} 约束: ${sameClass ? '同班' : '不同班'}`);
// Find current class assignments
const memberInfo = [];
groupStudents.forEach(student => {
for (let classIdx = 0; classIdx < classes.length; classIdx++) {
const studentIdx = classes[classIdx].findIndex(s => s.id === student.id);
if (studentIdx !== -1) {
memberInfo.push({
student,
classIdx,
studentIdx
});
break;
}
}
});
if (memberInfo.length !== groupStudents.length) {
logMessage(`警告: 多胞胎组 ${groupKey} 成员定位不完整 (${memberInfo.length}/${groupStudents.length})`);
}
if (memberInfo.length < 2) {
logMessage(`信息: 多胞胎组 ${groupKey} 找到的有效成员少于2人,无需处理。`);
return;
}
if (sameClass) {
// All should be in same class
logMessage('执行 "同班" 约束...');
// Find the class with most members
const classCounts = {};
memberInfo.forEach(info => {
classCounts[info.classIdx] = (classCounts[info.classIdx] || 0) + 1;
});
const targetClass = parseInt(Object.entries(classCounts)
.sort((a, b) => b[1] - a[1])[0][0]);
logMessage(`目标班级: ${targetClass+1} (当前有 ${classCounts[targetClass]} 名该组成员)`);
// Check if all are already in target class
if (memberInfo.every(info => info.classIdx === targetClass)) {
logMessage('所有成员已在此班级,无需移动。');
return;
}
// Move all to target class
const studentsToMove = memberInfo.filter(info => info.classIdx !== targetClass)
.map(info => info.student);
// Remove from current classes
memberInfo.forEach(info => {
if (info.classIdx !== targetClass) {
classes[info.classIdx].splice(info.studentIdx, 1);
}
});
// Add to target class
studentsToMove.forEach(student => {
classes[targetClass].push(student);
});
const names = studentsToMove.map(s => s.name).join(', ');
logMessage(`已将多胞胎 ${names} 移动到班级 ${targetClass + 1}`);
} else {
// All should be in different classes
logMessage('执行 "不同班" 约束...');
// Check for students in same class
const classCounts = {};
memberInfo.forEach(info => {
classCounts[info.classIdx] = (classCounts[info.classIdx] || 0) + 1;
});
const classesWithMultiple = Object.entries(classCounts)
.filter(([_, count]) => count > 1)
.map(([classIdx]) => parseInt(classIdx));
if (classesWithMultiple.length === 0) {
logMessage('该组多胞胎已满足"不同班"要求。');
return;
}
// Move extra students from overfilled classes
classesWithMultiple.forEach(classIdx => {
const studentsInClass = memberInfo.filter(info => info.classIdx === classIdx);
const numToMove = studentsInClass.length - 1;
if (numToMove <= 0) return;
logMessage(`班级 ${classIdx+1} 有 ${studentsInClass.length} 个该组成员,需移动 ${numToMove} 个。`);
// Select students to move (skip the first one)
const studentsToMove = studentsInClass.slice(1, numToMove + 1);
// Remove from current class
studentsToMove.forEach(info => {
classes[info.classIdx].splice(info.studentIdx, 1);
});
// Find available classes
const availableClasses = Array.from({ length: classNum }, (_, i) => i)
.filter(i => i !== classIdx);
if (availableClasses.length === 0) {
logMessage('警告: 无可分配的其他班级,部分多胞胎成员可能仍在一起。');
// Add back to original class
studentsToMove.forEach(info => {
classes[classIdx].push(info.student);
});
return;
}
// Distribute to other classes
studentsToMove.forEach((info, i) => {
const targetClass = availableClasses[i % availableClasses.length];
classes[targetClass].push(info.student);
logMessage(`将多胞胎 ${info.student.name} 从班级 ${classIdx+1} 移动到班级 ${targetClass+1}`);
});
});
}
});
logMessage('多胞胎约束处理完成。');
}
}
function optimizeAssignment() {
let optimized = false;
let iteration = 0;
let noImprovementCount = 0;
const maxNoImprovement = 15;
// Ensure class sizes are correct
enforceClassSizes();
// Calculate initial stats
const classStats = calculateClassStats();
// Track subject priorities
const subjectPriorities = {
chinese: 1.0,
math: 1.0,
english: 1.0,
science: 1.0,
total: 1.0
};
while (iteration < maxIterations) {
iteration++;
let improved = false;
// Update priorities based on current differences
const subjects = ['chinese', 'math', 'english', 'science', 'total'];
subjects.forEach(sub => {
if (classStats[sub].length > 0) {
const currentDiff = Math.max(...classStats[sub]) - Math.min(...classStats[sub]);
subjectPriorities[sub] = Math.max(1.0, currentDiff * 2);
}
});
// Try student swaps
for (let i = 0; i < classNum; i++) {
for (let j = i + 1; j < classNum; j++) {
// Focus on subject with highest priority
const focusSubject = Object.keys(subjectPriorities)
.reduce((a, b) => subjectPriorities[a] > subjectPriorities[b] ? a : b);
if (trySwapStudents(i, j, classStats, focusSubject)) {
improved = true;
updateClassStats(classStats, i);
updateClassStats(classStats, j);
break;
}
}
if (improved) break;
}
// Check if balanced
if (checkScoreBalanceCondition(classStats)) {
optimized = true;
logMessage(`迭代 ${iteration}: 满足成绩平衡条件 (差异 < ${balanceThreshold}分),优化完成!`);
break;
}
// Early termination check
if (!improved) {
noImprovementCount++;
if (noImprovementCount >= maxNoImprovement) {
logMessage(`连续${maxNoImprovement}次无改进,提前终止迭代`);
break;
}
} else {
noImprovementCount = 0;
}
// Log progress every 50 iterations
if (iteration % 50 === 0 || optimized) {
logMessage(`迭代 ${iteration}: 继续优化...`);
calculateAndLogStats();
}
}
return optimized;
}
function enforceClassSizes() {
const totalStudents = classes.reduce((sum, cls) => sum + cls.length, 0);
const baseSize = Math.floor(totalStudents / classNum);
const extra = totalStudents % classNum;
const targetSizes = Array.from({ length: classNum }, (_, i) => baseSize + (i < extra ? 1 : 0));
for (let classIdx = 0; classIdx < classNum; classIdx++) {
const currentSize = classes[classIdx].length;
const targetSize = targetSizes[classIdx];
if (currentSize > targetSize) {
logMessage(`班级 ${classIdx+1} 人数过多 (${currentSize} > ${targetSize}), 调整中...`);
let attempts = 0;
const maxAttempts = currentSize - targetSize + 5;
while (classes[classIdx].length > targetSize && attempts < maxAttempts) {
attempts++;
if (classes[classIdx].length === 0) {
logMessage(`警告: 尝试从空班级 ${classIdx+1} 移除学生`);
break;
}
const classStudents = classes[classIdx];
const meanScore = classStudents.reduce((sum, s) => sum + s.total, 0) / classStudents.length;
// Find student with score closest to mean
let closestIdx = 0;
let minDiff = Math.abs(classStudents[0].total - meanScore);
for (let i = 1; i < classStudents.length; i++) {
const diff = Math.abs(classStudents[i].total - meanScore);
if (diff < minDiff) {
minDiff = diff;
closestIdx = i;
}
}
// Remove student
const student = classes[classIdx].splice(closestIdx, 1)[0];
let studentAdded = false;
// Try to add to another class with space
for (let otherIdx = 0; otherIdx < classNum; otherIdx++) {
if (otherIdx !== classIdx && classes[otherIdx].length < targetSizes[otherIdx]) {
classes[otherIdx].push(student);
logMessage(`将 ${student.name} 从班级 ${classIdx+1} 移动到班级 ${otherIdx+1}`);
studentAdded = true;
break;
}
}
if (!studentAdded) {
// Add to smallest class
const smallestClass = classes.reduce((minIdx, cls, idx) =>
cls.length < classes[minIdx].length ? idx : minIdx, 0);
classes[smallestClass].push(student);
logMessage(`将 ${student.name} 从班级 ${classIdx+1} 移动到班级 ${smallestClass+1} (备用方案)`);
}
}
} else if (currentSize < targetSize) {
logMessage(`班级 ${classIdx+1} 人数不足 (${currentSize} < ${targetSize}), 调整中...`);
let attempts = 0;
const maxAttempts = targetSize - currentSize + 5;
while (classes[classIdx].length < targetSize && attempts < maxAttempts) {
attempts++;
let studentMoved = false;
// Try to find a student from another class to move
for (let otherIdx = 0; otherIdx < classNum; otherIdx++) {
if (otherIdx !== classIdx && classes[otherIdx].length > targetSizes[otherIdx] &&
classes[otherIdx].length > 0) {
const otherStudents = classes[otherIdx];
const otherMean = otherStudents.reduce((sum, s) => sum + s.total, 0) / otherStudents.length;
// Find student with score closest to mean
let closestIdx = 0;
let minDiff = Math.abs(otherStudents[0].total - otherMean);
for (let i = 1; i < otherStudents.length; i++) {
const diff = Math.abs(otherStudents[i].total - otherMean);
if (diff < minDiff) {
minDiff = diff;
closestIdx = i;
}
}
// Move student
const student = classes[otherIdx].splice(closestIdx, 1)[0];
classes[classIdx].push(student);
logMessage(`将 ${student.name} 从班级 ${otherIdx+1} 移动到班级 ${classIdx+1}`);
studentMoved = true;
break;
}
}
if (!studentMoved) {
logMessage(`警告: 无法为班级 ${classIdx+1} 找到更多学生进行补充`);
break;
}
}
}
}
}
function trySwapStudents(classA, classB, classStats, focusSubject) {
if (classA >= classes.length || classB >= classes.length) {
return false;
}
const classASize = classes[classA].length;
const classBSize = classes[classB].length;
// Determine how many candidates to consider
const sampleSizeA = Math.max(2, Math.min(4, Math.floor(classASize / 10)));
const sampleSizeB = Math.max(2, Math.min(4, Math.floor(classBSize / 10)));
// Sort classes by total score
const sortedClassA = [...classes[classA]].sort((a, b) => b.total - a.total);
const sortedClassB = [...classes[classB]].sort((a, b) => b.total - a.total);
// Select candidates - top and bottom performers
const candidatesA = [
...sortedClassA.slice(0, sampleSizeA),
...sortedClassA.slice(-sampleSizeA)
];
const candidatesB = [
...sortedClassB.slice(0, sampleSizeB),
...sortedClassB.slice(-sampleSizeB)
];
let bestImprovement = -1;
let bestPair = null;
// Get current stats for quick calculation
const statsA = {
chinese: classStats.chinese[classA],
math: classStats.math[classA],
english: classStats.english[classA],
science: classStats.science[classA],
total: classStats.total[classA],
male: classStats.male[classA],
female: classStats.female[classA],
count: classStats.count[classA]
};
const statsB = {
chinese: classStats.chinese[classB],
math: classStats.math[classB],
english: classStats.english[classB],
science: classStats.science[classB],
total: classStats.total[classB],
male: classStats.male[classB],
female: classStats.female[classB],
count: classStats.count[classB]
};
// Evaluate possible swaps
for (const studentA of candidatesA) {
for (const studentB of candidatesB) {
// Calculate temporary stats after swap
const tempStatsA = calculateTempStats(statsA, studentA, studentB);
const tempStatsB = calculateTempStats(statsB, studentB, studentA);
// Calculate improvement
const improvement = calculateImprovement(
classStats,
tempStatsA, tempStatsB,
classA, classB,
focusSubject
);
// Track best swap
if (improvement > bestImprovement) {
bestImprovement = improvement;
bestPair = { studentA, studentB };
}
}
}
// Execute best swap if improvement is significant
if (bestImprovement > 0.01 && bestPair) {
const { studentA, studentB } = bestPair;
// Find indices
const indexA = classes[classA].findIndex(s => s.id === studentA.id);
const indexB = classes[classB].findIndex(s => s.id === studentB.id);
if (indexA !== -1 && indexB !== -1) {
// Perform swap
classes[classA][indexA] = studentB;
classes[classB][indexB] = studentA;
logMessage(`交换学生: ${studentA.name}(班${classA+1}) <-> ${studentB.name}(班${classB+1}), 改善: ${bestImprovement.toFixed(4)}`);
return true;
}
}
return false;
}
function calculateTempStats(stats, outStudent, inStudent) {
const n = stats.count;
if (n === 0) return stats;
const newStats = { ...stats };
// Update scores
newStats.chinese = ((stats.chinese * n - outStudent.chinese + inStudent.chinese) / n);
newStats.math = ((stats.math * n - outStudent.math + inStudent.math) / n);
newStats.english = ((stats.english * n - outStudent.english + inStudent.english) / n);
newStats.science = ((stats.science * n - outStudent.science + inStudent.science) / n);
newStats.total = ((stats.total * n - outStudent.total + inStudent.total) / n);
// Update gender counts
if (outStudent.gender === '男') newStats.male--;
if (outStudent.gender === '女') newStats.female--;
if (inStudent.gender === '男') newStats.male++;
if (inStudent.gender === '女') newStats.female++;
return newStats;
}
function calculateImprovement(classStats, tempStatsA, tempStatsB, classA, classB, focusSubject) {
let improvement = 0;
const subjects = ['chinese', 'math', 'english', 'science', 'total'];
// Weights - higher for focus subject
const weights = {
chinese: focusSubject === 'chinese' ? 1.5 : 1.0,
math: focusSubject === 'math' ? 1.5 : 1.0,
english: focusSubject === 'english' ? 1.5 : 1.0,
science: focusSubject === 'science' ? 1.5 : 1.0,
total: focusSubject === 'total' ? 1.5 : 1.0
};
// Calculate improvement for each subject
subjects.forEach(sub => {
if (!classStats[sub] || classStats[sub].length === 0) return;
// Current max and min
const currentMax = Math.max(...classStats[sub]);
const currentMin = Math.min(...classStats[sub]);
const currentDiff = currentMax - currentMin;
// New max and min (only updating the two classes involved)
const newValues = [...classStats[sub]];
newValues[classA] = tempStatsA[sub];
newValues[classB] = tempStatsB[sub];
const newMax = Math.max(...newValues);
const newMin = Math.min(...newValues);
const newDiff = newMax - newMin;
// Improvement is reduction in difference
improvement += (currentDiff - newDiff) * weights[sub];
});
return improvement;
}
function checkScoreBalanceCondition(classStats) {
const subjects = ['chinese', 'math', 'english', 'science', 'total'];
for (const sub of subjects) {
if (!classStats[sub] || classStats[sub].length === 0) continue;
const maxVal = Math.max(...classStats[sub]);
const minVal = Math.min(...classStats[sub]);
if (maxVal - minVal > balanceThreshold) {
return false;
}
}
return true;
}
function calculateClassStats() {
const stats = {
class: [],
count: [],
male: [],
female: [],
chinese: [],
math: [],
english: [],
science: [],
total: []
};
for (let i = 0; i < classes.length; i++) {
const classStudents = classes[i];
stats.class.push(`班级 ${i+1}`);
stats.count.push(classStudents.length);
// Gender counts
const maleCount = classStudents.filter(s => s.gender === '男').length;
const femaleCount = classStudents.filter(s => s.gender === '女').length;
stats.male.push(maleCount);
stats.female.push(femaleCount);
// Subject averages
const chineseAvg = classStudents.reduce((sum, s) => sum + s.chinese, 0) / classStudents.length || 0;
const mathAvg = classStudents.reduce((sum, s) => sum + s.math, 0) / classStudents.length || 0;
const englishAvg = classStudents.reduce((sum, s) => sum + s.english, 0) / classStudents.length || 0;
const scienceAvg = classStudents.reduce((sum, s) => sum + s.science, 0) / classStudents.length || 0;
const totalAvg = classStudents.reduce((sum, s) => sum + s.total, 0) / classStudents.length || 0;
stats.chinese.push(parseFloat(chineseAvg.toFixed(1)));
stats.math.push(parseFloat(mathAvg.toFixed(1)));
stats.english.push(parseFloat(englishAvg.toFixed(1)));
stats.science.push(parseFloat(scienceAvg.toFixed(1)));
stats.total.push(parseFloat(totalAvg.toFixed(1)));
}
return stats;
}
function updateClassStats(classStats, classIdx) {
if (classIdx >= classes.length || classes[classIdx].length === 0) {
classStats.count[classIdx] = 0;
classStats.male[classIdx] = 0;
classStats.female[classIdx] = 0;
classStats.chinese[classIdx] = 0;
classStats.math[classIdx] = 0;
classStats.english[classIdx] = 0;
classStats.science[classIdx] = 0;
classStats.total[classIdx] = 0;
return;
}
const classStudents = classes[classIdx];
classStats.count[classIdx] = classStudents.length;
// Gender counts
const maleCount = classStudents.filter(s => s.gender === '男').length;
const femaleCount = classStudents.filter(s => s.gender === '女').length;
classStats.male[classIdx] = maleCount;
classStats.female[classIdx] = femaleCount;
// Subject averages
const chineseAvg = classStudents.reduce((sum, s) => sum + s.chinese, 0) / classStudents.length || 0;
const mathAvg = classStudents.reduce((sum, s) => sum + s.math, 0) / classStudents.length || 0;
const englishAvg = classStudents.reduce((sum, s) => sum + s.english, 0) / classStudents.length || 0;
const scienceAvg = classStudents.reduce((sum, s) => sum + s.science, 0) / classStudents.length || 0;
const totalAvg = classStudents.reduce((sum, s) => sum + s.total, 0) / classStudents.length || 0;
classStats.chinese[classIdx] = parseFloat(chineseAvg.toFixed(1));
classStats.math[classIdx] = parseFloat(mathAvg.toFixed(1));
classStats.english[classIdx] = parseFloat(englishAvg.toFixed(1));
classStats.science[classIdx] = parseFloat(scienceAvg.toFixed(1));
classStats.total[classIdx] = parseFloat(totalAvg.toFixed(1));
}
function calculateAndLogStats() {
const stats = calculateClassStats();
logMessage('\n班级统计:');
// Determine column widths
const headers = ['班级', '人数', '男生', '女生', '语文均分', '数学均分', '英语均分', '科学均分', '总分均分'];
const colWidths = headers.map(h => h.length);
// Update column widths based on data
for (let i = 0; i < stats.class.length; i++) {
colWidths[0] = Math.max(colWidths[0], stats.class[i].length);
colWidths[1] = Math.max(colWidths[1], stats.count[i].toString().length);
colWidths[2] = Math.max(colWidths[2], stats.male[i].toString().length);
colWidths[3] = Math.max(colWidths[3], stats.female[i].toString().length);
colWidths[4] = Math.max(colWidths[4], stats.chinese[i].toFixed(1).length);
colWidths[5] = Math.max(colWidths[5], stats.math[i].toFixed(1).length);
colWidths[6] = Math.max(colWidths[6], stats.english[i].toFixed(1).length);
colWidths[7] = Math.max(colWidths[7], stats.science[i].toFixed(1).length);
colWidths[8] = Math.max(colWidths[8], stats.total[i].toFixed(1).length);
}
// Create header row
let headerRow = '';
headers.forEach((header, i) => {
headerRow += header.padEnd(colWidths[i] + 2);
});
logMessage(headerRow);
logMessage('-'.repeat(headerRow.length));
// Create data rows
for (let i = 0; i < stats.class.length; i++) {
let row = '';
row += stats.class[i].padEnd(colWidths[0] + 2);
row += stats.count[i].toString().padEnd(colWidths[1] + 2);
row += stats.male[i].toString().padEnd(colWidths[2] + 2);
row += stats.female[i].toString().padEnd(colWidths[3] + 2);
row += stats.chinese[i].toFixed(1).padEnd(colWidths[4] + 2);
row += stats.math[i].toFixed(1).padEnd(colWidths[5] + 2);
row += stats.english[i].toFixed(1).padEnd(colWidths[6] + 2);
row += stats.science[i].toFixed(1).padEnd(colWidths[7] + 2);
row += stats.total[i].toFixed(1).padEnd(colWidths[8] + 2);
logMessage(row);
}
// Calculate max differences
const chineseDiff = Math.max(...stats.chinese) - Math.min(...stats.chinese);
const mathDiff = Math.max(...stats.math) - Math.min(...stats.math);
const englishDiff = Math.max(...stats.english) - Math.min(...stats.english);
const scienceDiff = Math.max(...stats.science) - Math.min(...stats.science);
const totalDiff = Math.max(...stats.total) - Math.min(...stats.total);
logMessage('\n各科最大差异:');
logMessage(`语文: ${chineseDiff.toFixed(2)}`);
logMessage(`数学: ${mathDiff.toFixed(2)}`);
logMessage(`英语: ${englishDiff.toFixed(2)}`);
logMessage(`科学: ${scienceDiff.toFixed(2)}`);
logMessage(`总分: ${totalDiff.toFixed(2)}`);
return stats;
}
function showStatistics() {
if (!classes || classes.length === 0 || classes.every(c => c.length === 0)) {
showError('请先完成分班');
return;
}
// Switch to stats tab
tabs.forEach(t => t.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
document.querySelector('.tab[data-tab="stats"]').classList.add('active');
document.getElementById('statsTab').classList.add('active');
// Update stats table
const stats = calculateClassStats();
statsTableBody.innerHTML = '';
for (let i = 0; i < stats.class.length; i++) {
const row = document.createElement('tr');
row.innerHTML = `
<td>${stats.class[i]}</td>
<td>${stats.count[i]}</td>
<td>${stats.male[i]}</td>
<td>${stats.female[i]}</td>
<td>${stats.chinese[i].toFixed(1)}</td>
<td>${stats.math[i].toFixed(1)}</td>
<td>${stats.english[i].toFixed(1)}</td>
<td>${stats.science[i].toFixed(1)}</td>
<td>${stats.total[i].toFixed(1)}</td>
`;
statsTableBody.appendChild(row);
}
// Update max differences
const chineseDiff = Math.max(...stats.chinese) - Math.min(...stats.chinese);
const mathDiff = Math.max(...stats.math) - Math.min(...stats.math);
const englishDiff = Math.max(...stats.english) - Math.min(...stats.english);
const scienceDiff = Math.max(...stats.science) - Math.min(...stats.science);
const totalDiff = Math.max(...stats.total) - Math.min(...stats.total);
document.getElementById('chineseDiff').textContent = chineseDiff.toFixed(2);
document.getElementById('mathDiff').textContent = mathDiff.toFixed(2);
document.getElementById('englishDiff').textContent = englishDiff.toFixed(2);
document.getElementById('scienceDiff').textContent = scienceDiff.toFixed(2);
document.getElementById('totalDiff').textContent = totalDiff.toFixed(2);
// Color code based on threshold
const setDiffClass = (element, diff) => {
element.className = diff <= balanceThreshold ? 'diff-value good' : 'diff-value bad';
};
setDiffClass(document.getElementById('chineseDiff'), chineseDiff);
setDiffClass(document.getElementById('mathDiff'), mathDiff);
setDiffClass(document.getElementById('englishDiff'), englishDiff);
setDiffClass(document.getElementById('scienceDiff'), scienceDiff);
setDiffClass(document.getElementById('totalDiff'), totalDiff);
}
function exportResults() {
// 检查XLSX库是否可用
if (typeof XLSX === 'undefined') {
showError('Excel导出功能需要XLSX库,请确保网络连接正常');
return;
}
if (!classes || classes.length === 0 || classes.every(c => c.length === 0)) {
showError('请先完成分班');
return;
}
try {
// Create a workbook
const wb = XLSX.utils.book_new();
// 1. Create "全校" sheet with all students
const allStudents = [];
classes.forEach((classStudents, classIdx) => {
classStudents.forEach(student => {
const studentWithClass = {
'序号': student.serial,
'姓名': student.name,
'性别': student.gender,
'语文': student.chinese,
'数学': student.math,
'英语': student.english,
'科学': student.science,
'总分': student.total,
'同名或近音': student.sameName,
'多胞胎': student.twins,
'多胞胎同班': student.twinsSameClass,
'班级': `${classIdx + 1}班`
};
allStudents.push(studentWithClass);
});
});
if (allStudents.length > 0) {
const allStudentsWS = XLSX.utils.json_to_sheet(allStudents);
XLSX.utils.book_append_sheet(wb, allStudentsWS, "全校");
}
// 2. Create sheets for each class
classes.forEach((classStudents, classIdx) => {
if (classStudents.length > 0) {
const classStudentsWithCNHeaders = classStudents.map(student => ({
'序号': student.serial,
'姓名': student.name,
'性别': student.gender,
'语文': student.chinese,
'数学': student.math,
'英语': student.english,
'科学': student.science,
'总分': student.total,
'同名或近音': student.sameName,
'多胞胎': student.twins,
'多胞胎同班': student.twinsSameClass
}));
const classWS = XLSX.utils.json_to_sheet(classStudentsWithCNHeaders);
XLSX.utils.book_append_sheet(wb, classWS, `${classIdx + 1}班`);
}
});
// 3. Create stats sheet
const stats = calculateClassStats();
const statsData = [];
for (let i = 0; i < stats.class.length; i++) {
statsData.push({
'班级': stats.class[i],
'人数': stats.count[i],
'男生': stats.male[i],
'女生': stats.female[i],
'语文均分': stats.chinese[i],
'数学均分': stats.math[i],
'英语均分': stats.english[i],
'科学均分': stats.science[i],
'总分均分': stats.total[i]
});
}
if (statsData.length > 0) {
const statsWS = XLSX.utils.json_to_sheet(statsData);
XLSX.utils.book_append_sheet(wb, statsWS, "分班统计");
}
// Generate file and trigger download
const fileName = prompt("请输入要保存的文件名(不带扩展名):", "分班结果");
if (fileName) {
XLSX.writeFile(wb, `${fileName}.xlsx`);
logMessage(`分班结果已导出至: ${fileName}.xlsx`);
}
} catch (error) {
showError(`导出Excel时出错: ${error.message}`);
}
}
function resetSystem() {
showModal('确认', '确定要重置系统吗?当前分班结果将丢失。', () => {
studentData = null;
classes = [];
studentIcons = {};
classSizes = [];
dataTableBody.innerHTML = '';
statsTableBody.innerHTML = '';
logContent.innerHTML = '';
exportBtn.disabled = true;
statsBtn.disabled = true;
updateStatus('就绪');
});
}
</script>
</body>
</html>