[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>智能监考抽签系统</title>
<style>
* {box-sizing: border-box;}
body {
font-family: "微软雅黑", Arial, sans-serif;
margin: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
min-height: 100vh;
}
.container {
width: 95%;
max-width: 1200px;
margin: 20px auto;
padding: 30px;
background: white;
border-radius: 15px;
box-shadow: 0px 8px 30px rgba(0,0,0,0.12);
}
#eventTitle {
width:100%;
padding:15px;
margin-bottom:15px;
border-radius:8px;
border:1px solid #ccc;
outline: none;
font-size:1.5em;
font-weight: 100;
color: #2c3e50;
text-align: center;
transition: all 0.3s;
}
h1 {
text-align: center;
}
.input-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 25px;
margin-bottom: 35px;
}
.file-upload {
padding: 25px 15px;
border-radius: 15px;
text-align: center;
background: #fff;
transition: all 0.3s;
position: relative;
overflow: hidden;
box-shadow: 0px 0px 15px rgba(0,0,0,0.2);
}
.file-upload:hover {
background: #fff9f2;
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.3);
}
.file-upload h3 {
margin-top: 0;
color: #2c3e50;
font-size: 1.3em;
}
.example {
font-size: 0.85em;
color: #7f8c8d;
margin-top: 12px;
display: block;
line-height: 1.5;
}
input[type="file"] {
margin: 15px 0;
width: 100%;
padding: 10px;
border-radius: 6px;
border: 1px solid #ddd;
background: white;
}
button {
background: #3498db;
color: white;
padding: 16px 35px;
border: none;
border-radius: 50px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
transition: all 0.2s;
display: block;
margin: 30px auto;
letter-spacing: 1px;
}
button:hover {
background: #2a5caa;
transform: translateY(-2px);
}
button:active {
transform: translateY(1px);
}
#resultTable {
width: 100%;
border-collapse: collapse;
margin-top: 25px;
border-radius: 10px;
overflow: hidden;
}
#resultTable th, #resultTable td {
border: 1px solid #e1e4e8;
padding: 6px;
text-align: center;
}
#resultTable th {
background: linear-gradient(180deg, #3498db, #2980b9);
color: white;
font-weight: bold;
}
#resultTable tr:nth-child(even) {
background: #f8f9fa;
}
#resultTable tr:hover {
background-color: #e3f2fd;
}
#toExcel {
text-decoration: none;
text-align: center;
background: #1d953f;
border: none;
color: #fff;
font-size: 16px;
font-weight: 100;
padding: 10px 20px;
padding: 16px 35px;
border-radius: 50px;
cursor: pointer;
transition: all 0.2s;
display: block;
margin: 30px auto;
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
letter-spacing: 1px;
}
#toExcel:hover {
background: #007947;
}
.warning {
text-align: left;
color: #e67e22;
margin: 15px 0;
padding: 15px;
background: #fdf2e9;
border-radius: 8px;
font-size: 0.95em;
}
.status-bar {
text-align: center;
padding: 12px 20px;
color: #7f8c8d;
background: #f8f9fa;
border-radius: 8px;
margin: 20px 0;
font-size: 0.95em;
}
.file-info {
font-size: 0.9em;
color: #27ae60;
margin-top: 8px;
font-weight: bold;
}
.instructions {
background: #f6f5ec;font-weight:900;
padding: 0px;width:600px;
border:none;
border-radius:15px;
box-shadow:0px 0px 20px rgba(0,0,0,0.5);
}
.instructions h2 {
text-align:center;padding:0;margin:0;line-height:3em;over-flow:hidden;
background-color: #ff6600;color:#fff;
}
.instructions ul {
padding: 0 30px;
}
.instructions li {
list-style: none;
line-height: 2;
font-weight:100;font-size:0.8em;
}
.instructions h3 {
font-weight:900;font-size:1.5em;padding:0;margin:10px 0;
}
.footer {
text-align: center;
margin-top: 30px;
color: #7f8c8d;
font-size: 0.9em;
}
.stats-box {
background: #e8f4fc;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
display: flex;
justify-content: space-around;
flex-wrap: wrap;
}
.stat-item {
text-align: center;
padding: 10px;
}
.stat-value {
font-size: 1.5em;
font-weight: bold;
color: #3498db;
}
.stat-label {
font-size: 0.9em;
color: #7f8c8d;
}
.popover {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 800px;
max-height: 80%;
overflow-y: auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 50px rgba(0,0,0,0.3);
z-index: 1000;
padding: 30px;
}
.popover.active {
display: block;
}
.popover-content {
position: relative;
}
.popover-content h2 {
margin-top: 0;
color: #2c3e50;
}
.popover-content pre {
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
overflow-x: auto;
font-family: monospace;
}
.popover-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 999;
}
.popover-overlay.active {
display: block;
}
[url=home.php?mod=space&uid=945662]@media[/url] (max-width: 768px) {
.input-section {
grid-template-columns: 1fr;
}
.container {
padding: 20px 15px;
}
h1 {
font-size: 1.8em;
}
.stats-box {
flex-direction: column;
}
.popover {
width: 95%;
padding: 20px;
}
}
</style>
<script src="js/xlsx.full.min.js"></script>
</head>
<body>
<div class="container" style="position: relative;">
<!-- 帮助按钮 -->
<button popovertarget="help" title="帮助" style="position:absolute;top:0px;right:30px;width:40px;height:40px;padding:0;border-radius:50%;background-color:#ef5b9c;font-weight:900;">?</button>
<div class="instructions" id="help" popover>
<h2>使用说明</h2>
<ul>
<li><h3>考试名称</h3>首先,请输入需要安排的考试名称。</li><hr>
<li><h3>考场信息表.xlsx</h3>不得修改表格格式!第一行为表头(包括:考场编号 | 考场地址 | 主监考人数 | 副监考人数)。</li><hr>
<li><h3>主监考名单.txt、副监考名单.txt</h3>每行一个监考姓名。如不需要副监考,请将“考场信息表.xlsx”的“副监考”栏的人数填写“0”。</li><hr>
<li><h3>点击 <span style="border:1px solid #000;border-radius:20px;padding:2px 6px;">开始抽签</span> 按钮后</h3>系统将随机分配监考人员,并可下载excel格式的监考安排表。<hr></li>
</ul>
<button popovertarget="help" popovertargetaction="hide">明白了</button>
</div>
<!-- 主容器 -->
<div class="content-wrapper">
<h1>智能监考抽签系统</h1>
<!-- 考试名称输入 -->
<input
type="text"
id="eventTitle"
placeholder="请输入考试名称"
style="margin-bottom:20px;"
>
<!-- 文件上传区域 -->
<div class="input-section">
<!-- 考场信息表上传 -->
<div class="file-upload">
<h3>上传 考场信息表.xlsx</h3>
<input type="file" id="examRoomFile" accept=".xlsx, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">
<div id="examRoomInfo" class="file-info"></div>
</div>
<!-- 主监考名单上传 -->
<div class="file-upload">
<h3>上传 主监考名单.txt</h3>
<input type="file" id="mainTeacherFile" accept=".txt">
<div id="mainTeacherInfo" class="file-info"></div>
</div>
<!-- 副监考名单上传 -->
<div class="file-upload">
<h3>上传 副监考名单.txt</h3>
<input type="file" id="subTeacherFile" accept=".txt">
<div id="subTeacherInfo" class="file-info"></div>
</div>
</div>
<!-- 统计信息 -->
<div class="stats-box" id="statsBox" style="display: none;">
<div class="stat-item">
<div class="stat-value" id="totalMainNeeded">0</div>
<div class="stat-label">需要主监考总数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="totalSubNeeded">0</div>
<div class="stat-label">需要副监考总数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="mainTeachersCount">0</div>
<div class="stat-label">主监考名单人数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="subTeachersCount">0</div>
<div class="stat-label">副监考名单人数</div>
</div>
</div>
<!-- 操作按钮 -->
<div style="text-align: center; margin-top: 30px;">
<button onclick="startLottery()">开始抽签</button>
</div>
<!-- 结果展示 -->
<div class="status-bar" id="statusInfo">等待上传文件...</div>
<div id="resultArea" style="display: none;">
<table id="resultTable">
<thead>
<tr>
<th colspan="5" style="text-align: center;">
<h2 id="titleHeader"></h2>
<span id="fullTime"></span>
</th>
</tr>
<tr>
<th width="8%">序号</th>
<th width="20%">考场编号</th>
<th width="20%">考场地址</th>
<th width="25%">主监考</th>
<th width="25%">副监考</th>
</tr>
</thead>
<tbody id="resultBody"></tbody>
</table>
<a href="javascript:void(0);" id="toExcel" onclick="exportToExcel()"style="margin-top: 15px;">导出Excel表格</a>
<div class="warning" id="warningMessage"></div>
</div>
</div>
</div>
<script>
// 数据存储
let examRooms = [];
let originalMainTeachers = [];
let originalSubTeachers = [];
let currentResult = [];
let totalMainNeeded = 0;
let totalSubNeeded = 0;
// 解析Excel文件
function parseExamRoomFile(content) {
const workbook = XLSX.read(content, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
let headers = [];
const data = [];
// 解析表头和数据
const rows = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
if (rows.length === 0) {
throw new Error('Excel文件为空或格式不正确');
}
// 获取表头
headers = rows[0].filter(cell => cell && cell.toString().trim() !== '');
if (headers.length === 0) {
throw new Error('Excel文件缺少表头行');
}
// 解析数据行
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
if (!row || row.length === 0) continue;
const obj = {};
for (let j = 0; j < headers.length; j++) {
if (headers[j] && row[j] !== undefined) {
obj[headers[j].trim()] = row[j];
}
}
// 验证必填字段
if (
obj['考场编号'] &&
obj['考场地址'] &&
obj['主监考人数'] !== undefined &&
obj['副监考人数'] !== undefined
) {
// 为每行数据添加序号(从1开始)
obj.seq = data.length + 1;
data.push(obj);
}
}
if (data.length === 0) {
throw new Error('Excel文件中没有有效的数据行');
}
// 转换为最终数据格式
return data.map(item => ({
seq: item.seq, // 添加seq字段
code: item['考场编号'].toString().trim(),
address: item['考场地址'].toString().trim(),
mainNum: parseInt(item['主监考人数']) || 0,
subNum: parseInt(item['副监考人数']) || 0
}));
}
// 解析教师名单文件
function parseTeacherFile(content) {
return content.split('\n')
.map(line => line.trim())
.filter(line => line !== '');
}
// 检查数据有效性
function checkDataValidity() {
if (examRooms.length && originalMainTeachers.length && originalSubTeachers.length) {
calculateTotals();
showStats();
}
}
// 计算监考需求总数
function calculateTotals() {
totalMainNeeded = examRooms.reduce((sum, room) => sum + room.mainNum, 0);
totalSubNeeded = examRooms.reduce((sum, room) => sum + room.subNum, 0);
// 更新统计信息
document.getElementById('totalMainNeeded').textContent = totalMainNeeded;
document.getElementById('totalSubNeeded').textContent = totalSubNeeded;
document.getElementById('mainTeachersCount').textContent = originalMainTeachers.length;
document.getElementById('subTeachersCount').textContent = originalSubTeachers.length;
}
// 显示统计信息
function showStats() {
document.getElementById('statsBox').style.display = 'flex';
let statusHTML = `已加载数据:`;
statusHTML += `考场:${examRooms.length}个 `;
statusHTML += `主监考:${originalMainTeachers.length}人 `;
statusHTML += `副监考:${originalSubTeachers.length}人 `;
statusHTML += `需求:主监考${totalMainNeeded}人 | 副监考${totalSubNeeded}人`;
document.getElementById('statusInfo').innerHTML = statusHTML;
const warningElement = document.getElementById('warningMessage');
if (totalMainNeeded > originalMainTeachers.length) {
warningElement.style.display = 'block';
warningElement.innerHTML = `
<div style="color:#e74c3c; font-weight:bold;">
<h4 style="margin:0 0 10px 0;">⚠️ 主监考不足</h4>
<p style="margin:0;">缺少 ${totalMainNeeded - originalMainTeachers.length} 人</p>
</div>
`;
} else if (totalSubNeeded > originalSubTeachers.length) {
warningElement.style.display = 'block';
warningElement.innerHTML = `
<div style="color:#e74c3c; font-weight:bold;">
<h4 style="margin:0 0 10px 0;">⚠️ 副监考不足</h4>
<p style="margin:0;">缺少 ${totalSubNeeded - originalSubTeachers.length} 人</p>
</div>
`;
} else {
warningElement.style.display = 'block';
warningElement.innerHTML = `
<div style="color:#27ae60; font-weight:bold;">
<h4 style="margin:0 0 10px 0;">✓ 数据完整</h4>
<p style="margin:0;">可以开始抽签</p>
</div>
`;
}
}
// 显示错误提示
function showErrorAlert(title, message) {
alert(`${title}\n\n${message}`);
document.getElementById('statusInfo').innerHTML =
`<span style="color:#e74c3c">${title}: ${message}</span>`;
}
// 开始抽签
function startLottery() {
if (!validateData()) return;
// 重置状态
resetStatus();
// 初始化抽签池
let mainPool = [...originalMainTeachers];
let subPool = [...originalSubTeachers];
currentResult = [];
// 执行抽签
examRooms.forEach(room => {
const mainSelected = selectTeachers(mainPool, room.mainNum);
const subSelected = selectTeachers(subPool, room.subNum);
currentResult.push({
seq: room.seq, // 使用从Excel解析的seq
code: room.code,
address: room.address,
main: mainSelected.join(', '),
sub: subSelected.join(', ')
});
});
// 显示结果
showResult();
}
// 随机选择教师
function selectTeachers(pool, count) {
if (count === 0) return [];
if (pool.length < count) {
throw new Error(`需要${count}人,但仅剩${pool.length}人`);
}
const selected = [];
for (let i = 0; i < count; i++) {
const randIndex = Math.floor(Math.random() * pool.length);
selected.push(pool.splice(randIndex, 1)[0]);
}
return selected;
}
// 验证数据
function validateData() {
if (examRooms.length === 0) {
showErrorAlert('错误', '请先上传考场信息表');
return false;
}
if (originalMainTeachers.length === 0) {
showErrorAlert('错误', '请先上传主监考名单');
return false;
}
if (originalSubTeachers.length === 0) {
showErrorAlert('错误', '请先上传副监考名单');
return false;
}
if (totalMainNeeded > originalMainTeachers.length) {
showErrorAlert('错误', `主监考不足:需要${totalMainNeeded}人,但只有${originalMainTeachers.length}人`);
return false;
}
if (totalSubNeeded > originalSubTeachers.length) {
showErrorAlert('错误', `副监考不足:需要${totalSubNeeded}人,但只有${originalSubTeachers.length}人`);
return false;
}
return true;
}
// 重置状态
function resetStatus() {
document.getElementById('statusInfo').innerHTML = '正在抽签...';
document.getElementById('warningMessage').style.display = 'none';
document.getElementById('resultArea').style.display = 'none';
}
// 显示结果
function showResult() {
const resultBody = document.getElementById('resultBody');
resultBody.innerHTML = '';
// 设置标题
const eventTitle = document.getElementById('eventTitle').value.trim();
document.getElementById('titleHeader').textContent = `${eventTitle} - 监考安排表`;
document.getElementById('fullTime').textContent = `抽签时间:${new Date().toLocaleString('zh-CN')}`;
// 填充表格数据
currentResult.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${item.seq}</td>
<td>${item.code}</td>
<td>${item.address}</td>
<td>${item.main || '-'}</td>
<td>${item.sub || '-'}</td>
`;
resultBody.appendChild(row);
});
document.getElementById('resultArea').style.display = 'block';
// 滚动到结果区域
document.getElementById('resultArea').scrollIntoView({
behavior: 'smooth'
});
document.getElementById('statusInfo').innerHTML =
`<span style="color:#27ae60; font-weight:bold;">✓ 抽签完成!共为 ${examRooms.length} 个考场分配了监考人员。</span>`;
}
// 导出到Excel
function exportToExcel() {
if (currentResult.length === 0) {
alert('请先完成抽签再导出');
return;
}
const eventTitle = document.getElementById('eventTitle').value.trim();
const title = `${eventTitle} - 监考安排表`;
const tableData = [
[title],
[`抽签时间:${new Date().toLocaleString('zh-CN')}`],
['序号', '考场编号', '考场地址', '主监考', '副监考'],
...currentResult.map(item => [
item.seq,
item.code,
item.address,
item.main || '-',
item.sub || '-'
])
];
const ws = XLSX.utils.aoa_to_sheet(tableData);
// 设置列宽
const wscols = [
{wch: 8}, // 序号
{wch: 15}, // 考场编号
{wch: 25}, // 考场地址
{wch: 25}, // 主监考
{wch: 25} // 副监考
];
ws['!cols'] = wscols;
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '监考安排');
const excelBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([excelBuffer], { type: 'application/octet-stream' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `监考安排_${title}.xlsx`;
link.click();
}
// 文件处理事件
document.getElementById('examRoomFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function() {
try {
examRooms = parseExamRoomFile(reader.result);
if (examRooms.length > 0) {
document.getElementById('examRoomInfo').innerHTML =
`✓ 已加载 <span style="color:#2ecc71">${examRooms.length}</span> 个考场`;
document.getElementById('statusInfo').innerHTML =
`已加载考场信息:${examRooms.length}个考场`;
checkDataValidity();
} else {
document.getElementById('examRoomInfo').innerHTML =
'❌ 文件解析失败,请检查格式';
}
} catch (error) {
showErrorAlert('解析考场信息文件时出错', error.message);
document.getElementById('examRoomInfo').innerHTML =
'❌ 解析错误:' + error.message;
}
};
reader.readAsArrayBuffer(file);
});
document.getElementById('mainTeacherFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function() {
try {
originalMainTeachers = parseTeacherFile(reader.result);
document.getElementById('mainTeacherInfo').innerHTML =
`✓ 已加载 <span style="color:#2ecc71">${originalMainTeachers.length}</span> 位主监考`;
checkDataValidity();
} catch (error) {
showErrorAlert('解析主监考文件时出错', error.message);
document.getElementById('mainTeacherInfo').innerHTML =
'❌ 解析错误:' + error.message;
}
};
reader.readAsText(file);
});
document.getElementById('subTeacherFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function() {
try {
originalSubTeachers = parseTeacherFile(reader.result);
document.getElementById('subTeacherInfo').innerHTML =
`✓ 已加载 <span style="color:#2ecc71">${originalSubTeachers.length}</span> 位副监考`;
checkDataValidity();
} catch (error) {
showErrorAlert('解析副监考文件时出错', error.message);
document.getElementById('subTeacherInfo').innerHTML =
'❌ 解析错误:' + error.message;
}
};
reader.readAsText(file);
});
// 初始化页面
window.onload = function() {
document.getElementById('eventTitle').focus();
document.getElementById('statusInfo').innerHTML = '请上传考场信息表(XLSX格式)和监考名单';
// 初始化帮助弹窗
const helpButton = document.querySelector('[onclick="toggleHelp()"]');
if (helpButton) {
helpButton.addEventListener('click', toggleHelp);
}
// 添加键盘快捷键支持
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const popover = document.getElementById('helpPopover');
if (popover.classList.contains('active')) {
toggleHelp();
}
}
});
};
</script>
</body>
</html>