1.前言
对象上课点名需要增加趣味性,然后就想着搞一个随机点名的系统,找了一下网上已有的方案简单,用deepseek做了下修改,用来支持Excel上传
2.废话不多说上硬货
html代码如下
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能课堂点名系统</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.5/xlsx.full.min.js"></script>
<style>
:root {
--primary-color: #4361ee;
--secondary-color: #3f37c9;
--accent-color: #4895ef;
--success-color: #4cc9f0;
--background-color: #f8f9fa;
}
body {
background: var(--background-color);
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.main-title {
color: var(--primary-color);
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
font-weight: 700;
}
.control-panel {
background: white;
border-radius: 15px;
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
padding: 20px;
margin-bottom: 30px;
}
.student-card {
transition: all 0.3s ease;
background: linear-gradient(145deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
border: 1px solid rgba(0,0,0,0.05);
position: relative;
overflow: hidden;
}
.student-card.called {
background: linear-gradient(145deg, #e3f2fd 0%, #bbdefb 100%);
}
.student-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0,0,0,0.1);
}
#nameDisplay {
font-size: 3.5rem;
font-weight: 700;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
color: var(--primary-color);
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
margin: 20px 0;
}
.progress {
height: 10px;
border-radius: 5px;
background-color: #e9ecef;
}
.progress-bar {
background-color: var(--accent-color);
transition: width 0.5s ease-in-out;
}
.stats-card {
background: white;
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--primary-color);
}
.custom-file-upload {
border: 2px dashed #dee2e6;
border-radius: 10px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.custom-file-upload:hover {
border-color: var(--primary-color);
background-color: rgba(67, 97, 238, 0.05);
}
.search-box {
position: relative;
margin-bottom: 20px;
}
.search-box input {
padding-left: 40px;
border-radius: 25px;
}
.search-box::before {
content: "🔍";
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
}
.floating-buttons {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.floating-button {
width: 50px;
height: 50px;
border-radius: 25px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.floating-button:hover {
transform: scale(1.1);
}
@keyframes highlight {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
.highlight-animation {
background: linear-gradient(270deg, #4361ee, #4cc9f0);
background-size: 200% 200%;
animation: highlight 2s ease infinite;
color: white !important;
}
</style>
</head>
<body>
<div class="container py-5">
<h1 class="main-title text-center mb-5 animate__animated animate__fadeIn">智能课堂点名系统 Pro</h1>
<div class="control-panel animate__animated animate__fadeInUp">
<div class="custom-file-upload mb-4">
<input type="file" id="fileInput" class="d-none" accept=".csv,.json,.xlsx">
<label for="fileInput" class="mb-0">
<div class="text-muted">
<i class="fs-3 mb-2">📁</i>
<p class="mb-0">点击或拖拽文件到此处上传名单</p>
<small>(支持 CSV 或 JSON 格式 或者 Excel 格式)</small>
</div>
</label>
</div>
<div class="d-flex justify-content-center gap-3">
<button class="btn btn-primary px-4" onclick="startRollCall()">
<i class="fs-5">🎲</i> 开始随机点名
</button>
<button class="btn btn-outline-primary px-4" onclick="resetRollCall()">
<i class="fs-5">🔄</i> 重置点名
</button>
</div>
</div>
<div id="nameDisplay" class="animate__animated"></div>
<div class="row g-4">
<div class="col-md-4">
<div class="stats-card animate__animated animate__fadeInLeft">
<h5 class="text-primary mb-4">课堂统计</h5>
<div class="row g-3">
<div class="col-6">
<div class="text-muted">总人数</div>
<div class="stat-value" id="totalCount">0</div>
</div>
<div class="col-6">
<div class="text-muted">已点名</div>
<div class="stat-value" id="calledCount">0</div>
</div>
</div>
<div class="progress mt-4">
<div id="progressBar" class="progress-bar" role="progressbar"></div>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card animate__animated animate__fadeInRight">
<div class="card-header bg-primary text-white">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">学生名单</h5>
<div class="btn-group">
<button class="btn btn-sm btn-light" onclick="toggleView('grid')">
📱 网格视图
</button>
<button class="btn btn-sm btn-light" onclick="toggleView('list')">
📝 列表视图
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="search-box">
<input type="text" class="form-control" placeholder="搜索学生..." id="searchInput">
</div>
<div id="studentList" class="row g-3"></div>
</div>
</div>
</div>
</div>
</div>
<div class="floating-buttons">
<button class="floating-button btn btn-primary" onclick="toggleSound()" id="soundToggle" title="声音开关">
🔊
</button>
<button class="floating-button btn btn-info" onclick="exportData()" title="导出数据">
💾
</button>
</div>
<script>
let students = [];
let calledStudents = new Set();
let viewMode = 'grid';
let soundEnabled = true;
let currentAnimation = null;
// 文件拖拽上传
const fileInput = document.getElementById('fileInput');
const dropZone = document.querySelector('.custom-file-upload');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.borderColor = 'var(--primary-color)';
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#dee2e6';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#dee2e6';
const files = e.dataTransfer.files;
if (files.length) {
fileInput.files = files;
handleFileUpload(files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length) {
handleFileUpload(e.target.files[0]);
}
});
function handleFileUpload(file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
if (file.name.endsWith('.json')) {
const data = JSON.parse(e.target.result);
processStudentData(data);
} else if (file.name.endsWith('.csv')) {
const csvData = e.target.result.split('\n');
const data = csvData.slice(1).map((line, index) => {
const [name, studentId, className] = line.split(',');
return {
name: name?.trim(),
studentId: studentId?.trim(),
class: className?.trim()
};
});
processStudentData(data);
} else if (file.name.endsWith('.xlsx')) {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, {
type: 'array',
WBProps: { CodePage: 936 } // 指定编码为GBK
});
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const studentsData = jsonData.slice(1).map(row => {
const name = row[0]?.toString()?.trim();
const studentId = row[1]?.toString()?.trim();
const class_name = row[2]?.toString()?.trim();
return {
name: name || `学生${row[0]}`,
studentId: studentId || `NO.${row[0]}`,
class: class_name || '默认班级'
};
});
processStudentData(studentsData);
}
} catch (error) {
showToast('文件解析失败,请检查文件格式是否正确!', 'danger');
console.error('文件解析错误:', error);
}
};
reader.readAsArrayBuffer(file);
}
function processStudentData(data) {
students = data.filter(item => item.name).map((item, index) => ({
id: index + 1,
name: item.name,
studentId: item.studentId || `NO.${index + 1}`,
class: item.class || '默认班级'
}));
updateStudentList();
updateStatistics();
showToast('名单上传成功!');
}
function startRollCall() {
if (students.length === 0) {
showToast('请先上传学生名单!', 'warning');
return;
}
const availableStudents = students.filter(s => !calledStudents.has(s.id));
if (availableStudents.length === 0) {
showToast('所有学生已完成点名!', 'info');
return;
}
if (currentAnimation) {
clearInterval(currentAnimation);
}
let count = 0;
const maxIterations = 20;
const finalStudent = availableStudents[Math.floor(Math.random() * availableStudents.length)];
const nameDisplay = document.getElementById('nameDisplay');
nameDisplay.classList.add('animate__animated', 'animate__rubberBand');
currentAnimation = setInterval(() => {
const randomStudent = availableStudents[Math.floor(Math.random() * availableStudents.length)];
nameDisplay.textContent = randomStudent.name;
if (++count >= maxIterations) {
clearInterval(currentAnimation);
currentAnimation = null;
nameDisplay.textContent = finalStudent.name;
calledStudents.add(finalStudent.id);
updateStudentList();
updateStatistics();
if (soundEnabled) playSound();
// 高亮显示被点到的学生卡片
const studentCard = document.querySelector(`[data-student-id="${finalStudent.id}"]`);
if (studentCard) {
studentCard.classList.add('highlight-animation');
setTimeout(() => {
studentCard.classList.remove('highlight-animation');
}, 2000);
}
}
}, 100);
}
function resetRollCall() {
if (confirm('确定要重置点名状态吗?')) {
calledStudents.clear();
updateStudentList();
updateStatistics();
document.getElementById('nameDisplay').textContent = '';
showToast('点名状态已重置!');
}
}
function updateStatistics() {
document.getElementById('totalCount').textContent = students.length;
document.getElementById('calledCount').textContent = calledStudents.size;
const progress = (calledStudents.size / students.length * 100).toFixed(1);
document.getElementById('progressBar').style.width = `${progress}%`;
document.getElementById('progressBar').setAttribute('aria-valuenow', progress);
}
function updateStudentList() {
const container = document.getElementById('studentList');
const template = viewMode === 'grid' ? getGridTemplate : getListTemplate;
container.innerHTML = students.map(template).join('');
}
function getGridTemplate(student) {
const isCalled = calledStudents.has(student.id);
return `
<div class="col-12 col-sm-6 col-lg-4">
<div class="student-card p-3 ${isCalled ? 'called' : ''}" data-student-id="${student.id}">
<div class="fw-bold">${student.name}</div>
<small class="text-muted">${student.studentId}</small>
<div class="badge bg-secondary mt-1">${student.class}</div>
${isCalled ? '<div class="badge bg-success mt-1">已点到</div>' : ''}
</div>
</div>
`;
}
function getListTemplate(student) {
const isCalled = calledStudents.has(student.id);
return `
<div class="col-12 mb-2">
<div class="student-card p-3 ${isCalled ? 'called' : ''}" data-student-id="${student.id}">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="fw-bold">${student.name}</div>
<small class="text-muted">${student.studentId}</small>
</div>
<div>
<span class="badge bg-secondary">${student.class}</span>
${isCalled ? '<span class="badge bg-success ms-2">已点到</span>' : ''}
</div>
</div>
</div>
</div>
`;
}
function toggleView(mode) {
viewMode = mode;
updateStudentList();
localStorage.setItem('viewMode', mode);
}
function toggleSound() {
soundEnabled = !soundEnabled;
const btn = document.getElementById('soundToggle');
btn.innerHTML = soundEnabled ? '🔊' : '🔈';
showToast(soundEnabled ? '声音已开启' : '声音已关闭');
}
function playSound() {
if (!soundEnabled) return;
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.type = 'sine';
const now = audioContext.currentTime;
oscillator.frequency.setValueAtTime(660, now);
oscillator.frequency.exponentialRampToValueAtTime(500, now + 0.1);
gainNode.gain.setValueAtTime(0, now);
gainNode.gain.linearRampToValueAtTime(0.3, now + 0.02);
gainNode.gain.linearRampToValueAtTime(0, now + 0.3);
oscillator.start(now);
oscillator.stop(now + 0.3);
}
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = 'position-fixed top-0 start-50 translate-middle-x p-3 animate__animated animate__fadeInDown';
toast.style.zIndex = '1050';
toast.innerHTML = `
<div class="toast show" role="alert">
<div class="toast-header bg-${type} text-white">
<strong class="me-auto">系统提示</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
${message}
</div>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.remove('animate__fadeInDown');
toast.classList.add('animate__fadeOutUp');
setTimeout(() => toast.remove(), 500);
}, 3000);
}
function exportData() {
const data = {
totalStudents: students.length,
calledCount: calledStudents.size,
timestamp: new Date().toLocaleString(),
calledStudents: students.filter(s => calledStudents.has(s.id)),
remainingStudents: students.filter(s => !calledStudents.has(s.id))
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `点名记录_${new Date().toLocaleDateString()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 搜索功能实现
document.getElementById('searchInput').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const filtered = students.filter(student =>
student.name.toLowerCase().includes(searchTerm) ||
student.studentId.toLowerCase().includes(searchTerm) ||
student.class.toLowerCase().includes(searchTerm)
);
const container = document.getElementById('studentList');
container.innerHTML = filtered.map(viewMode === 'grid' ? getGridTemplate : getListTemplate).join('');
});
// 初始化视图模式
const savedViewMode = localStorage.getItem('viewMode');
if (savedViewMode) {
viewMode = savedViewMode;
updateStudentList();
}
// 初始化声音设置
const savedSound = localStorage.getItem('soundEnabled');
if (savedSound !== null) {
soundEnabled = savedSound === 'true';
document.getElementById('soundToggle').innerHTML = soundEnabled ? '🔊' : '🔈';
}
function toggleSound() {
soundEnabled = !soundEnabled;
const btn = document.getElementById('soundToggle');
btn.innerHTML = soundEnabled ? '🔊' : '🔈';
localStorage.setItem('soundEnabled', soundEnabled);
showToast(soundEnabled ? '声音已开启' : '声音已关闭');
}
// 页面加载时初始化
window.addEventListener('DOMContentLoaded', () => {
// 移除初始动画类名
const nameDisplay = document.getElementById('nameDisplay');
nameDisplay.classList.remove('animate__rubberBand');
});
// 触摸设备支持
let touchTimer;
document.querySelectorAll('.student-card').forEach(card => {
card.addEventListener('touchstart', () => {
touchTimer = setTimeout(() => {
card.classList.add('called');
}, 500);
});
card.addEventListener('touchend', () => {
clearTimeout(touchTimer);
});
});
// 键盘快捷键支持
document.addEventListener('keydown', (e) => {
if (e.code === 'Space' && !currentAnimation) {
startRollCall();
e.preventDefault();
}
});
</script>
</body>
</html>
3.打包可执行文件
html我自己运行可以,但是给别人用还是不大方便,本来想将服务部署在公网上面,但是想着没有鉴权,可能学生会捣乱,所以还是打包成可执行文件,本地运行比较稳妥
1.创建一个虚拟环境
conda create -n flask python=3.8
2.安装所需的依赖库
pip install flask
pip install pyinstaller
3.打包对应为可执行文件
pyinstaller --onefile app.py
这时候打包的文件汇报错找不到index.html,这是因为默认打包方式没指定html文件
修改app.spec文件,指定模版路径
a = Analysis(
['app.py'],
pathex=[],
binaries=[],
datas=[('templates/*.html', 'templates')], # 指定模版路径
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='app',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
注:尝试将html直接写在app文件中,发现无法文件无法上传成功
4.使用方法
-
在Windows 双击 app.exe文件
-
将所需文件拖拽到上面,如:测试文件.xlsx
上传完成后点击开始随机点名
5.代码地址
将源码以及可执行文件已放入该地址,需要的可以自取