吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 562|回复: 20
上一主题 下一主题
收起左侧

[学习记录] 课堂抽签(用着方便)

  [复制链接]
跳转到指定楼层
楼主
快乐天使2 发表于 2026-10-20 11:16 回帖奖励
本帖最后由 快乐天使2 于 2026-10-20 18:15 编辑

我是一个高中教师,一直在找一个好用的抽签程序,也用了很多,都不是很如意,后来自己借助AI,做了这个抽签程序,一边用一边修改,现在觉得很好用了,就分享出来,我也不知道是不是原创,我花了很多时间修改调试了的,重点是真的很好用!
哪个大佬能帮我把这个程序打包成一个适应任何环境的可执行文件!
这是一份纯前端、单文件、零依赖的「课堂抽签」网页程序(约 819行,HTML + CSS + 原生 JavaScript 全在一个文件里),打开即用、断网可用。
一、抽签核心流程
5 秒滚动抽签‌:点击「抽签」后名字每 80ms 随机跳动一次,同时播放"嘀嗒"声,形成快速滚动效果。
按钮实时倒计时‌:抽签按钮上以‌秒为单位、保留两位小数‌实时显示剩余时间(如 3.42),归零自动停。
滚动中可提前停止‌:滚动过程中再点一次按钮,立即定格结果,不用干等 5 秒。
中签高亮‌:抽中的名字变为‌金色加粗、字号放大、带光晕背景‌,十分醒目。
结果写入记录‌:每次抽签结果自动落到下方历史记录里。
二、抽取模式(可切换)
不重复抽取(默认)‌:抽过的人在名单里‌自动置灰并加删除线‌,不再被抽到,避免同一人反复中签。
可重复抽取‌:每次都从全员里随机,适合无所谓重复的场景。
顶部一键切换‌:「切换:可重复 / 不重复」按钮,无需进设置面板。
抽完自动提醒‌:不重复模式下全员抽完时弹窗提示"请点击「重置已抽」开始新一轮",不会抽到空结果。
剩余可抽统计‌:信息条实时显示"总人数 / 剩余可抽"。
三、多人(小组)抽取
一次抽 N 人‌:点「抽多人(小组)」按钮,按设置里的人数一次抽出多人,结果以 张三 · 李四 · 王五 形式一行展示。
同次内不重复‌:多人抽取用 Fisher-Yates 洗牌抽样,保证‌这一次抽出的几个人互不相同‌。
人数上限校验‌:要抽取的人数超过剩余/总人数时会拦截并提示,不会出错。
四、班级管理
新建班级‌:带空值校验与重名校验("班级已存在")。
切换班级‌:顶部下拉框与设置面板下拉框联动,切换后名字框复位、名单与统计同步刷新。
删除班级‌:二次确认后连带学生名单与已抽记录一并清除。
重命名班级‌:改名同时迁移该班的‌已抽记录‌,不会丢状态。
记住上次班级‌:lastClass 独立持久化,刷新页面或重开浏览器都会‌回到你上次选的那个班‌(原版会重置到第一个班)。
五、学生名单管理
单个添加‌:输入姓名点添加,‌输入框内按回车可直接添加‌。
重复姓名提示‌:同名时会弹窗确认,避免重名造成识别歧义。
批量导入(追加)‌:多行文本批量加人,支持‌换行、逗号、中文逗号「,」、顿号「、」、分号「;」‌作分隔符,从 Word/Excel 粘贴也能识别。
批量替换‌:清空当前班级后整体导入新名单,同时重置已抽记录。
清空全部学生‌:一键清空当前班级名单(已抽记录同步重置)。
逐个删除‌:每个名字带红色「×」,点删即删;删除时会‌同步从已抽名单里移除‌,不留脏数据。
已抽可视化‌:已抽过的名字在名单里灰显划线,一眼看出进度。
六、抽签参数设置
滚动时长可调‌:1–30 秒自由设定。
单次抽取人数可调‌:1–20 人。
音效开关‌:可一键静音。
不重复开关‌:勾选框与顶部按钮双向同步。
设置保存在本地,‌下次打开自动沿用‌。
七、抽签历史记录
底部自动记录每次结果:‌时间 / 班级 / 抽到谁‌,最新记录排在最前。
最多保留 200 条‌,超出自动弹掉最旧的。
一键清空记录‌(不影响名单和已抽状态)。
记录持久化,刷新不丢失。
八、数据备份与迁移
导出备份‌:导出 课堂抽签备份_YYYYMMDD.json,内容包含‌班级名单 + 已抽记录 + 全部设置‌,用于换电脑/清缓存前备份。
导入备份‌:选 JSON 文件即恢复完整状态;‌兼容旧版本备份‌(只含班级名单的老文件也能导入)。
自动本地保存‌:所有数据实时写入浏览器本地,无需联网、无需服务器。
九、音效系统(纯离线合成)
滚动嘀嗒声‌:880Hz 短促音,配合名字跳动。
中签提示铃‌:模拟考试提示铃,用 5 个泛音分量 + 轻微失谐 + 敲击瞬态噪声合成金属钟声,双声"叮—咚"。
不依赖任何音频文件‌:全部由 Web Audio 实时合成,断网也能响。
首击无声已修复‌:音频上下文改为用户首次点击时解锁,第一个音就能正常播放。
十、键盘快捷操作
空格键‌:直接抽签(弹窗未打开时)。
ESC 键‌:关闭设置弹窗。
回车键‌:在添加学生输入框里快速添加。
十一、界面与适配
顶部信息条一屏显示‌总人数 / 剩余可抽 / 模式 / 时长‌四个关键状态。
蓝紫渐变背景 + 白色圆角卡片,视觉清爽。
移动端响应式‌:窗口宽度小于 600px 时字号、按钮、布局自动收缩,手机/平板上也能用。
十二、初始化与数据兼容
首次打开自动建班‌:预设班级「高一2619班」并填入 63 个占位名字(学生1…学生63),打开即有数据可试。
旧数据自动升级‌:检测到老版本数据时自动补全已抽记录结构,不会报错。


看到9楼的wufashihu网友改的程序比较好,搬过来传蓝奏了,
html版课堂抽签程序:https://wwblh.lanzouc.com/iah7N4cp5oef密码:5mwb

免费评分

