上课点名系统

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.使用方法

  1. 在Windows 双击 app.exe文件

  2. 网页打开 http://127.0.0.1:5000/

  3. 将所需文件拖拽到上面,如:测试文件.xlsx

上传完成后点击开始随机点名
 

 5.代码地址

将源码以及可执行文件已放入该地址,需要的可以自取

call-the-roll: 课堂随机点名系统

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值