智能学号抽取系统V5.6.4重磅发布

告别随机数,拥抱智能点名!—— 全新升级的“智能学号抽取系统V5.6.4”重磅发布!

摘要: 还在为课堂随机提问、活动抽奖而手动翻名单、查表格而烦恼吗?还在忍受传统点名工具的简陋和不智能吗?今天,我将为大家带来一款由我亲手打造的革命性工具——智能学号抽取系统V5.6.4。它已不再是简单的随机数生成器,而是一个集名单管理、智能抽取、视觉盛宴于一体的全能教学与活动助手!从 V5.3.2 到 V5.6.4,我们不仅保留了所有核心功能,更进行了代码精简、体验优化与交互革新,让工具回归本质,更加纯粹、高效和现代。

项目体验地址(直接打开即可使用): https://www.html2web.com/view/688deed142d51c6f14c0fae0

源码已开源,欢迎 Star & Fork! https://github.com/HerryABU/student-number-picker

本文章地址:
https://blog.csdn.net/Herryfyh/article/details/149863114


🚀 从“智能”到“更优”:V5.6.4 的精简、优化与交互革新

在 V5.3.2 版本广受好评的基础上,我们对系统进行了一次深度重构与体验升级。本次更新的核心思想是 “化繁为简,聚焦核心,提升交互”。我们移除了使用频率较低的“字体设置”功能,但保留了至关重要的“字号设置”,让您可以自由调节显示大小,确保在大屏幕投影时也能清晰可见。同时,我们对名单管理功能进行了增强,并为抽取过程注入了全新的活力。

🔥 1. 名单管理功能增强(全新优化)

痛点解决: 旧版无法删除单个学生名单或整个分组,管理不够灵活。
全新进化: 为名单管理面板添加了“删除”功能!

  • 在“学生名单管理”区域,每个学生姓名后都增加了 🗑️ 删除图标
  • 点击即可删除该学生,方便您随时更新名单。
  • 同时,分组标签页也支持删除整个分组,让您的数据管理更加得心应手。
🌐 2. 全新Excel拖拽上传(全新功能)

痛点解决: 旧版需要点击“上传”按钮,再从文件夹中选择文件,操作步骤繁琐。
全新进化: 重磅加入“拖拽上传”功能!

  • 您现在可以直接将电脑上的 .xlsx.xls 文件拖拽到指定区域,松开鼠标即可自动上传。
  • 上传区域会实时显示“释放鼠标即可上传文件”的提示,交互反馈清晰。
  • 效果:上传操作一气呵成,极大提升了操作效率和用户体验,符合现代 Web 应用的操作习惯。
🌀 3. 全新翻转动画效果(全新功能)

痛点解决: 抽取动画虽然多样,但缺少一种经典且富有仪式感的效果。
全新进化: 重磅加入“翻转动画”!

  • 当您点击“抽取学号”或“连抽”时,学号和姓名将从卡片背面3D翻转至正面,视觉效果更加生动和惊喜。
  • 这一动画与原有的“弹入”、“滚动”效果并存,您每次抽取都能获得不同的视觉反馈,让点名过程充满乐趣。
🛠️ 4. 抽取逻辑深度优化(功能增强)

痛点解决: 在特定操作顺序下(如快速抽取后立即连抽),存在极小概率出现重复抽取的潜在问题。
全新进化: 对抽取核心逻辑进行了全面优化!

  • 重构了 getWeightedRandomNumber 函数,确保在单次抽取、快速抽取、连抽、批量抽取等各种模式下,只要开启了“不重复抽取”,就能更可靠地避免重复。
  • (重要提示) 虽然已极大优化,但仍建议避免在“快速抽取”和“连抽/批量抽取”之间进行极其频繁的混用切换,以获得最稳定可靠的体验。日常使用中完全无需担心。

🎯 功能亮点总览

V6.0.1 版本在精简中求卓越,核心功能依然强大:

  • ✅ 双模式操作: 灵活选择“学号范围”或“名单导入”模式。
  • ✅ 多名单组管理: 支持创建、切换、删除多个独立的学生分组。
  • ✅ 全新暗黑模式: 一键切换浅色/深色主题,保护您的眼睛。
  • ✅ 多种抽取模式: 支持单次抽取、快速抽取(学号滚动)、连抽N次、批量抽取。
  • ✅ 不重复抽取: 开启后,确保每个学号在本轮抽取中仅出现一次。
  • ✅ 智能概率权重控制: 为不同学号区间设置不同的抽取概率。
  • ✅ 精简字号设置: 移除字体选择,保留字号调节滑块,更加简洁高效
  • ✅ 丰富的动画反馈: 新增翻转动画,搭配原有的高亮闪烁、庆祝动画,体验升级。
  • ✅ 数据持久化: 所有学生名单、分组、设置均自动保存,刷新不丢失。
  • ✅ URL 参数同步: 所有配置(当前分组、学号范围、字体、主题等)都会同步到 URL,方便分享与恢复。
  • ✅ 拖拽上传: 支持将 Excel 文件直接拖拽到网页进行上传,操作更便捷。

💡 技术实现精要

本次更新在 main.jsstyle.css 中进行了关键修改:

  1. 拖拽上传实现:通过监听 dragenter, dragover, dragleave, drop 等事件,实现了文件拖拽功能。handleDrop 函数在用户释放文件时被调用,并将文件传递给核心的 handleFileData 函数进行处理。
  2. 删除名单功能:为每个学生项添加了删除按钮,并绑定了 deleteStudent 方法,直接从当前分组的学生数组中移除指定项。
  3. 翻转动画实现:在 CSS 中定义了新的 flip 动画类,利用 transform: rotateY() 实现 3D 翻转效果,并通过 Vue 的响应式 currentAnimationClass 变量动态应用。
  4. 抽取逻辑优化:在 drawNumbermultiDraw 等函数中,强化了对 available 数组的检查和对结果的去重处理,确保抽取结果的唯一性。