参与人数 4吾爱币 +3 热心值 +4 收起 理由
swz7852151 + 1 + 1 我很赞同!
Simon_iln + 1 + 1 用心讨论,共获提升!
xuhao4577062311 + 1 + 1 我很赞同!
jiyu0418 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
lidada0627 发表于 2026-10-20 11:49
正好亲戚有人是老师 推荐一下这个好工具
3#
jspkun 发表于 2026-10-20 11:56
4#
wentao520 发表于 2026-10-20 12:26
5#
netfox2008 发表于 2026-10-20 12:28
所源码打包发链接吧。论坛吃代码,代码复制完打开,设置按钮无效,其它的也没法测试!
6#
Mzhang2008 发表于 2026-10-20 12:42
复制代码,生成HTML,设置,点击无效
7#
feng_129 发表于 2026-10-20 12:46
不完整,代码不全,只有界面,功能没反应。
8#
robyyang 发表于 2026-10-20 13:09
厉害厉害!老师也可以这么有想法!
9#
wufashihu 发表于 2026-10-20 13:33
本帖最后由 wufashihu 于 2026-10-20 14:02 编辑

exe1.0版本:下载:https://wwaye.lanzoue.com/iUJmS4cockgj 密码:a3c6

html1.0版本:下载:https://wwaye.lanzoue.com/ikTtm4coe0za 密码:i7oj

已修复以上说的所有bug问题了。

方便的话给点分数。谢谢!

更新2.0:最下面删除太麻烦了,添加了批量和清空

exe2.0版本:下载:https://wwaye.lanzoue.com/i4Chk4cohiqh 密码:1cbu

html2.0版本:下载:https://wwaye.lanzoue.com/ikCHS4cohiri 密码:530j

代码2.0公开一下吧。

