从“更优”到“更智”:V5.7.3 的交互革新、模式扩展与体验跃迁 - 详解

从“更优”到“更智”:V5.7.3 的交互革新、模式扩展与体验跃迁

在 V5.6.4 版本实现了“化繁为简,聚焦核心”的目标后,我们收到了许多宝贵的用户反馈。本次 V5.7.3 版本,我们以“交互更流畅、功能更智能、体验更沉浸”为核心,对系统进行了一次全面的体验升级。我们不仅修复了已知困难,更引入了全新的抽取模式和交互方式,让您的每一次抽取都充满惊喜。

1. 全新“批量抽取”模式(全新机制)

痛点解决:当您需要一次性选出多名学生(如小组代表、活动参与者)时,利用“连抽”模式应该多次点击,操作繁琐且结果分散。

全新进化:隆重推出“批量抽取”模式!

  • 一键抽取,结果清晰:在“抽取设置”区域,您可以直接设置需要抽取的人数(如 3 人、5 人)。
  • 不重复保证:框架会一次性为您抽取指定数量的、不重复的学号,并将它们作为一个完整的批次结果展示在历史记录中。
  • 场景丰富:无论是组建学习小组、分配课堂任务,还是评选优秀代表,批量抽取都能让您高效、公平地完成任务。
2. 智能“连抽”模式全面升级(全新优化)

痛点解决:旧版“连抽”模式仅是单次抽取的简单重复,缺乏视觉冲击力和仪式感。

全新进化:“连抽”模式现已全面重制,带来沉浸式全屏动态效果!

  • 全屏动态展示:点击“连抽”按钮后,系统将进入全屏模式,逐一、动态地展示每一次抽取的结果。
  • 炫酷动画加持:每个被抽中的学生姓名和学号都会伴随独特的动画(如翻转、弹入、滚动)出现,过程充满悬念与乐趣。
  • 结果汇总:抽取结束后,所有结果会自动汇总并添加到历史记录中,方便您回顾。
3. 交互革命:引入通用模态框系统(全新架构)

痛点解决:旧版的确认弹窗能力单一,无法处理复杂的用户输入(如新建组名),且代码复用性差。

全新进化:引入了通用、可复用的模态框(Modal)系统!

  • 功能统一:现在,“删除分组”、“清空名单”、“重置记录”、“新建组”、“重命名组”等所有要求用户确认或输入的运行,都使用了统一的模态框组件。
  • 交互更佳:模态框拥有半透明背景和模糊效果,视觉上更突出,交互体验更现代。输入框会自动聚焦,操作更流畅。
  • 代码更优: 通过 showModalWithConfig 方法,可以灵活配置模态框的标题、内容、按钮和回调函数,极大地提升了代码的可维护性和扩展性。
4. 主题与动画效果增强(体验优化)

痛点解决:为庆祝特性和通知系统增添更多视觉细节,提升整体美观度。

全新进化:细节之处见真章!

  • 庆祝动画升级: 庆祝彩带(confetti)的生成逻辑和动画效果得到优化,飘落更自然,色彩更丰富,将“全员抽取完毕”的喜悦感推向高潮。
  • 通知系统完善: 通知消息(notification)增加了图标和更明确的样式区分(成功/错误),信息传达更直观。
  • 深色模式覆盖:对模态框、通知等新增组件的深色模式支撑进行了全面检查和适配,确保在任何主题下都拥有完美的视觉体验。
5. 配置持久化升级:URL参数新增权重同步(关键增强)

痛点解决:之前的版本,您精心设置的概率权重无法通过链接分享,导致协作和远程教学时要求重新配置。

全新进化:现在,概率权重设置已与URL参数完全同步!

  • 完整配备共享:您为不同学号范围设置的权重(如“1-10号权重80%”)会被自动编码并附加到当前页面的URL上。
  • 一键分享,完美还原:当您将此链接分享给同事或学生时,他们打开链接后,所有设置,包括复杂的概率权重,都会被完美还原
  • 无缝恢复:即使您刷新页面,只要URL中包含参数,框架也会自动解析并应用这些设置,确保您的工作不丢失。