// 处理文件拖放
const handleDrop = (e) => {isDragOver.value = false;const files = e.dataTransfer.files;if (files.length > 0) {const file = files[0];if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) {handleFileData(file); // 核心处理函数} else {showNotification('请上传Excel文件(.xlsx或.xls)', 'error');}}
};// 删除学生名单
const deleteStudent = (index) => {if (confirm('确定要删除该学生吗?')) {currentGroupStudents.value.splice(index, 1);saveGroups();showNotification('已删除学生');}
};

🛠️ 适用场景

  • 👩‍🏫 课堂教学: 随机提问、作业检查、小组分配。
  • 🏢 企业会议: 抽奖、发言、任务分配。
  • 🎓 培训活动: 互动问答、游戏环节。
  • 🎉 社团活动: 幸运观众抽取。

📌 结语

“智能学号抽取系统V6.0.1”是一次成功的迭代。我们没有盲目增加功能,而是倾听用户反馈,对现有功能进行打磨和优化。“拖拽上传”让操作更丝滑,“删除名单”让管理更自由,“翻转动画”让体验更生动,而底层逻辑的优化则让工具更加可靠。

现在就去体验吧! 访问我们的在线体验地址,感受这份精简、强大与现代交互的完美结合。如果您喜欢这个项目,别忘了给我的 GitHub 仓库点个 Star!您的支持是我持续更新的最大动力。

项目体验地址: https://www.html2web.com/view/688deed142d51c6f14c0fae0

源码地址: https://github.com/HerryABU/student-number-picker

期待您的反馈,让我们一起把这款工具做得更好!

全部代码

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>智能学号抽取系统V5.6.4</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"><style>:root {/* 主题颜色变量 */--primary-color: #4361ee;--success-color: #4cc9f0;--danger-color: #f72585;--warning-color: #f8961e;--bg-color: #f8f9fa;--card-bg: white;--card-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);--border-color: #e9ecef;--text-dark: #2b2d42;--text-light: #8d99ae;--text-muted: #6c757d;--transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);--dark-primary-color: #5e72e4;--dark-success-color: #2dce89;--dark-danger-color: #f5365c;--dark-warning-color: #fb6340;--dark-bg-color: #1a1a2e;--dark-card-bg: #16213e;--dark-card-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);--dark-border-color: #2d3748;--dark-text-dark: #f8f9fa;--dark-text-light: #e2e8f0;--dark-text-muted: #a0aec0;/* 字体与字号变量 */--global-font: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--name-size: 5rem;--number-size: 80pt;/* 动画时长变量 */--anim-duration: 0.5s;--anim-easing: cubic-bezier(0.175, 0.885, 0.32, 1.275);}/* 深色模式变量覆盖 */.dark-mode {--primary-color: var(--dark-primary-color);--success-color: var(--dark-success-color);--danger-color: var(--dark-danger-color);--warning-color: var(--dark-warning-color);--bg-color: var(--dark-bg-color);--card-bg: var(--dark-card-bg);--card-shadow: var(--dark-card-shadow);--border-color: var(--dark-border-color);--text-dark: var(--dark-text-dark);--text-light: var(--dark-text-light);--text-muted: var(--dark-text-muted);}/* 全局背景过渡 */body {background: var(--bg-color);transition: background 0.3s ease;}.dark-mode body {background: var(--dark-bg-color);}/* 深色模式下的特定元素样式 */.dark-mode .display-area,.dark-mode .multi-draw-display {background: #1e293b;box-shadow: inset 0 -8px 12px rgba(0,0,0,0.3),inset 0 8px 12px rgba(255,255,255,0.05),0 4px 12px rgba(0,0,0,0.2);}.dark-mode .student-name,.dark-mode .student-id,.dark-mode .multi-draw-item-name {color: var(--dark-text-light);}.dark-mode .history {background: #1e293b;}.dark-mode .history-item {background: #2d3748;color: var(--dark-text-light);}.dark-mode .settings-panel,.dark-mode .student-list-panel,.dark-mode .batch-panel {background: #1e293b;}.dark-mode input[type="number"] {background: linear-gradient(#2d3748, #2d3748) padding-box, linear-gradient(45deg, #ff7eb3, #65d9ff, #c7f464, #ff7eb3) border-box;color: var(--dark-text-light);}/* 重置默认样式 */* {margin: 0;padding: 0;box-sizing: border-box;}/* 页面整体布局 */body {font-family: var(--global-font);display: flex;justify-content: center;align-items: center;min-height: 100vh;margin: 0;color: var(--text-dark);transition: background 0.3s ease, color 0.3s ease;}/* 主容器样式 */.container {background: var(--card-bg);padding: 2.5rem 3rem;border-radius: 16px;box-shadow: var(--card-shadow);width: min(90%, 1000px);text-align: center;position: relative;overflow: hidden;margin: 2rem 0;transition: background 0.3s ease, box-shadow 0.3s ease;}/* 容器顶部动态渐变条 */.container::before {content: '';position: absolute;top: 0;left: 0;width: 100%;height: 8px;background: linear-gradient(90deg, #4361ee, #4cc9f0, #f72585, #4361ee);background-size: 300% 100%;animation: gradientFlow 3s linear infinite;}.dark-mode .container::before {background: linear-gradient(90deg, #5e72e4, #2dce89, #f5365c, #5e72e4);}/* 渐变流动动画 */@keyframes gradientFlow {0% { background-position: 0% 50%; }100% { background-position: 100% 50%; }}/* 页面标题样式 */h1 {font-weight: 600;position: relative;padding-bottom: 1rem;font-size: 1.8rem;margin: 1rem 0 2rem;color: var(--text-dark);transition: color 0.3s ease;}/* 标题下方装饰线 */h1::after {content: "";position: absolute;bottom: 0;left: 50%;transform: translateX(-50%);width: 80px;height: 4px;background: var(--primary-color);border-radius: 2px;transition: background 0.3s ease;}/* 结果显示区域 */.display-area {margin: 30px 0;background: #f8f9fa;padding: 20px;border-radius: 8px;box-shadow: inset 0 -8px 12px rgba(0,0,0,0.1),inset 0 8px 12px rgba(255,255,255,0.7),0 4px 12px rgba(0,0,0,0.1);min-height: 320px;display: flex;flex-direction: column;align-items: center;justify-content: center;transition: all 0.3s ease;position: relative;}/* 姓名显示样式 */.student-name {font-size: var(--name-size);font-weight: bold;color: var(--primary-color);margin-bottom: 1rem;font-family: var(--global-font);text-shadow: 0 2px 4px rgba(0,0,0,0.1);transition: all var(--anim-duration) var(--anim-easing);position: relative;z-index: 1;}/* 学号显示样式 */.student-id {font-size: var(--number-size);font-weight: bold;color: #2c3e50;line-height: 1;font-family: 'Arial'; /* 固定为Arial,不再可选 */transition: all var(--anim-duration) var(--anim-easing);position: relative;z-index: 1;}/* 连抽结果显示区域 */.multi-draw-display {margin-top: 20px;max-height: 200px;overflow-y: auto;width: 100%;}/* 连抽结果网格布局 */.multi-draw-list {display: grid;grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));gap: 10px;padding: 10px;}/* 连抽结果单项样式 */.multi-draw-item {background: rgba(67, 97, 238, 0.1);border-radius: 8px;padding: 10px;text-align: center;box-shadow: 0 2px 4px rgba(0,0,0,0.1);transition: transform 0.2s;}.multi-draw-item:hover {transform: translateY(-2px);}/* 连抽结果学号 */.multi-draw-item-id {font-weight: bold;color: var(--primary-color);font-size: 1.2rem;}/* 连抽结果姓名 */.multi-draw-item-name {color: var(--text-dark);font-size: var(--name-size);font-weight: bold;margin-top: 5px;}.dark-mode .multi-draw-item {background: rgba(94, 114, 228, 0.2);}/* 主题切换按钮 */.theme-toggle {position: absolute;top: 20px;right: 20px;background: none;border: none;cursor: pointer;font-size: 1.5rem;color: var(--text-dark);transition: color 0.3s ease;z-index: 10;}.dark-mode .theme-toggle {color: var(--dark-text-light);}/* 按钮组布局 */.button-group {display: flex;gap: 1rem;justify-content: center;margin-top: 2rem;flex-wrap: wrap;}/* 通用按钮样式 */button {font-size: 1.1rem;padding: 0.8rem 1.8rem;border-radius: 8px;cursor: pointer;transition: all 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28);border: none;font-weight: 600;display: inline-flex;align-items: center;gap: 0.5rem;}button i {margin-right: 8px;}/* 主要按钮样式 */button.primary {background: var(--primary-color);color: white;box-shadow: 0 4px 6px rgba(67, 97, 238, 0.2);}button.primary:hover {background: #3a56d4;transform: translateY(-2px);box-shadow: 0 6px 8px rgba(67, 97, 238, 0.3);}/* 次要按钮样式 */button.secondary {background: white;color: var(--text-dark);border: 2px solid var(--border-color);}button.secondary:hover {color: var(--primary-color);border-color: var(--primary-color);transform: translateY(-2px);}/* 危险按钮样式 */button.danger {background: var(--danger-color);color: white;box-shadow: 0 4px 6px rgba(247, 37, 133, 0.2);}button.danger:hover {background: #e5177b;transform: translateY(-2px);box-shadow: 0 6px 8px rgba(247, 37, 133, 0.3);}/* 警告按钮样式 */button.warning {background: var(--warning-color);color: white;box-shadow: 0 4px 6px rgba(248, 150, 30, 0.2);}button.warning:hover {background: #e07e0f;transform: translateY(-2px);box-shadow: 0 6px 8px rgba(248, 150, 30, 0.3);}.dark-mode button.secondary {background: var(--dark-card-bg);color: var(--dark-text-light);border: 2px solid var(--dark-border-color);}/* 操作模式切换区域 */.mode-switch {display: flex;justify-content: center;margin: 2rem 0;gap: 1rem;}/* 模式切换按钮 */.mode-button {flex: 1;max-width: 200px;padding: 1.5rem 1rem;border-radius: 8px;cursor: pointer;transition: var(--transition);border: 2px solid var(--border-color);background-color: white;}.dark-mode .mode-button {background-color: var(--dark-card-bg);border-color: var(--dark-border-color);}.mode-button.active {border-color: var(--primary-color);background-color: rgba(67, 97, 238, 0.1);transform: translateY(-5px);box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);}.dark-mode .mode-button.active {background-color: rgba(94, 114, 228, 0.2);}/* 模式图标 */.mode-icon {font-size: 2rem;margin-bottom: 0.5rem;color: var(--primary-color);}/* 学生名单管理面板 */.student-list-panel {padding: 1.5rem;background: #f0f5ff;border-radius: 8px;margin-top: 1rem;max-height: 400px;overflow-y: auto;box-shadow: 0 4px 6px rgba(0,0,0,0.05);}/* 分组选择器 */.group-selector {margin: 1rem 0;}/* 分组标签页 */.group-tabs {display: flex;gap: 0.5rem;margin-bottom: 1rem;flex-wrap: wrap;}.group-tab {padding: 0.5rem 1rem;border-radius: 20px;cursor: pointer;background: #e9ecef;transition: var(--transition);display: flex;align-items: center;gap: 0.5rem;}.group-tab.active {background: var(--primary-color);color: white;}.dark-mode .group-tab {background: var(--dark-border-color);color: var(--dark-text-light);}.dark-mode .group-tab.active {background: var(--dark-primary-color);}/* 删除分组按钮 */.delete-group-btn {background: none;border: none;color: #dc3545;cursor: pointer;font-size: 0.8rem;opacity: 0;transition: opacity 0.2s;padding: 0;}.delete-group-btn:hover {color: #c82333;}.group-tab:hover .delete-group-btn {opacity: 1;}/* 文件上传区域 */.file-upload {margin: 1.5rem 0;border: 2px dashed var(--border-color);border-radius: 8px;padding: 2rem;transition: var(--transition);cursor: pointer;text-align: center;}.file-upload:hover, .file-upload.drag-over {border-color: var(--primary-color);background-color: rgba(67, 97, 238, 0.05);}.file-upload.drag-over {border-style: solid;background-color: rgba(67, 97, 238, 0.1);}.file-upload input {display: none;}/* 上传图标 */.upload-icon {font-size: 3rem;color: var(--text-light);margin-bottom: 1rem;transition: color 0.3s ease;}.file-upload:hover .upload-icon, .file-upload.drag-over .upload-icon {color: var(--primary-color);}/* 拖拽提示 */.drag-hint {margin-top: 1rem;color: var(--text-muted);font-size: 0.9rem;}/* 学生列表表格 */.student-list {width: 100%;border-collapse: collapse;margin-top: 1rem;background-color: white;border-radius: 8px;overflow: hidden;box-shadow: 0 2px 4px rgba(0,0,0,0.05);}.dark-mode .student-list {background-color: var(--dark-card-bg);}.student-list th, .student-list td {padding: 0.8rem;text-align: left;border-bottom: 1px solid var(--border-color);}.dark-mode .student-list th, .dark-mode .student-list td {border-bottom-color: var(--dark-border-color);}.student-list th {background-color: rgba(67, 97, 238, 0.1);font-weight: bold;}.dark-mode .student-list th {background-color: rgba(94, 114, 228, 0.2);}/* 批量抽取面板 */.batch-panel {padding: 1rem;background: #f0f5ff;border-radius: 8px;margin-top: 1rem;display: none;animation: fadeIn 0.3s;transition: background 0.3s ease;}.batch-panel.show {display: block;}/* 表单组布局 */.form-group {margin: 1.5rem 0;display: flex;align-items: center;justify-content: center;}/* 表单标签 */label {min-width: 120px;text-align: right;margin-right: 1rem;color: var(--text-dark);font-size: 1.1rem;font-weight: 500;transition: color 0.3s ease;}.dark-mode label {color: var(--dark-text-light);}/* 数字输入框和下拉框 */input[type="number"], select {border: 1px solid transparent;border-radius: 8px;padding: 0.8rem 1rem;width: 160px;transition: var(--transition);font-size: 1.1rem;color: var(--text-dark);font-weight: 500;outline: none;background: linear-gradient(white, white) padding-box, linear-gradient(45deg, #ff7eb3, #65d9ff, #c7f464, #ff7eb3) border-box;}input[type="number"]:focus {background: linear-gradient(white, white) padding-box, linear-gradient(45deg, #ff0076, #1eaeff, #28ffbf, #ff0076) border-box;box-shadow: 0 0 15px rgba(255, 0, 118, 0.7), 0 0 25px rgba(30, 174, 255, 0.7);color: #000;}/* 下拉框自定义样式 */select {appearance: none;background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%238d99ae'%3e%3cpath d='M7 10l5 5 5-5z'/%3e%3c/svg%3e");background-repeat: no-repeat;background-position: right 12px center;background-size: 16px;padding-right: 2.5rem;}/* 历史记录区域 */.history {margin-top: 2rem;padding: 1rem;background: #f8f9fa;border-radius: 8px;max-height: 150px;overflow-y: auto;transition: background 0.3s ease;}/* 历史记录标题 */.history-title {display: flex;justify-content: space-between;align-items: center;margin-bottom: 0.5rem;font-size: 0.9rem;color: var(--text-light);transition: color 0.3s ease;}/* 历史记录项容器 */.history-items {display: flex;flex-wrap: wrap;gap: 0.5rem;justify-content: center;}/* 历史记录单项 */.history-item {background: white;padding: 0.5rem 1rem;border-radius: 20px;font-size: 0.9rem;box-shadow: 0 1px 3px rgba(0,0,0,0.1);transition: all 0.3s ease;display: flex;align-items: center;}/* 历史记录学号 */.history-item .student-id {font-weight: bold;margin-right: 5px;font-size: 0.9rem;}/* 历史记录姓名 */.history-item .student-name {font-size: 0.9rem;color: var(--text-dark);font-weight: bold;}/* 最新记录高亮 */.history-item.latest {background: var(--success-color);color: white !important;transform: scale(1.1);animation: pulse 1s infinite alternate;}/* 脉冲动画 */@keyframes pulse {from { transform: scale(1); }to { transform: scale(1.1); }}/* 高级设置区域 */.advanced-settings {margin-top: 2rem;}/* 展开/收起高级设置按钮 */.toggle-advanced {background: none;border: none;color: var(--primary-color);cursor: pointer;font-size: 0.9rem;display: flex;align-items: center;gap: 0.5rem;margin: 0 auto;padding: 0.5rem;transition: color 0.3s ease;}.dark-mode .toggle-advanced {color: var(--dark-primary-color);}/* 高级设置面板 */.settings-panel {padding: 0 1.5rem;background: #f5f7fa;border-radius: 8px;margin-top: 0.5rem;text-align: left;max-height: 0;overflow: hidden;opacity: 0;transition: all 0.3s ease;}.settings-panel.show {max-height: 1000px;opacity: 1;padding: 1.5rem;}/* 概率范围控制 */.range-control {display: flex;align-items: center;gap: 0.5rem;margin-bottom: 1rem;flex-wrap: wrap;}.range-control input {flex: 1;min-width: 80px;}/* 通知消息样式 */.notification {position: fixed;top: 20px;right: 20px;padding: 1rem 1.5rem;border-radius: 8px;background-color: white;box-shadow: 0 4px 12px rgba(0,0,0,0.15);z-index: 9999;transform: translateX(120%);transition: transform 0.3s ease;display: flex;align-items: center;gap: 0.5rem;}.dark-mode .notification {background-color: var(--dark-card-bg);box-shadow: 0 4px 12px rgba(0,0,0,0.3);}.notification.show {transform: translateX(0);}/* 通知图标 */.notification-icon {font-size: 1.5rem;}/* 成功通知 */.notification.success {border-left: 4px solid var(--success-color);}/* 错误通知 */.notification.error {border-left: 4px solid var(--danger-color);}/* 成功通知图标颜色 */.notification.success .notification-icon {color: var(--success-color);}/* 错误通知图标颜色 */.notification.error .notification-icon {color: var(--danger-color);}/* 连抽/批量抽取全屏效果 */.multi-draw-effect {position: fixed;top: 0;left: 0;width: 100%;height: 100%;display: flex;flex-direction: column;justify-content: center;align-items: center;background: rgba(0,0,0,0.85);z-index: 1000;animation: fadeIn 0.3s;}/* 全屏效果中的数字 */.effect-number {font-size: 120px;font-weight: bold;color: white;text-shadow: 0 0 20px #ff0076;animation: zoomInOut 0.8s infinite alternate;margin-bottom: 20px;}/* 全屏效果中的姓名 */.effect-name {font-size: var(--name-size);color: white;text-shadow: 0 0 15px #4cc9f0;margin-top: 10px;font-weight: bold;}/* 全屏效果中的结果列表 */.effect-list {display: flex;flex-wrap: wrap;justify-content: center;gap: 20px;margin-top: 30px;max-width: 80%;}/* 全屏效果中的结果项 */.effect-item {background: rgba(255,255,255,0.1);backdrop-filter: blur(10px);border-radius: 15px;padding: 15px 25px;display: flex;flex-direction: column;align-items: center;box-shadow: 0 8px 32px rgba(0,0,0,0.3);border: 1px solid rgba(255,255,255,0.2);}/* 全屏效果中的学号 */.effect-item-id {font-size: 36px;font-weight: bold;color: var(--success-color);}/* 全屏效果中的姓名 */.effect-item-name {font-size: var(--name-size);color: white;margin-top: 5px;font-weight: bold;}/* 庆祝动画容器 */.celebration {position: fixed;top: 0;left: 0;width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;z-index: 100;pointer-events: none;}/* 彩带粒子 */.confetti {position: absolute;width: 10px;height: 10px;background: var(--danger-color);opacity: 0;will-change: transform, opacity;}/* 彩带飘落动画 */@keyframes confetti-fall {0% { transform: translateY(-100vh) rotate(0deg); opacity: 1; }100% { transform: translateY(100vh) rotate(360deg); opacity: 0; }}/* 庆祝消息 */.message {font-size: 2rem;color: var(--danger-color);text-shadow: 0 2px 4px rgba(0,0,0,0.1);background: white;padding: 1rem 2rem;border-radius: 8px;box-shadow: 0 10px 20px rgba(0,0,0,0.1);z-index: 101;animation: zoomIn 0.5s var(--anim-easing);}.dark-mode .message {background: var(--dark-card-bg);color: var(--dark-danger-color);box-shadow: 0 10px 20px rgba(0,0,0,0.3);}/* 消息弹出动画 */@keyframes zoomIn {from { transform: scale(0.5); opacity: 0; }to { transform: scale(1); opacity: 1; }}/* 淡入动画 */@keyframes fadeIn {from { opacity: 0; }to { opacity: 1; }}/* 缩放进出动画 */@keyframes zoomInOut {0% { transform: scale(1); }100% { transform: scale(1.2); }}/* 翻转动画 */@keyframes flip-enter {from { transform: rotateY(-180deg); opacity: 0; }to { transform: rotateY(0); opacity: 1; }}@keyframes flip-leave {from { transform: rotateY(0); opacity: 1; }to { transform: rotateY(180deg); opacity: 0; }}/* 翻转过渡类 */.flip-enter-active {animation: flip-enter 0.6s cubic-bezier(0.25, 0.8, 0.25, 1);transform-origin: center;backface-visibility: hidden;}.flip-leave-active {animation: flip-leave 0.6s cubic-bezier(0.25, 0.8, 0.25, 1);transform-origin: center;backface-visibility: hidden;}/* 单次抽取动画 - 翻转 */@keyframes flip {0% { transform: perspective(400px) rotateX(0); opacity: 1; }40% { transform: perspective(400px) rotateX(90deg); opacity: 0; }60% { transform: perspective(400px) rotateX(-90deg); opacity: 0; }100% { transform: perspective(400px) rotateX(0); opacity: 1; }}/* 单次抽取动画 - 突然放大 */@keyframes popIn {0% { transform: scale(0.5); opacity: 0; }50% { transform: scale(1.2); opacity: 1; }100% { transform: scale(1); opacity: 1; }}/* 连抽/批量抽取动画 - 滚动 */@keyframes rollIn {0% { transform: translateX(100%); opacity: 0; }100% { transform: translateX(0); opacity: 1; }}/* 响应式设计 */@media (max-width: 600px) {.container {padding: 1.5rem;width: 95%;}.form-group {flex-direction: column;align-items: flex-start;}label {text-align: left;margin-bottom: 0.5rem;min-width: auto;}:root {--name-size: 2.8rem;--number-size: 60pt;}.button-group {flex-direction: column;}button {width: 100%;}input[type="number"], select {width: 100%;}.range-control {flex-direction: column;align-items: flex-start;}.effect-number {font-size: 80px;}.effect-name {font-size: var(--name-size);}.effect-item-id {font-size: 28px;}.effect-item-name {font-size: var(--name-size);}.multi-draw-list {grid-template-columns: 1fr;}}</style>
</head>
<body><div id="app" class="container"><!-- 主题切换按钮 --><button class="theme-toggle" @click="toggleDarkMode"><i :class="darkMode ? 'fas fa-sun' : 'fas fa-moon'"></i></button><!-- 页面主内容,包含设置页和主功能页的切换,增加了翻转特效 --><transition name="flip" mode="out-in"><!-- 设置页面 --><div v-if="isSetupPage" key="setup"><h1><i class="fas fa-users-gear"></i> 学号抽取设置</h1><!-- 操作模式切换 --><div class="mode-switch"><div class="mode-button" :class="{ 'active': operationMode === 'range' }" @click="operationMode = 'range'"><div class="mode-icon"><i class="fas fa-sort-numeric-up"></i></div><div class="mode-name">学号范围模式</div></div><div class="mode-button" :class="{ 'active': operationMode === 'list' }" @click="operationMode = 'list'"><div class="mode-icon"><i class="fas fa-list-ol"></i></div><div class="mode-name">名单导入模式</div></div></div><!-- 学号范围模式下的输入框 --><div v-if="operationMode === 'range'"><div class="form-group"><label for="start">起始学号:</label><input type="number" v-model.number="start" id="start" min="1"></div><div class="form-group"><label for="end">结束学号:</label><input type="number" v-model.number="end" id="end" :min="start"></div></div><!-- 名单导入模式下的学生管理面板 --><div v-if="operationMode === 'list'"><div class="student-list-panel"><h3><i class="fas fa-users"></i> 学生名单管理</h3><!-- 分组标签页 --><div class="group-selector"><label>名单分组:</label><div class="group-tabs"><div v-for="(group, index) in studentGroups" :key="index" class="group-tab" :class="{ 'active': currentGroup === group.name }"@click="switchGroup(group.name)">{{ group.name }} ({{ group.students.length || 0 }})<!-- 删除分组按钮 --><button class="delete-group-btn" @click.stop="deleteGroup(group.name, index)"@click.prevent><i class="fas fa-times"></i></button></div><button class="primary" @click="addNewGroup" style="padding: 0.5rem 1rem;"><i class="fas fa-plus"></i> 新建组</button></div></div><!-- 文件上传区域 --><div class="file-upload" @click="selectFile"@dragenter.prevent="handleDragEnter"@dragover.prevent="handleDragOver"@dragleave.prevent="handleDragLeave"@drop.prevent="handleDrop":class="{ 'drag-over': isDragOver }"><input type="file" id="fileInput" accept=".xlsx,.xls" @change="handleFileUpload"><div class="upload-icon"><i class="fas fa-file-excel"></i></div><h3>点击或拖拽Excel文件到此处上传</h3><p>支持 .xlsx 和 .xls 格式,A列学号,B列姓名</p><p class="drag-hint" v-if="isDragOver">释放鼠标即可上传文件</p></div><!-- 显示当前组的学生列表 --><div v-if="currentStudents.length > 0"><div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"><p>共 {{ currentStudents.length }} 名学生</p><button class="danger" @click="clearCurrentGroup" style="padding: 0.5rem 1rem; font-size: 0.9rem;"><i class="fas fa-trash"></i> 清空当前组</button></div><table class="student-list"><thead><tr><th style="width: 40%;">学号</th><th style="width: 40%;">姓名</th><th style="width: 20%;">操作</th></tr></thead><tbody><tr v-for="(student, index) in currentStudents" :key="index"><td>{{ student.student_id }}</td><td>{{ student.name }}</td><td><button class="danger" @click="deleteStudent(index)" style="padding: 0.2rem 0.5rem; font-size: 0.8rem;"><i class="fas fa-times"></i></button></td></tr></tbody></table></div><div v-else class="empty-state"><p>暂无学生数据,请上传Excel文件</p></div></div></div><!-- 抽取模式选择 --><div class="form-group"><label for="mode">抽取模式:</label><select v-model="mode" id="mode"><option value="d">单次抽取模式</option><option value="s">快速抽取模式</option></select></div><!-- 不重复抽取选项 --><div class="form-group"><label></label><div style="display: flex; align-items: center;"><input type="checkbox" id="noRepeat" v-model="noRepeat" style="margin-right: 0.5rem;"><label for="noRepeat" style="margin: 0;">不重复抽取</label></div></div><!-- 开始抽取按钮 --><div class="button-group"><button class="primary" @click="validateAndNavigate":disabled="operationMode === 'list' && currentStudents.length === 0"><i class="fas fa-play"></i><span>开始抽取</span></button></div><!-- 高级设置区域 --><div class="advanced-settings"><button class="toggle-advanced" @click="showAdvanced = !showAdvanced"><i :class="['fas', showAdvanced ? 'fa-chevron-up' : 'fa-chevron-down']"></i>{{ showAdvanced ? '隐藏高级设置' : '显示高级设置' }}</button><!-- 高级设置面板 --><div class="settings-panel" :class="{ 'show': showAdvanced }"><h3><i class="fas fa-percentage"></i> 概率设置</h3><p style="color: var(--text-light); font-size: 0.9rem; margin-bottom: 1rem;">设置不同学号范围的抽取概率权重(默认均匀分布)</p><!-- 概率权重范围输入 --><div v-for="(range, index) in probabilityRanges" :key="index" class="range-control"><input v-model.number="range.start" type="number" placeholder="起始" :min="start" :max="range.end"><span></span><input v-model.number="range.end" type="number" placeholder="结束" :min="range.start" :max="end"><span>权重</span><input v-model.number="range.weight" type="number" placeholder="权重" min="1"><span>%</span><button class="danger" @click="removeRange(index)" style="padding: 0.5rem;"><i class="fas fa-trash"></i></button></div><button class="primary" @click="addRange" style="margin-bottom: 1.5rem;"><i class="fas fa-plus"></i> 添加范围</button><h3><i class="fas fa-text-height"></i> 字号设置</h3><!-- 姓名字号调节滑块 --><div class="form-group"><label>姓名字号:{{ nameSize }}rem</label><input type="range" v-model.number="nameSize" min="2" max="15" step="0.1"></div><!-- 学号大小调节滑块 --><div class="form-group"><label>学号大小:{{ numberSize }}pt</label><input type="range" v-model.number="numberSize" min="60" max="120" step="5"></div><button @click="applyFontSettings" class="primary" style="width: 100%;"><i class="fas fa-check"></i> 应用字号设置</button></div></div></div><!-- 主功能页面 --><div v-else key="main"><h1><i class="fas fa-random"></i> 学号抽取结果</h1><!-- 结果显示区域 --><div class="display-area"><!-- 姓名显示,仅在名单模式下且有姓名时显示 --><div class="student-name" v-if="operationMode === 'list' && currentStudent.name" :class="currentAnimationClass">{{ currentStudent.name }}</div><!-- 学号显示 --><div class="student-id" :class="currentAnimationClass">{{ currentNumber }}</div><!-- 连抽结果显示区域 --><div class="multi-draw-display" v-if="multiDrawResults.length > 0"><div class="multi-draw-list"><div v-for="(item, index) in multiDrawResults" :key="index" class="multi-draw-item"><div class="multi-draw-item-id">{{ item.student_id }}</div><div class="multi-draw-item-name" v-if="operationMode === 'list' && item.name">{{ item.name }}</div></div></div></div></div><!-- 操作按钮组 --><div class="button-group"><!-- 单次抽取按钮 --><button v-if="mode === 'd'" class="primary" @click="drawNumberWithAnimation"><i class="fas fa-dice"></i><span>抽取学号</span></button><!-- 快速抽取按钮 --><button v-if="mode === 's'" class="primary":class="{ 'loading-button': isContinuous }"@click="toggleContinuous"><i :class="['fas', isContinuous ? 'fa-stop' : 'fa-play']"></i><span>{{ isContinuous ? '停止抽取' : '开始抽取' }}</span></button><!-- 连抽按钮 --><button class="primary" @click="multiDrawWithAnimation"><i class="fas fa-bolt"></i><span>连抽{{ multiDrawCount }}次</span></button><!-- 批量抽取按钮 --><button class="warning" @click="showBatchSettings = !showBatchSettings"><i class="fas fa-users"></i><span>批量抽取</span></button><!-- 返回设置按钮 --><button class="secondary" @click="goBack"><i class="fas fa-arrow-left"></i><span>返回设置</span></button><!-- 重置记录按钮 --><button v-if="usedNumbers.length > 0" class="danger" @click="resetUsedNumbers"><i class="fas fa-sync-alt"></i><span>重置记录</span></button></div><!-- 批量抽取设置面板 --><transition name="fade"><div v-if="showBatchSettings" class="batch-panel show"><h3><i class="fas fa-users"></i> 批量抽取设置</h3><div class="form-group"><label for="customMultiCount">连抽次数:</label><input type="number" v-model.number="multiDrawCount" id="customMultiCount" min="1" max="50"></div><div class="form-group"><label for="batchSize">抽取人数:</label><input type="number" v-model.number="batchSize" id="batchSize" min="1" :max="getTotalNumbers() - usedNumbers.length"></div><button class="primary" @click="drawBatchNumbersWithAnimation"><i class="fas fa-user-friends"></i><span>抽取{{batchSize}}人</span></button></div></transition><!-- 历史记录区域 --><div v-if="usedNumbers.length > 0" class="history"><div class="history-title"><span>已抽取记录 ({{ usedNumbers.length }}/{{ getTotalNumbers() }})</span><button @click="clearHistory" style="background:none;border:none;color:var(--text-light);font-size:0.8rem;"><i class="fas fa-trash"></i> 清除</button></div><div class="history-items"><span v-for="(item, index) in usedNumbers" :key="index" class="history-item":class="{ 'latest': index === usedNumbers.length - 1 }"><span class="student-id">{{ item.student_id }}</span><span class="student-name" v-if="operationMode === 'list' && item.name">{{ item.name }}</span></span></div></div></div></transition><!-- 全屏抽取效果,用于连抽和批量抽取 --><div class="multi-draw-effect" v-if="showMultiEffect"><div class="effect-number">{{ multiEffectNumber }}</div><div class="effect-name" v-if="operationMode === 'list' && multiEffectName">{{ multiEffectName }}</div><div class="effect-list" v-if="multiDrawResults.length > 0"><div v-for="(item, index) in multiDrawResults" :key="index" class="effect-item"><div class="effect-item-id">{{ item.student_id }}</div><div class="effect-item-name" v-if="operationMode === 'list' && item.name">{{ item.name }}</div></div></div></div><!-- 庆祝动画,当所有学号被抽完时触发 --><div v-if="showCelebration" class="celebration"><div v-for="n in 50" :key="n" class="confetti":style="{left: Math.random() * 100 + '%',background: getRandomColor(),animation: `confetti-fall ${Math.random() * 3 + 2}s linear forwards`,animationDelay: Math.random() * 0.5 + 's',width: Math.random() * 10 + 5 + 'px',height: Math.random() * 10 + 5 + 'px'}"></div><div class="message"><i class="fas fa-trophy"></i> {{celebrationMessage}}</div></div><!-- 通知消息 --><div class="notification" :class="notification.type" :class="{ 'show': notification.show }"><div class="notification-icon"><i :class="notification.icon"></i></div><div class="notification-content">{{ notification.message }}</div></div></div><!-- 引入Vue 3和XLSX库 --><script src="https://cdn.jsdelivr.net/npm/vue@3.2.47/dist/vue.global.min.js"></script><script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script><script>const { createApp, ref, computed, onMounted, watch } = Vue;createApp({setup() {// --- 状态变量 ---const isDragOver = ref(false); // 拖拽状态const isSetupPage = ref(true); // 当前是否为设置页面const darkMode = ref(false); // 深色模式状态const showAdvanced = ref(false); // 高级设置面板是否展开const showBatchSettings = ref(false); // 批量抽取设置面板是否显示const showMultiEffect = ref(false); // 全屏抽取效果是否显示const showCelebration = ref(false); // 庆祝动画是否显示const isContinuous = ref(false); // 快速抽取模式是否正在运行const continuousIntervalId = ref(null); // 快速抽取的定时器IDconst currentAnimationClass = ref(''); // 当前应用的动画类名// --- 核心数据 ---const operationMode = ref('range'); // 操作模式: 'range' | 'list'const start = ref(1); // 起始学号const end = ref(40); // 结束学号const mode = ref('d'); // 抽取模式: 'd' (单次) | 's' (快速)const noRepeat = ref(true); // 是否不重复抽取const currentNumber = ref('—'); // 当前显示的学号const currentStudent = ref({ student_id: '', name: '' }); // 当前显示的学生const usedNumbers = ref([]); // 已抽取的学号历史记录const multiDrawResults = ref([]); // 连抽/批量抽取的中间结果const studentGroups = ref([]); // 存储所有学生分组const currentGroup = ref('默认组'); // 当前选中的分组名const probabilityRanges = ref([]); // 概率权重范围数组const celebrationMessage = ref('所有学号已抽取完成!'); // 庆祝消息const notification = ref({ // 通知消息对象show: false,message: '',type: '',icon: ''});const multiEffectNumber = ref(0); // 全屏效果中显示的学号const multiEffectName = ref(''); // 全屏效果中显示的姓名// --- 用户界面设置 ---const nameSize = ref(5); // 姓名字号 (rem)const numberSize = ref(80); // 学号大小 (pt)const multiDrawCount = ref(5); // 连抽次数const batchSize = ref(1); // 批量抽取人数// --- 计算属性 ---// 计算当前分组的学生列表const currentStudents = computed(() => {const group = studentGroups.value.find(g => g.name === currentGroup.value);return group ? group.students : [];});// 计算总共有多少个可抽取的学号const getTotalNumbers = () => {if (operationMode.value === 'list') {return currentStudents.value.length;} else {return end.value - start.value + 1;}};// 获取所有可用的学号(包括学号和姓名)const getAllNumbers = () => {if (operationMode.value === 'list') {return [...currentStudents.value];} else {const numbers = [];for (let i = start.value; i <= end.value; i++) {numbers.push({ student_id: i, name: '' });}return numbers;}};// --- URL参数处理 ---// 从URL参数中解析初始设置const parseUrlParams = () => {const params = new URLSearchParams(window.location.search);return {start: params.has('start') ? parseInt(params.get('start')) : 1,end: params.has('end') ? parseInt(params.get('end')) : 40,mode: params.get('mode') || 'd',noRepeat: params.has('noRepeat') ? params.get('noRepeat') === 'true' : true,operationMode: params.get('operationMode') || 'range',nameSize: params.has('nameSize') ? parseFloat(params.get('nameSize')) : 5,numberSize: params.has('numberSize') ? parseInt(params.get('numberSize')) : 80,multiDrawCount: params.has('multiDrawCount') ? parseInt(params.get('multiDrawCount')) : 5,batchSize: params.has('batchSize') ? parseInt(params.get('batchSize')) : 1};};// 将当前设置同步到URL参数const updateUrlParams = () => {const params = new URLSearchParams();params.set('start', start.value);params.set('end', end.value);params.set('mode', mode.value);params.set('noRepeat', noRepeat.value);params.set('operationMode', operationMode.value);params.set('nameSize', nameSize.value);params.set('numberSize', numberSize.value);params.set('multiDrawCount', multiDrawCount.value);params.set('batchSize', batchSize.value);const newUrl = window.location.pathname + '?' + params.toString();window.history.replaceState(null, '', newUrl);};// --- 通知系统 ---// 显示一个通知消息const showNotification = (message, type = 'success') => {notification.value.message = message;notification.value.type = type;notification.value.icon = type === 'success' ? 'fas fa-check-circle' : 'fas fa-exclamation-circle';notification.value.show = true;setTimeout(() => {notification.value.show = false;}, 3000);};// --- 主题切换 ---// 切换深色/日间模式const toggleDarkMode = () => {darkMode.value = !darkMode.value;document.body.classList.toggle('dark-mode', darkMode.value);localStorage.setItem('darkMode', darkMode.value);};// --- 文件上传处理 ---// 拖拽进入const handleDragEnter = () => {isDragOver.value = true;};// 拖拽悬停const handleDragOver = () => {isDragOver.value = true;};// 拖拽离开const handleDragLeave = () => {isDragOver.value = false;};// 处理文件拖放const handleDrop = (e) => {isDragOver.value = false;const files = e.dataTransfer.files;if (files.length > 0) {const file = files[0];if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) {handleFileData(file);} else {showNotification('请上传Excel文件(.xlsx或.xls)', 'error');}}};// --- 随机数与概率 ---// 获取随机颜色,用于庆祝动画const getRandomColor = () => {const colors = ['#4361ee', '#4cc9f0', '#f72585', '#f8961e', '#7209b7', '#3a86ff'];return colors[Math.floor(Math.random() * colors.length)];};// 根据权重获取一个随机学号const getWeightedRandomNumber = () => {const available = getAllNumbers().filter(item => !usedNumbers.value.some(used => used.student_id === item.student_id));if (available.length === 0) return null;// 如果没有设置概率范围,或处于名单模式,则使用均匀分布if (probabilityRanges.value.length === 0 || operationMode.value === 'list') {const index = Math.floor(Math.random() * available.length);return available[index];}// 根据权重构建抽取池let pool = [];let totalWeight = 0;probabilityRanges.value.forEach(range => {const nums = getAllNumbers().filter(n => n.student_id >= range.start && n.student_id <= range.end).filter(n => !usedNumbers.value.some(u => u.student_id === n.student_id));nums.forEach(n => {pool.push({ num: n, weight: range.weight });totalWeight += range.weight;});});if (pool.length === 0) return null;// 进行加权随机抽取let random = Math.random() * totalWeight;for (const item of pool) {if (random < item.weight) return item.num;random -= item.weight;}return pool[0].num;};// --- 抽取动画 ---// 应用动画并执行回调const applyAnimation = (animationClass, callback) => {currentAnimationClass.value = animationClass;// 关键修改:立即执行回调函数,而不是等待动画结束if (callback) callback();// 但仍然在动画时间后移除动画类,以完成动画循环setTimeout(() => {currentAnimationClass.value = '';}, 500); // 动画持续时间};// --- 单次抽取 ---// 带有动画的单次抽取const drawNumberWithAnimation = () => {multiDrawResults.value = [];const available = getAllNumbers().filter(item => !usedNumbers.value.some(used => used.student_id === item.student_id));if (available.length === 0) {triggerCelebration();return;}// 随机选择一个动画const animations = ['flip', 'popIn'];const randomAnimation = animations[Math.floor(Math.random() * animations.length)];// 应用动画并立即执行抽取逻辑applyAnimation(randomAnimation, drawNumber);};// 执行单次抽取逻辑const drawNumber = () => {const result = getWeightedRandomNumber();if (result !== null) {if (noRepeat.value) {const exists = usedNumbers.value.some(item => item.student_id === result.student_id);if (!exists) {usedNumbers.value.push(result);}}currentNumber.value = result.student_id;currentStudent.value = result;multiDrawResults.value = [result];if (usedNumbers.value.length === getTotalNumbers()) {triggerCelebration();}} else {triggerCelebration();}};// --- 快速抽取 ---// 切换快速抽取的开始/停止const toggleContinuous = () => {if (isContinuous.value) {// 当前正在快速抽取,需要停止clearInterval(continuousIntervalId.value);isContinuous.value = false;// 关键修复:停止时,将最终显示的学号作为一次“单次抽取”来处理// 这确保了它会遵循“不重复”规则,并被正确记录drawNumber(); // 复用单次抽取的核心逻辑} else {// 当前未抽取,开始快速抽取isContinuous.value = true;// 使用 setInterval 代替 requestAnimationFrame,实现高速滚动continuousIntervalId.value = setInterval(() => {const available = getAllNumbers().filter(item => !usedNumbers.value.some(used => used.student_id === item.student_id));if (available.length === 0) {clearInterval(continuousIntervalId.value);isContinuous.value = false;triggerCelebration();return;}const result = getWeightedRandomNumber();if (result) {currentNumber.value = result.student_id;currentStudent.value = result;multiDrawResults.value = [result];}}, 30); // 30ms 的间隔,速度非常快}};// --- 连抽 ---// 带有动画的连抽const multiDrawWithAnimation = () => {const available = getAllNumbers().filter(item => !usedNumbers.value.some(used => used.student_id === item.student_id));const drawCount = Math.min(multiDrawCount.value, available.length);if (drawCount === 0) {showNotification('没有可抽取的学号了!', 'error');return;}// 对于连抽,使用 rollIn 动画applyAnimation('rollIn', multiDraw);};// 执行连抽逻辑const multiDraw = () => {const available = getAllNumbers().filter(item => !usedNumbers.value.some(used => used.student_id === item.student_id));const drawCount = Math.min(multiDrawCount.value, available.length);if (drawCount === 0) return;multiDrawResults.value = [];showMultiEffect.value = true;let count = 0;const interval = setInterval(() => {if (count >= drawCount) {clearInterval(interval);setTimeout(() => {showMultiEffect.value = false;if (multiDrawResults.value.length > 0) {// 修复:始终只将最后一个结果作为“当前”结果const lastResult = multiDrawResults.value[multiDrawResults.value.length - 1];currentNumber.value = lastResult.student_id;currentStudent.value = lastResult;// 如果开启了不重复,则将所有结果都加入历史记录if (noRepeat.value) {multiDrawResults.value.forEach(result => {const exists = usedNumbers.value.some(item => item.student_id === result.student_id);if (!exists) {usedNumbers.value.push(result);}});}if (usedNumbers.value.length === getTotalNumbers()) {triggerCelebration();}}}, 1000);return;}// 关键修复:在抽取循环内,确保本次抽取的结果不重复let result;let attempts = 0; // 防止无限循环do {result = getWeightedRandomNumber();attempts++;// 如果没有可用学号或尝试次数过多,则跳出if (!result || attempts > 100) break;} while (result && multiDrawResults.value.some(item => item.student_id === result.student_id));if (result) {multiDrawResults.value.push(result);multiEffectNumber.value = result.student_id;multiEffectName.value = result.name || '';count++;}}, 300);};// --- 批量抽取 ---// 带有动画的批量抽取const drawBatchNumbersWithAnimation = () => {const available = getAllNumbers().filter(item => !usedNumbers.value.some(used => used.student_id === item.student_id));const actualSize = Math.min(batchSize.value, available.length);if (actualSize === 0) {showNotification('没有可抽取的学号了!', 'error');return;}// 对于批量抽取,也使用 rollIn 动画applyAnimation('rollIn', drawBatchNumbers);};// 执行批量抽取逻辑const drawBatchNumbers = () => {const available = getAllNumbers().filter(item => !usedNumbers.value.some(used => used.student_id === item.student_id));const actualSize = Math.min(batchSize.value, available.length);if (actualSize === 0) return;const batch = [];for (let i = 0; i < actualSize; i++) {const result = getWeightedRandomNumber();if (result) {// 确保批次内不重复if (!batch.some(item => item.student_id === result.student_id)) {batch.push(result);// 如果开启了不重复,且该学号不在历史记录中,则加入if (noRepeat.value) {const exists = usedNumbers.value.some(item => item.student_id === result.student_id);if (!exists) {usedNumbers.value.push(result);}}}}}if (batch.length > 0) {multiDrawResults.value = batch;// 修复:始终只将最后一个结果作为“当前”结果const lastResult = batch[batch.length - 1];currentNumber.value = lastResult.student_id;currentStudent.value = lastResult;if (batch.length > 3) {triggerCelebration(`成功抽取${batch.length}人!`, batch.length * 100);} else if (usedNumbers.value.length === getTotalNumbers()) {triggerCelebration();}}};// --- 庆祝动画 ---// 触发庆祝动画const triggerCelebration = (message = '所有学号已抽取完成!', duration = 3000) => {celebrationMessage.value = message;showCelebration.value = true;setTimeout(() => {showCelebration.value = false;}, duration);};// --- 文件处理 ---// 处理上传的文件数据const handleFileData = (file) => {const reader = new FileReader();reader.onload = function(e) {try {const data = new Uint8Array(e.target.result);const workbook = XLSX.read(data, { type: 'array' });const firstSheetName = workbook.SheetNames[0];const worksheet = workbook.Sheets[firstSheetName];const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });const newStudents = [];for (let i = 0; i < jsonData.length; i++) {const row = jsonData[i];if (!row || row.length === 0) continue;const studentId = String(row[0]).trim();const name = row.length > 1 ? String(row[1]).trim() : '';if (studentId) {newStudents.push({ student_id: studentId, name });}}if (newStudents.length === 0) {showNotification('未找到有效的学生数据', 'error');return;}const groupIndex = studentGroups.value.findIndex(g => g.name === currentGroup.value);if (groupIndex >= 0) {const existingIds = studentGroups.value[groupIndex].students.map(s => s.student_id);newStudents.forEach(student => {if (!existingIds.includes(student.student_id)) {studentGroups.value[groupIndex].students.push(student);}});saveGroups();showNotification(`成功导入 ${newStudents.length} 条学生记录`);}} catch (error) {showNotification(`解析Excel文件失败: ${error.message}`, 'error');}};reader.readAsArrayBuffer(file);};// --- 页面导航与验证 ---// 验证设置并导航到主页面const validateAndNavigate = () => {if (operationMode.value === 'range') {if (start.value > end.value) {showNotification('错误:起始学号不能大于结束学号', 'error');return;}if (start.value < 1 || end.value < 1) {showNotification('错误:学号不能小于1', 'error');return;}probabilityRanges.value.forEach(range => {if (range.start > range.end) {showNotification(`错误:范围 ${range.start}-${range.end} 起始值不能大于结束值`, 'error');return;}if (range.start < start.value || range.end > end.value) {showNotification(`错误:范围 ${range.start}-${range.end} 超出学号范围`, 'error');return;}if (range.weight <= 0) {showNotification(`错误:范围 ${range.start}-${range.end} 权重必须大于0`, 'error');return;}});} else if (operationMode.value === 'list') {if (currentStudents.value.length === 0) {showNotification('错误:当前组没有学生', 'error');return;}}isSetupPage.value = false;usedNumbers.value = [];currentNumber.value = '—';currentStudent.value = { student_id: '', name: '' };multiDrawResults.value = [];updateUrlParams();};// 返回设置页面const goBack = () => {if (isContinuous.value) {clearInterval(continuousIntervalId.value);isContinuous.value = false;}isSetupPage.value = true;updateUrlParams();};// --- 历史记录管理 ---// 重置已抽取记录const resetUsedNumbers = () => {if (confirm('确定要重置已抽取记录吗?')) {usedNumbers.value = [];currentNumber.value = '—';currentStudent.value = { student_id: '', name: '' };multiDrawResults.value = [];showNotification('已重置抽取记录');}};// 清除历史记录const clearHistory = () => {usedNumbers.value = [];currentNumber.value = '—';currentStudent.value = { student_id: '', name: '' };multiDrawResults.value = [];};// --- 文件上传辅助函数 ---// 选择文件const selectFile = () => {document.getElementById('fileInput').click();};// 处理文件上传事件const handleFileUpload = (event) => {const file = event.target.files[0];if (!file) return;handleFileData(file);event.target.value = '';};// --- 学生分组管理 ---// 切换当前分组const switchGroup = (groupName) => {currentGroup.value = groupName;};// 添加新分组const addNewGroup = () => {const name = prompt('请输入新组名称:');if (name && name.trim()) {if (!studentGroups.value.some(g => g.name === name)) {studentGroups.value.push({ name, students: [] });currentGroup.value = name;saveGroups();showNotification(`已创建新组: ${name}`);} else {showNotification('组名已存在', 'error');}}};// 删除学生const deleteStudent = (index) => {const groupIndex = studentGroups.value.findIndex(g => g.name === currentGroup.value);if (groupIndex >= 0) {studentGroups.value[groupIndex].students.splice(index, 1);saveGroups();showNotification('学生已删除');}};// 清空当前组const clearCurrentGroup = () => {if (confirm(`确定要清空"${currentGroup.value}"组的所有学生吗?`)) {const groupIndex = studentGroups.value.findIndex(g => g.name === currentGroup.value);if (groupIndex >= 0) {studentGroups.value[groupIndex].students = [];saveGroups();showNotification('已清空学生名单');}}};// 删除分组const deleteGroup = (groupName, index) => {if (studentGroups.value.length <= 1) {showNotification('至少需要保留一个分组', 'error');return;}const confirmed = confirm(`确定要删除"${groupName}"组吗?此操作不可撤销。`);if (confirmed) {// 如果要删除的是当前分组,则切换到另一个分组if (currentGroup.value === groupName) {const otherIndex = index === 0 ? 1 : 0;currentGroup.value = studentGroups.value[otherIndex].name;}studentGroups.value.splice(index, 1);saveGroups();showNotification(`已删除组: ${groupName}`);}};// --- 数据持久化 ---// 保存学生分组到 localStorageconst saveGroups = () => {localStorage.setItem('studentGroups', JSON.stringify(studentGroups.value));};// --- 字号设置 ---// 应用字号设置const applyFontSettings = () => {document.documentElement.style.setProperty('--name-size', nameSize.value + 'rem');document.documentElement.style.setProperty('--number-size', numberSize.value + 'pt');showNotification('字号设置已应用');updateUrlParams();};// --- 概率范围调整 ---// 调整概率范围,确保其在学号范围内const adjustProbabilityRanges = () => {probabilityRanges.value.forEach(range => {if (range.start < start.value) range.start = start.value;if (range.end > end.value) range.end = end.value;if (range.start > range.end) range.start = range.end;});};// --- 概率范围管理 ---// 添加一个新的概率范围const addRange = () => {probabilityRanges.value.push({ start: start.value, end: end.value, weight: 50 });};// 移除一个概率范围const removeRange = (index) => {probabilityRanges.value.splice(index, 1);};// --- 生命周期钩子 ---// 组件挂载后执行onMounted(() => {try {// 从 localStorage 加载学生分组const savedGroups = localStorage.getItem('studentGroups');if (savedGroups) {studentGroups.value = JSON.parse(savedGroups);}// 如果没有分组,则创建默认组if (studentGroups.value.length === 0) {studentGroups.value = [{ name: '默认组', students: [] }];}// 从 localStorage 加载深色模式设置const savedDarkMode = localStorage.getItem('darkMode');if (savedDarkMode !== null) {darkMode.value = savedDarkMode === 'true';document.body.classList.toggle('dark-mode', darkMode.value);}} catch (e) {console.error('Failed to load data from localStorage', e);}// 如果没有概率范围,则添加一个默认范围if (probabilityRanges.value.length === 0) {addRange();}// 应用初始字号设置applyFontSettings();// 解析并应用URL参数const urlParams = parseUrlParams();start.value = urlParams.start;end.value = urlParams.end;mode.value = urlParams.mode;noRepeat.value = urlParams.noRepeat;operationMode.value = urlParams.operationMode;nameSize.value = urlParams.nameSize;numberSize.value = urlParams.numberSize;multiDrawCount.value = urlParams.multiDrawCount;batchSize.value = urlParams.batchSize;});// --- 侦听器 ---// 侦听多个设置项的变化,同步到URLwatch([start, end, mode, noRepeat, operationMode, nameSize, numberSize, multiDrawCount, batchSize], updateUrlParams, { deep: true });// 侦听起始和结束学号的变化,调整概率范围watch([start, end], adjustProbabilityRanges);// --- 返回供模板使用的数据和方法 ---return {// 状态isDragOver, isSetupPage, darkMode, showAdvanced, showBatchSettings,showMultiEffect, showCelebration, isContinuous, currentAnimationClass,// 核心数据operationMode, start, end, mode, noRepeat, currentNumber, currentStudent,usedNumbers, multiDrawResults, studentGroups, currentGroup, probabilityRanges,celebrationMessage, notification, multiEffectNumber, multiEffectName,// UI设置nameSize, numberSize, multiDrawCount, batchSize,// 计算属性currentStudents, getTotalNumbers,// 方法showNotification, toggleDarkMode, handleDragEnter, handleDragOver,handleDragLeave, handleDrop, getRandomColor, getWeightedRandomNumber,applyAnimation, drawNumberWithAnimation, drawNumber, toggleContinuous,multiDrawWithAnimation, multiDraw, drawBatchNumbersWithAnimation,drawBatchNumbers, triggerCelebration, handleFileData, validateAndNavigate,goBack, resetUsedNumbers, clearHistory, selectFile, handleFileUpload,switchGroup, addNewGroup, deleteStudent, clearCurrentGroup, saveGroups,addRange, removeRange, applyFontSettings, adjustProbabilityRanges, deleteGroup};}}).mount('#app');</script>
</body>
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/91782.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/91782.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Leetcode-141.环形链表

dict和set 1. 结构上的区别&#xff1a;类型键&#xff08;Key&#xff09;值&#xff08;Value&#xff09;示例dict有有{a: 1, b: 2}set有没有{a, b} dict 是**键值对&#xff08;key-value&#xff09;**的集合。set 是只有键&#xff08;key&#xff09;没有值的一组唯一元…

调节步进电机速度时调PSC和调ARR的区别

在步进电机控制中&#xff0c;调节速度通常是通过改变脉冲频率实现的。代码中选择调节ARR&#xff08;Auto-Reload Register&#xff09;而非PSC&#xff08;Prescaler&#xff09;的原因如下&#xff1a; 1. ARR 与 PSC 的核心区别 • ARR&#xff08;自动重载寄存器&#xff…

在 AKS 中运行 Azure DevOps 私有代理-1

简介 配置 Azure DevOps 私有代理的传统方法是将其部署在虚拟机 (VM) 上。然而,一个有趣的替代方案是利用 Azure Kubernetes 服务 (AKS) 来实现此目的。 本文将指导您如何使用 Helm Chart 在 AKS 集群中设置 Azure DevOps 私有代理,并提供该过程的分步说明。 在 AKS 中部署…

C# _Json数据

目录 1、添加Json库 2、数据序列化&#xff08;对象转 JSON&#xff09;和反序列化&#xff08;JSON 转对象&#xff09;操作 3、序列化 创建和读取Json数据 创建Json数据 定义一个CreateJson方法 读取 解析 Json数据 定义一个ReadJson方法 4、程序运行结果 在 C# 中&…

JavaScript 原始值与引用值

JavaScript 原始值与引用值 ECMAScript变量可以包含两种不同类型的数据&#xff1a;原始值和引用值。 原始值&#xff08;primitive value&#xff09;就是最简单的数据&#xff0c;引用值&#xff08;reference value&#xff09;则是由多个值构成的对象。 保存原始值的变量是…

linux中挂载磁盘和卸载

查找磁盘 找到你想要挂载的磁盘。可以使用lsblk或fdisk -l命令来查看系统中所有的磁盘和分区信息。 lsblk 对数据盘进行分区 在fdisk交互界面里&#xff0c;按以下步骤操作 fdisk /dev/vdb- 输入n来创建新分区。 - 按照提示设置分区的起始扇区、结束扇区等信息&#xff0c;…

java8学习笔记-Stream流

JDK1.8新增了Stream类&#xff0c;从而把函数式编程的风格引入到Java语言中&#xff0c;Stream类的API提供了强大的功能&#xff0c;使用Stream后&#xff0c;可以写出更加强大&#xff0c;更加简洁的代码首先&#xff0c;Stream流有一些特性&#xff1a;Stream流不是一种数据结…

Flutter开发 dart语言基本语法

特点 Dart语言支持JIT与AOT。 Dart语言采用单线程模型。 Dart语言是强类型编程语言&#xff0c;但是允许弱类型语言式编程。 基本语法 1.变量和常量 变量 var、object、dynamic关键字或数据类型显式声明变量。 命名规则&#xff1a; 变量名称必须由数字、字母、下划线或$组成&a…

SpringBoot:基于 Redis 自定义注解实现后端接口防重复提交校验(幂等操作)

SpringBoot&#xff1a;基于 Redis 自定义注解实现后端接口防重复提交校验&#xff08;幂等操作&#xff09;可基于 时间间隔 和 用于幂等判断的参数名称 实现防重复提交校验 客户端发送请求 ↓ [Spring Boot 应用入口]↓ ┌─────────────────────────…

【语音技术】意图与语料

目录 1. 意图 1.1. 意图分类 1.1.1 入口意图&#xff08;Entry Intent&#xff09; 1.1.2 对话意图&#xff08;Dialog Intent&#xff09; 1.2. 意图类型切换操作步骤 2. 语料 2.1 语料分类详解 2.2 语料编写规范详解 2.3 标签符号深度说明 3. 词槽 3.1 符类型要求 …

【MySQL集群架构与实践5】使用Docker实现水平分片

目录 一. 在Docker中安装ShardingSphere 二. 实践&#xff1a;水平分片 2.1 应用场景 2.2 架构图 2.3 服务器规划 2.4 创建server-user容器 2.5 创建server-order0和server-order1容器 2.6.日志配置 2.7 数据节点配置 2.8.测试数据节点 2.8.1.测试server_order0.t_or…

视觉图像处理中级篇 [1]—— 彩色照相机的效果与预处理

在工业检测中&#xff0c;黑白相机虽应用广泛&#xff0c;但在应对颜色差异检测时往往力不从心。彩色照相机凭借其对色彩信息的精准捕捉&#xff0c;成为复杂场景下的理想选择&#xff0c;而预处理技术则进一步释放了其性能潜力。一、彩色照相机的效果检查盖子上的金色标签可以…

使用 BERT 的 NSP 实现语义感知切片 —— 提升 RAG 系统的检索质量

在构建 Retrieval-Augmented Generation&#xff08;RAG&#xff09;系统时&#xff0c;文档的切片方式至关重要。我们需要将长文本切分成合适的段落&#xff08;chunks&#xff09;&#xff0c;然后存入向量数据库进行召回。如果切得太粗&#xff0c;会丢失上下文细节&#xf…

使用STM32CubeMX生成的STM32CubeIDE工程在更改工程名后编译失败问题解决

0 问题描述 使用STM32CubeMX生成STM32CubeIDE工程,然后使用STM32CubeIDE改名后编译提示如下错误: 1 问题原因及解决办法 1.1 问题原因 原因在于更名后STM32CubeIDE没有自动更新引用关系,这是因为我们使用STM32CubeMX生成代码时没有勾选在根目录下生成: 取消勾选在根目…

8月3日星期日今日早报简报微语报早读

8月3日星期日&#xff0c;农历闰六月初十&#xff0c;早报#微语早读。1、广西防城港&#xff1a;奔驰女司机身份已查清&#xff0c;结果将统一对外发布&#xff1b;2、陈艺文、陈佳包揽游泳世锦赛女子跳水三米板金银牌&#xff1b;3、九省份保险业已赔付暴雨灾害损失5.2亿元&am…

wxPython 实践(六)对话框

wxPython 实践&#xff08;一&#xff09;概述 wxPython 实践&#xff08;二&#xff09;基础控件 wxPython 实践&#xff08;三&#xff09;页面布局 wxPython 实践&#xff08;四&#xff09;事件响应 wxPython 实践&#xff08;五&#xff09;高级控件 wxPython 实践&#x…

MATLAB科研数据可视化技术

互联网的飞速发展伴随着海量信息的产生&#xff0c;而海量信息的背后对应的则是海量数据。如何从这些海量数据中获取有价值的信息来供人们学习和工作使用&#xff0c;这就不得不用到大数据挖掘和分析技术。数据可视化分析作为大数据技术的核心一环&#xff0c;其重要性不言而喻…

文明存续的时间博弈:论地球资源枯竭临界期的技术突围与行动紧迫性

摘要当地球资源消耗以指数级速度逼近生态承载力极限&#xff0c;人类文明正面临“存续还是消亡”的终极抉择。本文基于地球资源枯竭的实证数据与技术突破的可行性分析&#xff0c;揭示文明存续的时间窗口已进入不可逆临界期&#xff08;2040-2070年&#xff09;&#xff0c;论证…

Elasticsearch 8.19.0 和 9.1.0 中 LogsDB 和 TSDS 的性能与存储改进

作者&#xff1a;来自 Elastic Martijn Van Groningen 探索 TSDS 和 LogsDB 的最新增强功能&#xff0c;包括优化 I/O、提升合并性能等。 Elasticsearch 带来了许多新功能&#xff0c;帮助你为你的使用场景构建最佳搜索解决方案。通过我们的示例笔记本深入学习&#xff0c;开始…

cs336之注意pytorch的tensor在哪里?(assert的使用)

问题 记住&#xff1a;无论何时你在pytorch中有一个张量tensor&#xff0c;你应该始终问一个问题&#xff1a;它当前位于哪里&#xff1f; 注意它在CPU还是在GPU中。要判断它在哪里&#xff0c;可以使用python的assert断言语句。 assert断言 在 Python 中&#xff0c;assert 是…