[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' blob: data:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' blob: data:; media-src 'self' blob:;">
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
<meta name="theme-color" content="#2b7cd3">
<meta name="format-detection" content="telephone=no">
<title>课堂抽签</title>
<style>
/* ============ 基础重置 ============ */
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
html,body{height:100%;margin:0;}
body{
    font-family:"Microsoft Yahei","PingFang SC","Helvetica Neue",Arial,sans-serif;
    background:linear-gradient(135deg,#4fc3f7 0%,#eaf6ff 45%,#0ea5e9 100%);
    background-attachment:fixed;
    text-align:center;padding:16px;color:#102a43;
    min-height:100vh;-webkit-text-size-adjust:100%;
}
.wrap{max-width:960px;margin:0 auto;}
button{font-family:inherit;}

/* ============ 顶部工具栏 ============ */
.header-bar{
    display:flex;justify-content:space-between;align-items:center;
    max-width:760px;margin:0 auto 14px;flex-wrap:wrap;gap:10px;
}
.current-class-text{font-size:20px;font-weight:bold;display:flex;align-items:center;gap:8px;color:#102a43;flex-wrap:wrap;}
#topClassSelect{
    font-size:18px;font-weight:bold;padding:8px 12px;border:2px solid #2b7cd3;border-radius:12px;
    background:#fff;color:#2b7cd3;cursor:pointer;outline:none;max-width:240px;
    box-shadow:0 3px 8px rgba(0,0,0,0.18);
}
.toolbar{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;}
.toolbar button{
    padding:10px 16px;font-size:15px;cursor:pointer;border:none;border-radius:10px;min-height:44px;
    background:#6c757d;color:#fff;box-shadow:0 2px 6px rgba(0,0,0,0.18);transition:.15s;touch-action:manipulation;
}
.toolbar button:active{transform:translateY(1px);}
.toolbar button.primary{background:#2b7cd3;}
.toolbar button.warn{background:#e67e22;}
.toolbar button.danger{background:#c0392b;}
.toolbar button.success{background:#27ae60;}

/* ============ 抽签主卡片 ============ */
.box{
    max-width:760px;margin:0 auto;background:linear-gradient(135deg,#ffffff 0%,#eaf4ff 55%,#ffffff 100%);
    padding:14px 16px;border-radius:20px;box-shadow:0 8px 28px rgba(0,0,0,0.18);
}
#infoBar{font-size:14px;color:#486581;margin:2px 0 8px;display:flex;flex-wrap:wrap;gap:6px;justify-content:center;align-items:center;}
.kv{display:inline-block;background:#fff;padding:5px 10px;border-radius:8px;font-size:13px;color:#486581;box-shadow:0 1px 3px rgba(0,0,0,0.08);}
#toggleModeBtn{
    padding:6px 12px;font-size:13px;cursor:pointer;border:1px solid #2b7cd3;border-radius:8px;
    background:#e7f1fd;color:#2b7cd3;min-height:34px;touch-action:manipulation;
}
#nameBoxWrap{min-height:150px;display:flex;align-items:center;justify-content:center;}
#nameBox{
    width:100%;padding:18px 20px;font-size:clamp(28px,8vw,46px);line-height:1.3;border-radius:18px;
    box-shadow:inset 0 2px 8px rgba(0,0,0,0.06),0 4px 12px rgba(0,0,0,0.10);
    background:linear-gradient(135deg,#e3f2fd,#bbdefb);color:#102a43;
    word-break:break-word;overflow-wrap:anywhere;transition:background .2s,color .2s;
}
.selected{
    color:#a35b00 !important;font-weight:bold !important;
    background:linear-gradient(135deg,#fff7e6,#ffe7ba) !important;
    box-shadow:0 0 0 4px #ffd666 inset,0 6px 20px rgba(218,165,32,0.4) !important;
}
#btnWrap{margin-top:12px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
#startBtn{
    padding:12px 24px;font-size:clamp(22px,6vw,32px);font-weight:bold;cursor:pointer;border:none;border-radius:14px;
    background:linear-gradient(135deg,#5b8def,#1859b3);color:#fff;min-height:56px;min-width:160px;
    box-shadow:0 6px 16px rgba(43,124,211,0.45);transition:.15s;touch-action:manipulation;
}
#startBtn:active{transform:translateY(1px);}
.tip{margin-top:14px;color:#2b7cd3;font-size:clamp(18px,5vw,30px);font-weight:bold;}

/* ============ 历史记录 ============ */
.history-box{
    margin:16px auto 0;padding:12px 14px;background:#f0f4f8;border-radius:12px;text-align:left;max-width:760px;
}
.history-title{font-weight:bold;color:#102a43;margin-bottom:6px;display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;}
.history-list{font-size:14px;color:#334e68;max-height:110px;overflow-y:auto;line-height:1.9;-webkit-overflow-scrolling:touch;}
.history-list span{display:inline-block;margin-right:8px;padding:2px 8px;background:#fff;border-radius:6px;margin-bottom:3px;}

/* ============ 弹窗 ============ */
.modal-mask{
    position:fixed;inset:0;background:rgba(0,0,0,0.55);
    display:none;align-items:flex-start;justify-content:center;z-index:999;padding:20px 10px;overflow-y:auto;
    -webkit-overflow-scrolling:touch;
}
.modal-mask.show{display:flex;}
.modal-box{
    background:#fff;padding:18px;border-radius:16px;width:820px;max-width:96vw;text-align:left;
    box-shadow:0 10px 40px rgba(0,0,0,0.3);margin:auto;
}
.modal-box h3{margin:0 0 6px;text-align:center;color:#102a43;}
.close-modal{text-align:right;margin-bottom:4px;}
#closeBtn{padding:8px 18px;cursor:pointer;border:none;border-radius:8px;background:#bcccdc;min-height:40px;font-size:15px;}
.section-title{font-weight:bold;color:#102a43;margin:16px 0 8px;padding-bottom:5px;border-bottom:2px solid #d9e2ec;}
.row{margin:10px 0;display:flex;gap:8px;flex-wrap:wrap;align-items:center;}
.row label{font-size:14px;color:#334e68;}
input,select,textarea{font-family:inherit;}
input,select{padding:9px 10px;font-size:16px;border:1px solid #bcccdc;border-radius:8px;outline:none;background:#fff;color:#102a43;}
input:focus,select:focus,textarea:focus{border-color:#2b7cd3;}
input[type="number"]{width:84px;}
input[type="checkbox"]{width:18px;height:18px;}
#classNameInput{flex:1;min-width:180px;}
#stuNameInput{flex:1;min-width:180px;}
textarea{padding:9px;font-size:16px;width:100%;height:110px;resize:vertical;border:1px solid #bcccdc;border-radius:8px;outline:none;line-height:1.6;}
button.small{padding:9px 14px;font-size:14px;cursor:pointer;border:none;border-radius:8px;background:#2b7cd3;color:#fff;min-height:40px;touch-action:manipulation;}
button.small.gray{background:#829ab1;}
button.small.danger{background:#c0392b;}
button.small.warn{background:#e67e22;}
button.small.success{background:#27ae60;}
.opts-row{display:flex;gap:18px;flex-wrap:wrap;align-items:center;font-size:15px;color:#334e68;margin:8px 0;}
.opts-row label{display:inline-flex;align-items:center;gap:6px;cursor:pointer;}
.list-box{border:1px solid #d9e2ec;padding:10px;min-height:80px;max-height:240px;overflow-y:auto;margin-top:8px;border-radius:8px;background:#fafbfc;-webkit-overflow-scrolling:touch;}
.student-item{display:inline-flex;align-items:center;padding:6px 10px;margin:3px;background:#e3f2fd;border-radius:16px;font-size:15px;color:#102a43;}
.student-item.drawn{background:#eee;color:#999;text-decoration:line-through;}
.del-stu{color:#c0392b;cursor:pointer;margin-left:8px;font-weight:bold;font-size:18px;line-height:1;padding:0 2px;}
.stu-check{width:18px;height:18px;padding:0;margin:0 8px 0 0;border:none;border-radius:3px;cursor:pointer;flex:none;vertical-align:middle;}
.list-head{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;}
.list-head .hb{display:flex;gap:6px;flex-wrap:wrap;}
.stu-count{font-size:12px;color:#829ab1;font-weight:normal;}
.drawn-hint{font-size:12px;color:#829ab1;}
#fileInput{display:none;}
.foot-note{font-size:12px;color:#829ab1;margin-top:14px;text-align:center;}

/* ============ 响应式:手机 ============ */
@media (max-width:600px){
    body{padding:10px;}
    .header-bar{justify-content:center;}
    .current-class-text{font-size:16px;justify-content:center;width:100%;}
    #topClassSelect{font-size:16px;max-width:200px;}
    .box{padding:12px;}
    #nameBoxWrap{min-height:120px;}
    .tip{margin-top:10px;}
    .modal-box{padding:14px;}
    .row label{min-width:auto;}
}
</style>
</head>
<body>
<div class="wrap">
    <div class="header-bar">
        <div class="current-class-text">
            <span>当前班级:</span>
            <select id="topClassSelect" title="点击切换班级" aria-label="切换班级"><option value="">未选择班级</option></select>
        </div>
        <div class="toolbar">
            <button class="primary" id="settingBtn" type="button">&#9881; 设置</button>
            <button class="success" id="fullBtn" type="button" title="全屏投影更适合课堂">&#9974; 全屏</button>
            <button class="warn" id="resetDrawnBtn" type="button" title="重置当前班级的已抽名单,使所有人重新可抽">&#8634; 重置已抽</button>
        </div>
    </div>

    <div class="box">
        <div id="infoBar">
            <span class="kv" id="statTotal">总人数:0</span>
            <span class="kv" id="statLeft">剩余可抽:0</span>
            <span class="kv" id="statMode">模式:不重复</span>
            <span class="kv" id="statDuration">时长:5秒</span>
            <button id="toggleModeBtn" type="button">切换:可重复 / 不重复</button>
        </div>
        <div id="nameBoxWrap">
            <div id="nameBox" role="status" aria-live="polite">准备抽签</div>
        </div>
        <div id="btnWrap">
            <button id="startBtn" type="button">抽签</button>
        </div>
        <div class="tip">好运伴你行 能量满满哦</div>
    </div>

    <div class="history-box">
        <div class="history-title">
            <span>&#128203; 本次抽签记录</span>
            <button class="small gray" id="clearHistoryBtn" type="button">清空记录</button>
        </div>
        <div class="history-list" id="historyList"><span style="background:none;color:#999;">暂无记录</span></div>
    </div>
    <div class="foot-note">数据保存在本机浏览器,换电脑请用「设置 → 导出/导入 JSON 备份」</div>
</div>

<!-- 设置弹窗 -->
<div class="modal-mask" id="mask" role="dialog" aria-modal="true" aria-label="班级与学生管理">
    <div class="modal-box">
        <div class="close-modal"><button id="closeBtn" type="button">关闭</button></div>
        <h3>班级 & 学生管理</h3>

        <div class="section-title">班级管理</div>
        <div class="row">
            <input id="classNameInput" placeholder="输入班级名称,如:高一2619班" autocomplete="off">
            <button class="small success" id="addClassBtn" type="button">新建班级</button>
        </div>
        <div class="row">
            <select id="classSelect" aria-label="选择班级"></select>
            <button class="small danger" id="delClassBtn" type="button">删除当前班级</button>
            <button class="small warn" id="renameClassBtn" type="button">重命名</button>
        </div>

        <div class="section-title">学生管理</div>
        <div class="row">
            <input id="stuNameInput" placeholder="单个添加:输入学生姓名后回车" autocomplete="off">
            <button class="small" id="addStuBtn" type="button">添加单个</button>
            <button class="small gray" id="clearStuInputBtn" type="button">清空输入</button>
        </div>
        <div class="row">
            <textarea id="batchStuInput" placeholder="批量导入:一行一个名字(也支持用逗号、顿号、空格分隔)张三李四王五"></textarea>
        </div>
        <div class="row">
            <button class="small" id="batchAddBtn" type="button">批量导入(追加)</button>
            <button class="small warn" id="batchReplaceBtn" type="button">批量替换(清空后导入)</button>
            <button class="small danger" id="clearAllStuBtn" type="button">清空当前班级全部学生</button>
        </div>
        <div class="row">
            <button class="small gray" id="resetDrawnModalBtn" type="button">重置当前班级已抽名单</button>
            <span class="drawn-hint">重置后所有人都可以再次被抽到</span>
        </div>

        <div class="section-title">抽签设置</div>
        <div class="row">
            <label>滚动时长:</label>
            <input type="number" id="durationInput" min="1" max="30" step="1" value="5"> 秒
            <label style="margin-left:12px;">单次抽取人数:</label>
            <input type="number" id="multiCountInput" min="1" max="20" step="1" value="1"> 人
            <button class="small" id="saveOptBtn" type="button">保存设置</button>
        </div>
        <div class="opts-row">
            <label><input type="checkbox" id="soundToggle" checked> 开启音效</label>
            <label><input type="checkbox" id="noRepeatToggle" checked> 不重复抽取(抽完自动提示重置)</label>
        </div>

        <div class="section-title">数据备份</div>
        <div class="row">
            <button class="small" id="exportBtn" type="button">导出全部数据(JSON备份)</button>
            <button class="small gray" id="importBtn" type="button">导入JSON备份</button>
            <input type="file" id="fileInput" accept=".json,application/json">
            <span class="drawn-hint">导出可用于换电脑迁移</span>
        </div>

        <div class="section-title list-head">
            <span>当前班级学生名单 <span class="stu-count" id="stuCount"></span></span>
            <span class="hb">
                <button class="small gray" id="selectAllStuBtn" type="button">全选/取消</button>
                <button class="small danger" id="batchDelStuBtn" type="button">删除选中</button>
                <button class="small danger" id="clearListBtn" type="button">清空名单</button>
            </span>
        </div>
        <div class="list-box" id="stuListBox"></div>
    </div>
</div>

<script>
(function(){
"use strict";
/* ================================================================
 *  课堂抽签  ·  HTML5 单文件版
 *  数据结构: classData{班级:[姓名]} / drawnData{班级:[已抽姓名]}
 *            options{duration,multiCount,soundOn,noRepeat} / historyList[]
 * ================================================================ */

/* ---------- 安全存储:localStorage 不可用时自动降级到内存 ---------- */
var memStore = {};
var storageOK = (function(){
    try{
        var k = "__cq_test__";
        window.localStorage.setItem(k, "1");
        window.localStorage.removeItem(k);
        return true;
    }catch(e){ return false; }
})();
var store = {
    get:function(k){
        if(storageOK){ try{ return window.localStorage.getItem(k); }catch(e){} }
        return Object.prototype.hasOwnProperty.call(memStore,k) ? memStore[k] : null;
    },
    set:function(k,v){
        if(storageOK){ try{ window.localStorage.setItem(k,v); return; }catch(e){} }
        memStore[k]=v;
    }
};

var KEYS = {
    DATA:"chouqianClassData", DRAWN:"chouqianDrawnData",
    OPT:"chouqianOptions", LAST:"chouqianLastClass", HISTORY:"chouqianHistory"
};

var classData = {}, drawnData = {}, currentClassName = "", historyList = [];
var options = { duration:5, multiCount:1, soundOn:true, noRepeat:true };

/* ---------- DOM ---------- */
function $(id){ return document.getElementById(id); }
var nameBox=$("nameBox"), startBtn=$("startBtn"), settingBtn=$("settingBtn"),
    fullBtn=$("fullBtn"), resetDrawnBtn=$("resetDrawnBtn"), mask=$("mask"), closeBtn=$("closeBtn"),
    classSelect=$("classSelect"), classNameInput=$("classNameInput"),
    stuNameInput=$("stuNameInput"), batchStuInput=$("batchStuInput"), stuListBox=$("stuListBox"),
    topClassSelect=$("topClassSelect"), statTotal=$("statTotal"), statLeft=$("statLeft"),
    statMode=$("statMode"), statDuration=$("statDuration"), historyBox=$("historyList"),
    durationInput=$("durationInput"), multiCountInput=$("multiCountInput"),
    soundToggle=$("soundToggle"), noRepeatToggle=$("noRepeatToggle"), toggleModeBtn=$("toggleModeBtn");

var rollingTimer=null, countDownTimer=null, remainMs=0, isRolling=false, pendingCount=1;

/* ---------- 存储读写 ---------- */
function loadAll(){
    try{ classData = JSON.parse(store.get(KEYS.DATA)) || {}; }catch(e){ classData = {}; }
    try{ drawnData = JSON.parse(store.get(KEYS.DRAWN)) || {}; }catch(e){ drawnData = {}; }
    try{ var o = JSON.parse(store.get(KEYS.OPT)); if(o && typeof o==="object") mergeOptions(o); }catch(e){}
    try{ var h = JSON.parse(store.get(KEYS.HISTORY)); historyList = Array.isArray(h)?h:[]; }catch(e){ historyList=[]; }
    currentClassName = store.get(KEYS.LAST) || "";
    // 规范化
    Object.keys(classData).forEach(function(k){
        if(!Array.isArray(classData[k])) classData[k]=[];
        if(!Array.isArray(drawnData[k])) drawnData[k]=[];
    });
}
function mergeOptions(o){
    if(typeof o.duration==="number" && o.duration>=1 && o.duration<=30) options.duration=o.duration;
    if(typeof o.multiCount==="number" && o.multiCount>=1 && o.multiCount<=20) options.multiCount=o.multiCount;
    if(typeof o.soundOn==="boolean") options.soundOn=o.soundOn;
    if(typeof o.noRepeat==="boolean") options.noRepeat=o.noRepeat;
}
function saveData(){ store.set(KEYS.DATA, JSON.stringify(classData)); }
function saveDrawn(){ store.set(KEYS.DRAWN, JSON.stringify(drawnData)); }
function saveOptions(silent){
    var d=parseInt(durationInput.value,10), m=parseInt(multiCountInput.value,10);
    options.duration = (isNaN(d)||d<1)?5:Math.min(30,d);
    options.multiCount = (isNaN(m)||m<1)?1:Math.min(20,m);
    options.soundOn = soundToggle.checked;
    options.noRepeat = noRepeatToggle.checked;
    store.set(KEYS.OPT, JSON.stringify(options));
    durationInput.value=options.duration; multiCountInput.value=options.multiCount;
    syncOptionUI();
    if(!silent) toast("设置已保存!");
}
function saveLast(){ store.set(KEYS.LAST, currentClassName); }
function saveHistory(){ store.set(KEYS.HISTORY, JSON.stringify(historyList)); }

/* ---------- 轻提示(替代频繁 alert) ---------- */
var toastTimer=null;
function toast(msg){
    var t=$("__toast__");
    if(!t){
        t=document.createElement("div"); t.id="__toast__";
        t.style.cssText="position:fixed;left:50%;bottom:40px;transform:translateX(-50%);"+
            "background:rgba(16,42,67,.92);color:#fff;padding:12px 20px;border-radius:10px;"+
            "font-size:15px;z-index:2000;max-width:88vw;box-shadow:0 6px 20px rgba(0,0,0,.3);"+
            "opacity:0;transition:opacity .2s;pointer-events:none;";
        document.body.appendChild(t);
    }
    t.textContent=msg; t.style.opacity="1";
    clearTimeout(toastTimer);
    toastTimer=setTimeout(function(){ t.style.opacity="0"; },1800);
}

/* ---------- 音效(Web Audio,含特性检测 + webkit 前缀 + resume) ---------- */
var audioCtx=null;
function ensureAudio(){
    if(audioCtx) return true;
    try{
        var AC = window.AudioContext || window.webkitAudioContext;
        if(!AC) return false;
        audioCtx = new AC();
        return true;
    }catch(e){ return false; }
}
function resumeAudio(){
    if(audioCtx && audioCtx.state==="suspended" && audioCtx.resume){ audioCtx.resume().catch(function(){}); }
}
function playTick(){
    if(!options.soundOn || !ensureAudio()) return; resumeAudio();
    try{
        var t0=audioCtx.currentTime, osc=audioCtx.createOscillator(), g=audioCtx.createGain();
        osc.type="sine"; osc.frequency.value=880;
        g.gain.setValueAtTime(0.0001,t0);
        g.gain.exponentialRampToValueAtTime(0.06,t0+0.005);
        g.gain.exponentialRampToValueAtTime(0.0001,t0+0.05);
        osc.connect(g); g.connect(audioCtx.destination);
        osc.start(t0); osc.stop(t0+0.06);
    }catch(e){}
}
function playBell(strikes){
    if(!options.soundOn || !ensureAudio()) return; resumeAudio();
    try{
        var master=audioCtx.createGain(); master.gain.value=0.8; master.connect(audioCtx.destination);
        var base=audioCtx.currentTime+0.01;
        var partials=[{r:1,g:1,d:1.3},{r:2,g:0.45,d:0.85},{r:2.76,g:0.28,d:0.55},{r:4,g:0.16,d:0.32}];
        function strike(t0,f0,level){
            partials.forEach(function(p){
                var osc=audioCtx.createOscillator(), gain=audioCtx.createGain();
                osc.type="sine"; osc.frequency.value=f0*p.r;
                var peak=p.g*0.26*level;
                gain.gain.setValueAtTime(0.0001,t0);
                gain.gain.exponentialRampToValueAtTime(peak,t0+0.004);
                gain.gain.exponentialRampToValueAtTime(0.0001,t0+p.d);
                osc.connect(gain); gain.connect(master);
                osc.start(t0); osc.stop(t0+p.d+0.02);
            });
        }
        strike(base,1318.5,1.0);
        if(strikes===2) strike(base+0.30,784.0,0.75);
    }catch(e){}
}
function vibrate(pattern){ try{ if(navigator.vibrate) navigator.vibrate(pattern); }catch(e){} }

/* ---------- 导入 / 导出 ---------- */
function exportData(){
    try{
        var payload={classData:classData,drawnData:drawnData,options:options,exportAt:new Date().toISOString()};
        var blob=new Blob([JSON.stringify(payload,null,2)],{type:"application/json"});
        var url=URL.createObjectURL(blob), a=document.createElement("a");
        var n=new Date(), t=n.getFullYear()+pad(n.getMonth()+1)+pad(n.getDate());
        a.href=url; a.download="课堂抽签备份_"+t+".json";
        document.body.appendChild(a); a.click(); document.body.removeChild(a);
        setTimeout(function(){ URL.revokeObjectURL(url); },1000);
        toast("已导出备份 JSON 文件");
    }catch(e){ alert("导出失败:"+e.message); }
}
function importData(ev){
    var file=ev.target.files && ev.target.files[0];
    if(!file) return;
    var reader=new FileReader();
    reader.onload=function(e){
        try{
            var obj=JSON.parse(e.target.result);
            if(obj && obj.classData){
                classData=obj.classData;
                if(obj.drawnData) drawnData=obj.drawnData; else drawnData={};
                if(obj.options) mergeOptions(obj.options);
            }else if(obj && typeof obj==="object" && !Array.isArray(obj)){
                classData=obj; drawnData={}; // 兼容旧备份
            }else{ throw new Error("文件格式不正确"); }
            Object.keys(classData).forEach(function(k){
                if(!Array.isArray(classData[k])) classData[k]=[];
                if(!Array.isArray(drawnData[k])) drawnData[k]=[];
            });
            saveData(); saveDrawn(); store.set(KEYS.OPT,JSON.stringify(options));
            $("fileInput").value="";
            var keys=Object.keys(classData);
            currentClassName = classData[currentClassName] ? currentClassName : (keys[0]||"");
            saveLast(); syncOptionUI(); renderClassSelect(); renderHistory(); refreshStat();
            toast("导入备份成功!");
        }catch(err){ alert("文件解析失败,请选择正确的抽签备份 JSON:\n"+err.message); }
    };
    reader.readAsText(file);
}
function pad(x){ return (x<10?"0":"")+x; }

/* ---------- 渲染 ---------- */
function renderClassSelect(){
    var keys=Object.keys(classData);
    classSelect.innerHTML=""; topClassSelect.innerHTML="";
    if(keys.length===0){
        var o=document.createElement("option"); o.value=""; o.textContent="未选择班级";
        topClassSelect.appendChild(o); currentClassName="";
    }else{
        if(!classData[currentClassName]) currentClassName=keys[0];
        keys.forEach(function(name){
            var a=document.createElement("option"); a.value=name; a.textContent=name; classSelect.appendChild(a);
            var b=document.createElement("option"); b.value=name; b.textContent=name; topClassSelect.appendChild(b);
        });
        classSelect.value=currentClassName; topClassSelect.value=currentClassName;
    }
    saveLast(); renderStudentList(); refreshStat();
}
function renderStudentList(){
    stuListBox.innerHTML="";
    var cnt=$("stuCount");
    if(!classData[currentClassName]){
        if(cnt) cnt.textContent="";
        stuListBox.innerHTML='<span style="color:#999;font-size:13px;">请先新建或选择班级</span>'; return;
    }
    var stus=classData[currentClassName];
    var drawnArr=drawnData[currentClassName]||[];
    if(!stus.length){
        if(cnt) cnt.textContent="(共 0 人)";
        stuListBox.innerHTML='<span style="color:#999;font-size:13px;">当前班级暂无学生,请添加</span>'; return;
    }
    if(cnt) cnt.textContent="(共 "+stus.length+" 人,已抽 "+drawnArr.length+" 人)";
    var drawnSet={}; drawnArr.forEach(function(n){ drawnSet[n]=true; });
    stus.forEach(function(name,idx){
        var span=document.createElement("span");
        span.className="student-item"+(drawnSet[name]?" drawn":"");
        var cb=document.createElement("input");
        cb.type="checkbox"; cb.className="stu-check"; cb.setAttribute("data-idx",idx);
        cb.setAttribute("aria-label","选择 "+name); cb.title="勾选 "+name;
        var txt=document.createElement("span"); txt.textContent=name;
        var del=document.createElement("span"); del.className="del-stu"; del.textContent="×"; del.title="删除 "+name;
        del.setAttribute("role","button");
        del.onclick=function(){ delStudent(idx); };
        span.appendChild(cb); span.appendChild(txt); span.appendChild(del); stuListBox.appendChild(span);
    });
}
function refreshStat(){
    var list=classData[currentClassName];
    if(!list){ statTotal.textContent="总人数:0"; statLeft.textContent="剩余可抽:0"; }
    else{
        var total=list.length, drawn=(drawnData[currentClassName]||[]).length;
        statTotal.textContent="总人数:"+total;
        statLeft.textContent = options.noRepeat ? ("剩余可抽:"+Math.max(0,total-drawn)) : ("可抽:"+total);
    }
    statMode.textContent = options.noRepeat?"模式:不重复":"模式:可重复";
    statDuration.textContent = "时长:"+options.duration+"秒";
    toggleModeBtn.textContent = options.noRepeat?"当前:不重复(点击切可重复)":"当前:可重复(点击切不重复)";
}
function escapeHtml(s){
    return String(s).replace(/[&<>"']/g,function(c){
        return {"&":"&","<":"<",">":">",'"':""","'":"'"}[c];
    });
}
function renderHistory(){
    if(!historyList.length){
        historyBox.innerHTML='<span style="background:none;color:#999;">暂无记录</span>'; return;
    }
    historyBox.innerHTML = historyList.slice().reverse().map(function(it){
        var names=(it.names||[]).map(escapeHtml).join("、");
        return '<span><b>['+escapeHtml(it.time||"")+']</b> '+escapeHtml(it.cls||"")+" → "+names+'</span>';
    }).join("");
}
function syncOptionUI(){
    durationInput.value=options.duration; multiCountInput.value=options.multiCount;
    soundToggle.checked=options.soundOn; noRepeatToggle.checked=options.noRepeat;
    refreshStat();
}
function resetNameBox(text){
    nameBox.textContent = (text===undefined?"":text);
    nameBox.classList.remove("selected");
}

/* ---------- 班级 / 学生操作 ---------- */
function switchToClass(name){
    if(isRolling) stopRoll(true); // 切换时若在滚动,安全停止
    currentClassName=name; classSelect.value=name; topClassSelect.value=name;
    saveLast(); renderStudentList(); refreshStat(); resetNameBox("准备抽签");
}
function addClass(){
    var c=(classNameInput.value||"").trim();
    if(!c){ alert("请输入班级名称"); return; }
    if(classData[c]){ alert("班级已存在"); return; }
    classData[c]=[]; drawnData[c]=[]; currentClassName=c;
    saveData(); saveDrawn(); saveLast(); renderClassSelect();
    classNameInput.value=""; toast("已新建班级:"+c);
}
function delClass(){
    if(!currentClassName){ alert("当前没有可删除的班级"); return; }
    if(!confirm("确定删除班级【"+currentClassName+"】吗?名单与已抽记录将全部清除。")) return;
    delete classData[currentClassName]; delete drawnData[currentClassName];
    saveData(); saveDrawn();
    var keys=Object.keys(classData); currentClassName=keys[0]||"";
    saveLast(); renderClassSelect(); resetNameBox("准备抽签"); toast("已删除班级");
}
function renameClass(){
    if(!currentClassName){ alert("请先选择要重命名的班级"); return; }
    var nn=(prompt("请输入新的班级名称:",currentClassName)||"").trim();
    if(!nn || nn===currentClassName) return;
    if(classData[nn]){ alert("已存在同名班级"); return; }
    classData[nn]=classData[currentClassName];
    drawnData[nn]=drawnData[currentClassName]||[];
    delete classData[currentClassName]; delete drawnData[currentClassName];
    currentClassName=nn; saveData(); saveDrawn(); saveLast(); renderClassSelect(); toast("已重命名");
}
function addStudent(){
    if(!currentClassName){ alert("请先新建/选择班级"); return; }
    var s=(stuNameInput.value||"").trim();
    if(!s){ alert("请输入学生姓名"); return; }
    if(classData[currentClassName].indexOf(s)>=0){
        if(!confirm('当前班级已存在"'+s+'",仍然添加?(重名会造成识别歧义)')) return;
    }
    classData[currentClassName].push(s);
    saveData(); renderStudentList(); refreshStat();
    stuNameInput.value=""; stuNameInput.focus();
}
function parseBatch(text){
    return String(text||"").split(/[\r\n,,、;;\t ]+/).map(function(s){return s.trim();}).filter(function(s){return s;});
}
function batchAdd(){
    if(!currentClassName){ alert("请先新建/选择班级!"); return; }
    var arr=parseBatch(batchStuInput.value);
    if(!arr.length){ alert("没有读取到学生姓名,请检查输入!"); return; }
    Array.prototype.push.apply(classData[currentClassName],arr);
    saveData(); renderStudentList(); refreshStat(); batchStuInput.value="";
    toast("成功追加 "+arr.length+" 名学生!");
}
function batchReplace(){
    if(!currentClassName){ alert("请先新建/选择班级!"); return; }
    var arr=parseBatch(batchStuInput.value);
    if(!arr.length){ alert("没有读取到学生姓名,请检查输入!"); return; }
    if(!confirm("将清空当前班级全部学生,并导入 "+arr.length+" 名新学生,同时重置已抽名单。确定吗?")) return;
    classData[currentClassName]=arr; drawnData[currentClassName]=[];
    saveData(); saveDrawn(); renderStudentList(); refreshStat(); batchStuInput.value="";
    toast("已替换为 "+arr.length+" 名学生!");
}
function clearAllStudents(){
    if(!currentClassName) return;
    if(!confirm("确定清空当前班级的所有学生吗?已抽记录也将一并重置。")) return;
    classData[currentClassName]=[]; drawnData[currentClassName]=[];
    saveData(); saveDrawn(); renderStudentList(); refreshStat(); toast("已清空学生");
}
function delStudent(idx){
    if(!classData[currentClassName]) return;
    var removed=classData[currentClassName].splice(idx,1)[0];
    if(removed && drawnData[currentClassName]){
        drawnData[currentClassName]=drawnData[currentClassName].filter(function(n){return n!==removed;});
    }
    saveData(); saveDrawn(); renderStudentList(); refreshStat();
}
function getCheckedIdx(){
    var boxes=stuListBox.querySelectorAll(".stu-check:checked"), out=[];
    Array.prototype.forEach.call(boxes,function(b){
        var i=parseInt(b.getAttribute("data-idx"),10); if(!isNaN(i)) out.push(i);
    });
    return out;
}
function toggleSelectAll(){
    var boxes=stuListBox.querySelectorAll(".stu-check");
    if(!boxes.length){ toast("当前没有可勾选的学生"); return; }
    var anyUnchecked=Array.prototype.some.call(boxes,function(b){ return !b.checked; });
    Array.prototype.forEach.call(boxes,function(b){ b.checked=anyUnchecked; });
}
function batchDeleteSelected(){
    if(!classData[currentClassName]){ alert("请先选择班级"); return; }
    var idxs=getCheckedIdx();
    if(!idxs.length){ alert("请先勾选要删除的学生(可点「全选/取消」快速全选)"); return; }
    if(!confirm("确定删除选中的 "+idxs.length+" 名学生吗?(同时从已抽名单移除)")) return;
    idxs.sort(function(a,b){ return b-a; }); // 从后往前删,避免下标错位
    var removed=[];
    idxs.forEach(function(i){
        if(i>=0 && i<classData[currentClassName].length){
            var nm=classData[currentClassName].splice(i,1)[0];
            if(nm!==undefined) removed.push(nm);
        }
    });
    if(drawnData[currentClassName] && removed.length){
        var set={}; removed.forEach(function(n){ set[n]=true; });
        drawnData[currentClassName]=drawnData[currentClassName].filter(function(n){ return !set[n]; });
    }
    saveData(); saveDrawn(); renderStudentList(); refreshStat();
    toast("已删除选中的 "+removed.length+" 名学生");
}
function resetDrawnInClass(){
    if(!currentClassName){ alert("请先选择班级"); return; }
    if(!confirm("确定重置当前班级的已抽名单?重置后所有人都可以再次被抽到。")) return;
    drawnData[currentClassName]=[]; saveDrawn(); renderStudentList(); refreshStat(); toast("已重置已抽名单");
}

/* ---------- 抽签核心 ---------- */
function getCandidates(){
    var list=classData[currentClassName]||[];
    if(!options.noRepeat) return list.slice();
    var drawnSet={}; (drawnData[currentClassName]||[]).forEach(function(n){drawnSet[n]=true;});
    return list.filter(function(n){return !drawnSet[n];});
}
function pickRandom(arr){ return arr[Math.floor(Math.random()*arr.length)]; }

function startDraw(count){
    var list=classData[currentClassName];
    if(!list || !list.length){ alert("当前班级还没有学生,请点击「设置」添加!"); return; }
    var c=Math.max(1,count||options.multiCount||1);
    var candidates=getCandidates();
    if(options.noRepeat && !candidates.length){
        alert("班级【"+currentClassName+"】所有人都已抽过了!请点击「重置已抽」开始新一轮。"); return;
    }
    var poolSize = options.noRepeat ? candidates.length : list.length;
    if(c>poolSize){
        alert("可抽人数("+poolSize+")少于要抽取的人数("+c+")。请减少抽取人数或重置已抽。"); return;
    }
    pendingCount=c;
    resetNameBox(""); isRolling=true;
    remainMs=options.duration*1000;
    startBtn.textContent=(remainMs/1000).toFixed(1)+" 秒";
    var pool = options.noRepeat ? candidates : list;

    rollingTimer=setInterval(function(){
        if(c===1){ nameBox.textContent=pickRandom(pool); }
        else{
            var set={},out=[];
            while(out.length<Math.min(c,pool.length)){
                var p=pickRandom(pool);
                if(!set[p]){ set[p]=true; out.push(p); }
            }
            nameBox.textContent=out.join("  ·  ");
        }
        playTick();
    },80);

    countDownTimer=setInterval(function(){
        remainMs-=50;
        startBtn.textContent=(Math.max(0,remainMs)/1000).toFixed(1)+" 秒";
        if(remainMs<=0) stopRoll(false);
    },50);
}
function stopRoll(cancel){
    clearInterval(rollingTimer); clearInterval(countDownTimer);
    rollingTimer=countDownTimer=null; isRolling=false;
    startBtn.textContent="抽签";
    if(cancel){ resetNameBox("准备抽签"); return; }

    var c=pendingCount||1, candidates=getCandidates();
    var pool=(options.noRepeat && candidates.length)?candidates:(classData[currentClassName]||[]).slice();
    var picked=[], pickPool=pool.slice(), n=Math.min(c,pickPool.length);
    for(var i=0;i<n;i++){
        var idx=Math.floor(Math.random()*pickPool.length);
        picked.push(pickPool.splice(idx,1)[0]);
    }
    if(!picked.length){ resetNameBox("无可抽人员"); return; }

    if(options.noRepeat){
        if(!drawnData[currentClassName]) drawnData[currentClassName]=[];
        picked.forEach(function(nm){ if(drawnData[currentClassName].indexOf(nm)<0) drawnData[currentClassName].push(nm); });
        saveDrawn();
    }
    nameBox.textContent = picked.length===1 ? picked[0] : picked.join("  ·  ");
    nameBox.classList.add("selected");
    playBell(picked.length>1?1:2); vibrate(picked.length>1?[60,40,60]:[80]);

    var d=new Date(), ts=pad(d.getHours())+":"+pad(d.getMinutes())+":"+pad(d.getSeconds());
    historyList.push({time:ts,cls:currentClassName,names:picked});
    if(historyList.length>200) historyList=historyList.slice(-200);
    saveHistory(); renderHistory(); renderStudentList(); refreshStat();

    if(options.noRepeat && getCandidates().length===0){
        setTimeout(function(){ toast("本轮已全部抽完,可点「重置已抽」开始新一轮"); },400);
    }
}

/* ---------- 全屏(含 webkit 前缀兼容) ---------- */
function toggleFullscreen(){
    var el=document.documentElement;
    var isFull=document.fullscreenElement||document.webkitFullscreenElement||document.mozFullScreenElement||document.msFullscreenElement;
    try{
        if(!isFull){
            if(el.requestFullscreen) el.requestFullscreen();
            else if(el.webkitRequestFullscreen) el.webkitRequestFullscreen();
            else if(el.msRequestFullscreen) el.msRequestFullscreen();
            else toast("当前浏览器不支持全屏");
        }else{
            if(document.exitFullscreen) document.exitFullscreen();
            else if(document.webkitExitFullscreen) document.webkitExitFullscreen();
            else if(document.msExitFullscreen) document.msExitFullscreen();
        }
    }catch(e){ toast("全屏切换失败"); }
}

/* ---------- 弹窗 ---------- */
function openModal(){ syncOptionUI(); renderClassSelect(); mask.classList.add("show"); }
function closeModal(){ mask.classList.remove("show"); }

/* ---------- 事件绑定 ---------- */
function bind(){
    startBtn.addEventListener("click",function(){
        ensureAudio(); resumeAudio();
        if(isRolling){ stopRoll(false); } else { startDraw(options.multiCount); }
    });
    settingBtn.addEventListener("click",openModal);
    closeBtn.addEventListener("click",closeModal);
    mask.addEventListener("click",function(e){ if(e.target===mask) closeModal(); });
    fullBtn.addEventListener("click",toggleFullscreen);
    resetDrawnBtn.addEventListener("click",resetDrawnInClass);

    topClassSelect.addEventListener("change",function(){ if(topClassSelect.value) switchToClass(topClassSelect.value); });
    classSelect.addEventListener("change",function(){ if(classSelect.value) switchToClass(classSelect.value); });

    toggleModeBtn.addEventListener("click",function(){
        options.noRepeat=!options.noRepeat; noRepeatToggle.checked=options.noRepeat;
        store.set(KEYS.OPT,JSON.stringify(options)); refreshStat();
        toast(options.noRepeat?"已切换为【不重复】:抽过的人不再被抽到":"已切换为【可重复】:每次从全员随机");
    });
    $("clearHistoryBtn").addEventListener("click",function(){
        if(!confirm("清空本次抽签记录?(不影响名单和已抽状态)")) return;
        historyList=[]; saveHistory(); renderHistory();
    });

    $("addClassBtn").addEventListener("click",addClass);
    $("delClassBtn").addEventListener("click",delClass);
    $("renameClassBtn").addEventListener("click",renameClass);
    $("addStuBtn").addEventListener("click",addStudent);
    $("clearStuInputBtn").addEventListener("click",function(){ stuNameInput.value=""; batchStuInput.value=""; stuNameInput.focus(); });
    $("batchAddBtn").addEventListener("click",batchAdd);
    $("batchReplaceBtn").addEventListener("click",batchReplace);
    $("clearAllStuBtn").addEventListener("click",clearAllStudents);
    $("selectAllStuBtn").addEventListener("click",toggleSelectAll);
    $("batchDelStuBtn").addEventListener("click",batchDeleteSelected);
    $("clearListBtn").addEventListener("click",clearAllStudents);
    $("resetDrawnModalBtn").addEventListener("click",resetDrawnInClass);
    $("saveOptBtn").addEventListener("click",function(){ saveOptions(false); });
    $("exportBtn").addEventListener("click",exportData);
    $("importBtn").addEventListener("click",function(){ $("fileInput").click(); });
    $("fileInput").addEventListener("change",importData);

    classNameInput.addEventListener("keydown",function(e){ if(e.key==="Enter"){ e.preventDefault(); addClass(); } });
    stuNameInput.addEventListener("keydown",function(e){ if(e.key==="Enter"){ e.preventDefault(); addStudent(); } });

    // 键盘快捷键:空格抽签、Esc 关弹窗(输入框内不拦截空格)
    document.addEventListener("keydown",function(e){
        var tag=(e.target && e.target.tagName||"").toLowerCase();
        var typing = tag==="input"||tag==="textarea"||tag==="select";
        if(e.key==="Escape"){ if(mask.classList.contains("show")) closeModal(); return; }
        if((e.code==="Space"||e.key===" ") && !typing && !mask.classList.contains("show")){
            e.preventDefault(); startBtn.click();
        }
    });
    // 页面不可见时停止滚动,避免后台空转
    document.addEventListener("visibilitychange",function(){ if(document.hidden && isRolling) stopRoll(true); });
}

/* ---------- 初始化 ---------- */
function init(){
    loadAll();
    if(!Object.keys(classData).length){
        var demo=[]; for(var i=1;i<=50;i++) demo.push("学生"+i);
        classData["示例班级"]=demo; drawnData["示例班级"]=[]; currentClassName="示例班级";
        saveData(); saveDrawn(); saveLast();
    }else if(!classData[currentClassName]){
        currentClassName=Object.keys(classData)[0]||""; saveLast();
    }
    bind();
    syncOptionUI();
    renderClassSelect();
    renderHistory();
    refreshStat();
}
if(document.readyState==="loading") document.addEventListener("DOMContentLoaded",init);
else init();
})();
</script>
</body>
</html>



免费评分

参与人数 3吾爱币 +3 热心值 +3 收起 理由
快乐天使2 + 1 + 1 用心讨论,共获提升!
wxqfy + 1 + 1 感谢大佬出手打包分享
Simon_iln + 1 + 1 用不上,但有个老师朋友

查看全部评分

10#
Tomyi 发表于 2026-10-20 13:34
直接复制代码不行,点了都没反应
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-10-22 11:02

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表