6. 核心逻辑与已知问题修复(稳定性提升)
  • URL 参数解析修复: 修正了 noRepeat 参数的解析逻辑,确保通过 URL 分享的链接能正确还原“不重复抽取”的开关状态。
  • 概率权重逻辑微调: 优化了 getWeightedRandomNumber 函数在边界条件下的处理,确保在极端设置下依然能稳定工作。
  • 状态同步优化: 改进了 watch 侦听器的配置,确保 probabilityRanges 等复杂对象的变化能更可靠地同步到 URL 和界面。

功能亮点总览

V5.7.3 版本在“更优”的基础上,实现了“更智”的飞跃,核心特性全面升级:

  • ✅ 双模式操作:灵活选择“学号范围”或“名单导入”模式。
  • ✅ 多名单组管理:支持创建、切换、删除多个独立的学生分组。
  • ✅ 全新暗黑模式:一键切换浅色/深色主题,保护您的眼睛。
  • ✅ 多种抽取模式:支持单次抽取、快速抽取(学号滚动)、连抽N次(带全屏特效)批量抽取
  • ✅ 不重复抽取:开启后,确保每个学号在本轮抽取中仅出现一次。
  • ✅ 智能概率权重控制:为不同学号区间设置不同的抽取概率,且支持通过URL完整分享和持久化
  • ✅ 精简字号设置:移除字体选择,保留字号调节滑块,更加简洁高效
  • ✅ 沉浸式交互体验:“连抽”与“批量抽取”模式带来前所未有的动态视觉盛宴。
  • ✅ 统一模态框系统:所有确认与输入操作交互一致,体验更流畅。
  • ✅ 信息持久化:所有学生名单、分组、设置均自动保存,刷新不丢失。
  • ✅ URL 参数同步:所有配置(当前分组、学号范围、字体、主题、概率权重等)都会同步到 URL,方便分享与恢复。
  • ✅ 拖拽上传:支持将 Excel 资料直接拖拽到网页进行上传,操作更便捷。

技术达成精要

本次更新在 main.jsstyle.css 中进行了关键重构与新增:

  1. 批量抽取搭建:新增 drawBatchNumbersdrawBatchNumbersWithAnimation 函数,利用 getAllNumbers 获取所有可选学号,结合 noRepeat 逻辑,一次性生成指定数量的不重复结果。
  2. 连抽模式重制:将 multiDraw 逻辑与全新的全屏动态效果组件(multiEffectNumber, multiEffectName)结合,通过 multiDrawWithAnimation 函数驱动整个动画流程。
  3. 模态框系统构建:创建了 showModalWithConfigcloseModal 方法,配合 modalConfig 响应式对象,实现了高度可配置的模态框。所有相关操作(showDeleteGroupModal, showAddGroupModal 等)均基于此系统重构。
  4. URL参数深度同步:在 parseUrlParamsupdateUrlParams 函数中,新增了对 probabilityRanges 数组的编解码逻辑(encodeProbabilityRanges / decodeProbabilityRanges),实现了权重设置的URL持久化。
  5. 状态管理与侦听watch 侦听器现在深度监听 probabilityRanges 数组,确保其变化能及时更新 URL。

现在就去体验吧!访问我们的在线体验地址,感受这份智能、高效与沉浸式交互的完美结合。如果您喜欢这个计划,别忘了给我的 GitHub 仓库点个 Star!您的支持是我持续更新的最大动力。

项目体验地址:https://arlodmuy.html2web.com/
推荐把HTML放在本地,.html2web好像有一点点小问题

源码地址:https://github.com/HerryABU/student-number-picker
本文地址: https://blog.csdn.net/Herryfyh/article/details/149956928

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

源代码

<!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>智能学号抽取系统V5.7.3</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;
              }
              }
              /* ============ 新增的模态框 (Modal) 样式 ============ */
              .modal-backdrop {
              position: fixed;
              top: 0;
              left: 0;
              width: 100%;
              height: 100%;
              background: rgba(0, 0, 0, 0.5);
              backdrop-filter: blur(5px);
              z-index: 1001;
              display: flex;
              justify-content: center;
              align-items: center;
              animation: fadeIn 0.3s ease;
              }
              .modal {
              background: var(--card-bg);
              border-radius: 12px;
              box-shadow: var(--card-shadow);
              width: min(90%, 400px);
              overflow: hidden;
              transform: scale(0.8);
              opacity: 0;
              animation: modalPop 0.3s ease forwards;
              transition: transform 0.3s ease;
              }
              .dark-mode .modal {
              background: var(--dark-card-bg);
              }
              .modal-header {
              padding: 1.5rem 1.5rem 1rem;
              border-bottom: 1px solid var(--border-color);
              color: var(--text-dark);
              }
              .dark-mode .modal-header {
              border-color: var(--dark-border-color);
              color: var(--dark-text-light);
              }
              .modal-title {
              font-size: 1.3rem;
              font-weight: 600;
              }
              .modal-body {
              padding: 1.5rem;
              color: var(--text-muted);
              font-size: 1.1rem;
              }
              .dark-mode .modal-body {
              color: var(--dark-text-muted);
              }
              .modal-footer {
              padding: 1rem 1.5rem;
              display: flex;
              justify-content: flex-end;
              gap: 1rem;
              border-top: 1px solid var(--border-color);
              }
              .dark-mode .modal-footer {
              border-color: var(--dark-border-color);
              }
              .modal-footer button {
              padding: 0.6rem 1.2rem;
              font-size: 1rem;
              }
              .modal-footer button.secondary {
              background: white;
              color: var(--text-dark);
              border: 2px solid var(--border-color);
              }
              .dark-mode .modal-footer button.secondary {
              background: var(--dark-card-bg);
              border-color: var(--dark-border-color);
              }
              .modal-footer button.primary {
              background: var(--primary-color);
              color: white;
              }
              .modal-footer button.danger {
              background: var(--danger-color);
              color: white;
              }
              @keyframes modalPop {
              0% {
              transform: scale(0.8);
              opacity: 0;
              }
              70% {
              transform: scale(1.05);
              }
              100% {
              transform: scale(1);
              opacity: 1;
              }
              }
              /* 修复模态框内输入框样式 */
              .modal-body input[type="text"] {
              width: 100%;
              padding: 0.8rem;
              margin-top: 0.5rem;
              border: 1px solid var(--border-color);
              border-radius: 8px;
              outline: none;
              transition: border-color 0.3s ease;
              }
              .modal-body input[type="text"]:focus {
              border-color: var(--primary-color);
              }
              .dark-mode .modal-body input[type="text"] {
              background: var(--dark-card-bg);
              color: var(--dark-text-light);
              border-color: var(--dark-border-color);
              }
            </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="showRenameGroupModal(group.name, index)"
                                                @click.prevent>
                                            <i class="fas fa-edit"></i>
                                            </button>
                                            <!-- 删除分组按钮 -->
                                                <button class="delete-group-btn"
                                                @click.stop="showDeleteGroupModal(group.name, index)"
                                                @click.prevent>
                                            <i class="fas fa-times"></i>
                                            </button>
                                          </div>
                                          <!-- “添加新组”按钮 -->
                                              <button class="primary" @click="showAddGroupModal" 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="showClearGroupModal" 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="showResetHistoryModal">
                                                                            <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="showClearHistoryModal" 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>
                                                                      <!-- ============ 新增的模态框 (Modal) ============ -->
                                                                        <!-- 统一的模态框 -->
                                                                            <div v-if="showModal" class="modal-backdrop" @click="closeModal">
                                                                              <div class="modal" @click.stop>
                                                                                <div class="modal-header">
                                                                              <h5 class="modal-title">{{ modalConfig.title }}</h5>
                                                                              </div>
                                                                                <div class="modal-body">
                                                                              <p>{{ modalConfig.message }}</p>
                                                                                <input
                                                                                  v-if="modalConfig.inputRequired"
                                                                                  type="text"
                                                                                  v-model="modalConfig.inputValue"
                                                                                  :placeholder="modalConfig.inputPlaceholder"
                                                                                  @keyup.enter="modalConfig.onConfirm"
                                                                                  ref="modalInputRef"
                                                                                >
                                                                              </div>
                                                                                <div class="modal-footer">
                                                                              <button class="secondary" @click="closeModal">取消</button>
                                                                                  <button :class="['primary', modalConfig.danger ? 'danger' : '']" @click="modalConfig.onConfirm">
                                                                                  {{ modalConfig.confirmText }}
                                                                                </button>
                                                                              </div>
                                                                            </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);
                                                                            // 快速抽取的定时器ID
                                                                            const currentAnimationClass = ref('');
                                                                            // 当前应用的动画类名
                                                                            // --- 模态框状态 ---
                                                                            const showModal = ref(false);
                                                                            const modalConfig = ref({
                                                                            title: '',
                                                                            message: '',
                                                                            confirmText: '确定',
                                                                            danger: false,
                                                                            onConfirm: () =>
                                                                            {
                                                                            },
                                                                            // 新增:用于动态输入
                                                                            inputRequired: false,
                                                                            inputValue: '',
                                                                            inputPlaceholder: ''
                                                                            });
                                                                            // 用于在模态框打开后自动聚焦输入框
                                                                            const modalInputRef = ref(null);
                                                                            // --- 核心数据 ---
                                                                            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 encodeProbabilityRanges = (ranges) =>
                                                                            {
                                                                            return ranges.map(r =>
                                                                            `${r.start
                                                                            }-${r.end
                                                                            }-${r.weight
                                                                            }`).join(';');
                                                                            };
                                                                            // 将URL中的字符串解码为概率权重数组
                                                                            const decodeProbabilityRanges = (str) =>
                                                                            {
                                                                            if (!str) return [];
                                                                            return str.split(';').map(part =>
                                                                            {
                                                                            const [start, end, weight] = part.split('-').map(Number);
                                                                            return { start, end, weight
                                                                            };
                                                                            }).filter(r =>
                                                                            !isNaN(r.start) &&
                                                                            !isNaN(r.end) &&
                                                                            !isNaN(r.weight));
                                                                            };
                                                                            // 从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,
                                                                            probabilityRanges: decodeProbabilityRanges(params.get('probabilityRanges'))
                                                                            };
                                                                            };
                                                                            // 将当前设置同步到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);
                                                                            if (probabilityRanges.value.length >
                                                                            0) {
                                                                            params.set('probabilityRanges', encodeProbabilityRanges(probabilityRanges.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;
                                                                            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);
                                                                            }
                                                                            };
                                                                            // --- 连抽 ---
                                                                            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;
                                                                            }
                                                                            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;
                                                                            }
                                                                            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 clearHistory = () =>
                                                                              {
                                                                              usedNumbers.value = [];
                                                                              currentNumber.value = '—';
                                                                              currentStudent.value = { student_id: '', name: ''
                                                                              };
                                                                              multiDrawResults.value = [];
                                                                              };
                                                                              const resetUsedNumbers = () =>
                                                                              {
                                                                              clearHistory();
                                                                              showNotification('已重置抽取记录');
                                                                              };
                                                                              // --- 文件上传辅助函数 ---
                                                                              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 = modalConfig.value.inputValue.trim();
                                                                              if (!name) {
                                                                              showNotification('组名不能为空', 'error');
                                                                              return;
                                                                              }
                                                                              if (!studentGroups.value.some(g => g.name === name)) {
                                                                              studentGroups.value.push({ name, students: []
                                                                              });
                                                                              currentGroup.value = name;
                                                                              saveGroups();
                                                                              showNotification(`已创建新组: ${name
                                                                              }`);
                                                                              closeModal();
                                                                              } else {
                                                                              showNotification('组名已存在', 'error');
                                                                              }
                                                                              };
                                                                              const renameGroup = (oldName, index) =>
                                                                              {
                                                                              const newName = modalConfig.value.inputValue.trim();
                                                                              if (!newName) {
                                                                              showNotification('组名不能为空', 'error');
                                                                              return;
                                                                              }
                                                                              if (studentGroups.value.some((g, i) => g.name === newName && i !== index)) {
                                                                              showNotification('组名已存在', 'error');
                                                                              return;
                                                                              }
                                                                              studentGroups.value[index].name = newName;
                                                                              if (currentGroup.value === oldName) {
                                                                              currentGroup.value = newName;
                                                                              }
                                                                              saveGroups();
                                                                              showNotification(`已将组 "${oldName
                                                                              }" 重命名为 "${newName
                                                                              }"`);
                                                                              closeModal();
                                                                              };
                                                                              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 = () =>
                                                                              {
                                                                              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 otherIndex = index === 0 ? 1 : 0;
                                                                              const newCurrentGroup = studentGroups.value[otherIndex].name;
                                                                              studentGroups.value.splice(index, 1);
                                                                              if (currentGroup.value === groupName) {
                                                                              currentGroup.value = newCurrentGroup;
                                                                              }
                                                                              saveGroups();
                                                                              showNotification(`已删除组: ${groupName
                                                                              }`);
                                                                              };
                                                                              // --- 数据持久化 ---
                                                                              const 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 {
                                                                              const savedGroups = localStorage.getItem('studentGroups');
                                                                              if (savedGroups) {
                                                                              studentGroups.value = JSON.parse(savedGroups);
                                                                              }
                                                                              if (studentGroups.value.length === 0) {
                                                                              studentGroups.value = [{ name: '默认组', students: []
                                                                              }];
                                                                              }
                                                                              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();
                                                                              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;
                                                                              if (urlParams.probabilityRanges.length >
                                                                              0) {
                                                                              probabilityRanges.value = urlParams.probabilityRanges;
                                                                              }
                                                                              });
                                                                              // --- 侦听器 ---
                                                                              watch([
                                                                              start, end, mode, noRepeat, operationMode, nameSize, numberSize, multiDrawCount, batchSize, probabilityRanges
                                                                              ], updateUrlParams, { deep: true
                                                                              });
                                                                              watch([start, end], adjustProbabilityRanges);
                                                                              // --- 新增的模态框方法 ---
                                                                              const showModalWithConfig = (config) =>
                                                                              {
                                                                              modalConfig.value = {
                                                                              ...config
                                                                              };
                                                                              // 如果需要输入,清空之前的输入值
                                                                              if (config.inputRequired) {
                                                                              modalConfig.value.inputValue = '';
                                                                              }
                                                                              showModal.value = true;
                                                                              // 在模态框打开后,延迟聚焦输入框
                                                                              if (config.inputRequired) {
                                                                              setTimeout(() =>
                                                                              {
                                                                              if (modalInputRef.value) {
                                                                              modalInputRef.value.focus();
                                                                              }
                                                                              }, 300);
                                                                              }
                                                                              };
                                                                              const closeModal = () =>
                                                                              {
                                                                              showModal.value = false;
                                                                              };
                                                                              const showDeleteGroupModal = (groupName, index) =>
                                                                              {
                                                                              if (studentGroups.value.length <= 1) {
                                                                              showNotification('至少需要保留一个分组', 'error');
                                                                              return;
                                                                              }
                                                                              showModalWithConfig({
                                                                              title: '删除分组',
                                                                              message: `确定要删除"${groupName
                                                                              }"组吗?此操作不可撤销。`,
                                                                              confirmText: '删除',
                                                                              danger: true,
                                                                              onConfirm: () =>
                                                                              {
                                                                              deleteGroup(groupName, index);
                                                                              closeModal();
                                                                              }
                                                                              });
                                                                              };
                                                                              const showClearGroupModal = () =>
                                                                              {
                                                                              showModalWithConfig({
                                                                              title: '清空名单',
                                                                              message: `确定要清空"${currentGroup.value
                                                                              }"组的所有学生吗?`,
                                                                              confirmText: '清空',
                                                                              danger: true,
                                                                              onConfirm: () =>
                                                                              {
                                                                              clearCurrentGroup();
                                                                              closeModal();
                                                                              }
                                                                              });
                                                                              };
                                                                              const showResetHistoryModal = () =>
                                                                              {
                                                                              showModalWithConfig({
                                                                              title: '重置记录',
                                                                              message: '确定要重置已抽取记录吗?',
                                                                              confirmText: '重置',
                                                                              danger: true,
                                                                              onConfirm: () =>
                                                                              {
                                                                              resetUsedNumbers();
                                                                              closeModal();
                                                                              }
                                                                              });
                                                                              };
                                                                              const showClearHistoryModal = () =>
                                                                              {
                                                                              showModalWithConfig({
                                                                              title: '清除历史',
                                                                              message: '确定要清除所有历史记录吗?',
                                                                              confirmText: '清除',
                                                                              danger: true,
                                                                              onConfirm: () =>
                                                                              {
                                                                              clearHistory();
                                                                              closeModal();
                                                                              }
                                                                              });
                                                                              };
                                                                              const showAddGroupModal = () =>
                                                                              {
                                                                              showModalWithConfig({
                                                                              title: '新建组',
                                                                              message: '请输入新组名称:',
                                                                              inputRequired: true,
                                                                              inputPlaceholder: '例如:三年二班',
                                                                              confirmText: '创建',
                                                                              danger: false,
                                                                              onConfirm: addNewGroup
                                                                              });
                                                                              };
                                                                              const showRenameGroupModal = (groupName, index) =>
                                                                              {
                                                                              showModalWithConfig({
                                                                              title: '重命名组',
                                                                              message: `请输入 "${groupName
                                                                              }" 的新名称:`,
                                                                              inputRequired: true,
                                                                              inputPlaceholder: '例如:三年三班',
                                                                              confirmText: '重命名',
                                                                              danger: false,
                                                                              onConfirm: () =>
                                                                              renameGroup(groupName, index)
                                                                              });
                                                                              };
                                                                              // --- 返回供模板使用的数据和方法 ---
                                                                              return {
                                                                              // 状态
                                                                              isDragOver, isSetupPage, darkMode, showAdvanced, showBatchSettings,
                                                                              showMultiEffect, showCelebration, isContinuous, currentAnimationClass,
                                                                              showModal, modalConfig, modalInputRef,
                                                                              // 核心数据
                                                                              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, renameGroup, deleteStudent, clearCurrentGroup, saveGroups,
                                                                              addRange, removeRange, applyFontSettings, adjustProbabilityRanges, deleteGroup,
                                                                              // 新增模态框方法
                                                                              showModalWithConfig, closeModal, showDeleteGroupModal, showClearGroupModal,
                                                                              showResetHistoryModal, showClearHistoryModal, showAddGroupModal, showRenameGroupModal
                                                                              };
                                                                              }
                                                                              }).mount('#app');
                                                                            </script>
                                                                          </body>
                                                                        </html>
posted @ 2025-08-14 20:46  wzzkaifa  阅读(11)  评论(0)    收藏  举报