智能学号抽取系统 V3.7.5 —— 一个基于 Vue.js 的交互式网页应用
智能学号抽取系统 V3.7.5 —— 一个基于 Vue.js 的交互式网页应用
在教学和课堂管理中,公平、随机地抽取学生是教师常见的需求。为了满足这一场景,我开发了一款功能强大、界面美观、操作简便的智能学号抽取系统(版本 V3.7.5),该系统使用 HTML + CSS + Vue.js 构建,支持多种抽取模式、概率权重设置以及历史记录等功能。
🌟 系统亮点
- 多种抽取模式:支持单次抽取与快速滚动抽取,适用于不同教学场景。
- 批量抽取功能:一次抽取多个学号,提升效率。
- 自定义概率权重:为不同的学号区间设置不同的抽取概率,实现“重点抽查”或“均匀分布”等策略。
- 响应式设计:适配手机和平板设备,随时随地使用。
- 动画与视觉反馈:丰富的按钮交互效果、庆祝动画、历史记录高亮等增强用户体验。
- URL 参数保存配置:方便分享或重复使用特定设置。
🧩 技术架构
本系统采用Vue.js 3 Composition API进行状态管理与页面渲染,结合现代 CSS 技术(如 Flexbox、Grid、CSS 变量、过渡动画等),实现了良好的可维护性与扩展性。
前端资源引入如下:
- Font Awesome 提供图标支持。
- 使用 CDN 引入 Vue.js。
- 所有逻辑封装在单个 HTML 文件中,便于部署和分发。
📐 功能详解
0.系统截图
1. 学号范围设置
用户可以输入起始与结束学号,并选择抽取模式:
- 单次抽取模式(d):点击按钮手动抽取一个学号。
- 快速抽取模式(s):连续滚动显示随机学号,点击停止后记录当前结果。
2. 概率权重设置(高级功能)
通过展开“高级设置”,用户可以添加多个学号区间并为其设置不同的抽取概率权重。例如:
- 1~20 权重 80%
- 21~40 权重 20%
这样可以在不完全随机的前提下控制抽取倾向,适用于需要侧重某些学生的场景。
3. 历史记录展示
每次抽取的结果都会记录在历史区域中,最新抽取的学号会以动画形式突出显示。
4. 批量抽取
支持一次性抽取多个学号,适合小组活动、点名等场合,提高效率。
5. 庆祝动画
当所有学号都被抽取完毕时,系统会自动触发一个烟花般的彩纸动画,并弹出提示信息,增加互动趣味性。
💡 使用建议
- 教师课堂点名:公平、高效地随机抽取学生回答问题。
- 小组活动分配:批量抽取若干学生组成临时小组。
- 教学演示工具:用于讲解概率、统计等数学概念的实际应用案例。
- 游戏化教学:结合奖励机制,提升学生参与积极性。
🔧 开发说明
本项目完全开源,无需服务器即可运行,只需将代码保存为 .html
文件并在浏览器中打开即可使用。非常适合嵌入到学校管理系统、教学平台或作为独立工具使用。
📄 示例代码(完整版)
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>智能学号抽取系统V3.7.5</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-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;--transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);}body {display: flex;justify-content: center;align-items: center;min-height: 100vh;margin: 0;font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);color: var(--text-dark);}.container {background: white;padding: 2.5rem 3rem;border-radius: 16px;box-shadow: var(--card-shadow);transition: var(--transition);width: min(90%, 600px);text-align: center;position: relative;overflow: hidden;}/* 动态渐变顶部装饰条 */.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;}@keyframes gradientFlow {0% { background-position: 0% 50%; }100% { background-position: 100% 50%; }}h1 {color: var(--text-dark);margin: 0 0 2rem 0;font-weight: 600;position: relative;padding-bottom: 1rem;font-size: 1.8rem;}h1::after {content: "";position: absolute;bottom: 0;left: 50%;transform: translateX(-50%);width: 80px;height: 4px;background: var(--primary-color);border-radius: 2px;}input[type="number"], select {border: 2px solid var(--border-color);border-radius: 8px;padding: 0.8rem 1rem;width: 160px;transition: var(--transition);font-size: 1.1rem;color: var(--text-dark);font-weight: 500;}input[type="number"]:focus,select:focus {border-color: var(--primary-color);box-shadow: 0 0 0 4px rgba(67, 97, 238, 0.1);outline: none;}select {appearance: none;background: white 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") no-repeat right 12px center/16px;padding-right: 2.5rem;}/* 增强数字区域立体效果 */.number-display {font-size: 300pt; font-weight: normal;text-align: center;border: none;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);color: #2c3e50;transition: var(--transition);height: auto;line-height: 1;text-shadow: 0 2px 4px rgba(0,0,0,0.1);position: relative;}.number-display.empty {color: var(--text-light);background: #f8f9fa;}/* 增强按钮交互效果 */.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: var(--transition);border: none;position: relative;overflow: hidden;font-weight: 600;letter-spacing: 0.5px;display: inline-flex;align-items: center;justify-content: center;gap: 0.5rem;transform-style: preserve-3d;transition: all 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28);}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);background: white;transform: translateY(-2px);}button.danger {background: var(--danger-color);color: white;}button.danger:hover {background: #e5177b;}button.warning {background: var(--warning-color);color: white;}button.warning:hover {background: #e07e0f;}/* 按钮按压效果 */button:active {transform: translateY(4px) scale(0.98);box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;}label {display: inline-block;min-width: 100px;text-align: right;margin-right: 1rem;color: var(--text-dark);font-size: 1.1rem;font-weight: 500;}.form-group {margin: 1.5rem 0;display: flex;align-items: center;justify-content: center;}.history {margin-top: 2rem;padding: 1rem;background: var(--bg-color);border-radius: 8px;max-height: 120px;overflow-y: auto;}.history-title {font-size: 0.9rem;color: var(--text-light);margin-bottom: 0.5rem;display: flex;justify-content: space-between;align-items: center;}.history-items {display: flex;flex-wrap: wrap;gap: 0.5rem;}.history-item {background: white;padding: 0.3rem 0.8rem;border-radius: 20px;font-size: 0.9rem;box-shadow: 0 1px 3px rgba(0,0,0,0.1);transition: all 0.3s ease;}.history-item.latest {background: var(--success-color) !important;color: white !important;transform: scale(1.1);animation: pulse 1s infinite alternate;}@keyframes pulse {from { transform: scale(1); }to { transform: scale(1.1); }}/* 动画效果 */.fade-enter-active,.fade-leave-active {transition: opacity 0.3s ease, transform 0.3s ease;}.fade-enter-from,.fade-leave-to {opacity: 0;transform: translateY(10px);}.slide-enter-active,.slide-leave-active {transition: all 0.3s ease;max-height: 500px;overflow: hidden;}.slide-enter-from,.slide-leave-to {opacity: 0;max-height: 0;}/* 按钮加载动画 */.loading-button::after {content: '';position: absolute;top: 0;left: 0;height: 100%;width: 100%;background: rgba(255, 255, 255, 0.3);animation: loadingPulse 1.5s infinite;}@keyframes loadingPulse {0% { opacity: 0.3; }50% { opacity: 0.7; }100% { opacity: 0.3; }}/* 高级设置 */.advanced-settings {margin: 1rem 0;text-align: left;}.toggle-advanced {background: none;border: none;color: var(--primary-color);cursor: pointer;font-size: 0.9rem;display: flex;align-items: center;gap: 0.5rem;padding: 0.5rem;margin: 0 auto;}.settings-panel {padding: 1rem;background: #f5f7fa;border-radius: 8px;margin-top: 0.5rem;}.range-control {display: flex;gap: 0.5rem;align-items: center;margin-bottom: 0.5rem;flex-wrap: wrap;justify-content: center;}.range-control input {width: 80px !important;padding: 0.5rem !important;}.range-control button {padding: 0.5rem 1rem !important;font-size: 0.9rem !important;}/* 庆祝动画 */.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 cubic-bezier(0.175, 0.885, 0.32, 1.275);}@keyframes zoomIn {from { transform: scale(0.5); opacity: 0; }to { transform: scale(1); 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;}.number-display {font-size: 180pt;margin: 15px 0;}.button-group button {padding: 0.8rem 1rem;font-size: 1rem;}.range-control {flex-direction: column;align-items: flex-start;}.range-control input {width: 100% !important;}}</style>
</head>
<body><div id="app" class="container"><transition name="fade" mode="out-in"><div v-if="isIndexPage" key="setup"><h1>学号抽取设置</h1><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 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="advanced-settings"><button class="toggle-advanced" @click="showAdvanced = !showAdvanced"><i :class="['fas', showAdvanced ? 'fa-chevron-up' : 'fa-chevron-down']"></i>{{ showAdvanced ? '隐藏高级设置' : '显示高级设置' }}</button><transition name="slide"><div v-if="showAdvanced" class="settings-panel"><h3><i class="fas fa-cog"></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="1" :max="end"><span>至</span><input v-model.number="range.end" type="number" placeholder="结束" min="1" :max="end"><span>权重</span><input v-model.number="range.weight" type="number" placeholder="权重" min="1"><span>%</span><button class="danger" @click="removeRange(index)"><i class="fas fa-trash"></i></button></div><button class="primary" @click="addRange"><i class="fas fa-plus"></i> 添加范围</button></div></transition></div><div class="button-group"><button class="primary" @click="validateAndNavigate"><i class="fas fa-play"></i><span>开始抽取</span></button></div></div><div v-else key="main"><h1>学号抽取结果</h1><div class="number-display" :class="{ 'empty': currentNumber === '—' }">{{ currentNumber }}</div><div class="button-group"><button v-if="mode === 'd'" class="primary" @click="drawNumber"><i class="fas fa-dice"></i><span>抽取学号</span></button><button v-if="mode === 'd'" class="warning" @click="showBatchSettings = !showBatchSettings"><i class="fas fa-users"></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="secondary" @click="goBack"><i class="fas fa-arrow-left"></i><span>返回设置</span></button><button v-if="usedNumbers.length > 0" class="danger" @click="resetUsedNumbers"><i class="fas fa-sync-alt"></i><span>重置记录</span></button></div><!-- 批量抽取设置面板 --><transition name="slide"><div v-if="showBatchSettings" class="settings-panel"><h3><i class="fas fa-users"></i> 批量抽取设置</h3><div class="form-group"><label for="batchSize">抽取人数:</label><input type="number" v-model.number="batchSize" id="batchSize" min="1" :max="end - start + 1 - usedNumbers.length"></div><button class="primary" @click="drawBatchNumbers"><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 }}/{{ end - start + 1 }})</span><button @click="clearHistory" style="background:none;border:none;color:var(--text-light);font-size:0.8rem;"><i class="fas fa-trash"></i> 清除</button></div><div class="history-items"><span v-for="(num, index) in usedNumbers" :key="num" class="history-item":class="{ 'latest': index === usedNumbers.length - 1 }">{{ num }}</span></div></div></div></transition><!-- 庆祝动画 --><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><script src="https://cdn.jsdelivr.net/npm/vue@3.2.47/dist/vue.global.min.js"></script><script>const { createApp, ref, computed, watch, onMounted } = Vue;createApp({setup() {const start = ref(1);const end = ref(40);const mode = ref('d');const currentNumber = ref('—');const isIndexPage = ref(true);const usedNumbers = ref([]);const isContinuous = ref(false);const animationFrameId = ref(null);const showAdvanced = ref(false);const probabilityRanges = ref([]);const showCelebration = ref(false);const celebrationMessage = ref('所有学号已抽取完成!');let celebrationTimeout = null;// 新增批量抽取相关状态const batchSize = ref(1);const showBatchSettings = ref(false);const allDrawn = computed(() => {return usedNumbers.value.length === (end.value - start.value + 1);});const getAllNumbers = () => {const numbers = [];for (let i = start.value; i <= end.value; i++) {numbers.push(i);}return numbers;};const getRandomColor = () => {const colors = ['#4361ee', '#4cc9f0', '#f72585', '#f8961e', '#7209b7', '#3a86ff'];return colors[Math.floor(Math.random() * colors.length)];};const getRandomNumber = () => {const available = getAllNumbers().filter(n => !usedNumbers.value.includes(n));if (available.length === 0) return null;const index = Math.floor(Math.random() * available.length);return available[index];};const getWeightedRandomNumber = () => {// 先检查是否有可用数字const available = getAllNumbers().filter(n => !usedNumbers.value.includes(n));if (available.length === 0) return null;// 如果没有设置概率范围或范围无效,使用均匀随机if (probabilityRanges.value.length === 0) {return available[Math.floor(Math.random() * available.length)];}// 构建权重池let pool = [];let totalWeight = 0;probabilityRanges.value.forEach(range => {const nums = getAllNumbers().filter(n => n >= range.start && n <= range.end).filter(n => !usedNumbers.value.includes(n));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 drawNumber = () => {if (allDrawn.value) {triggerCelebration();return;}const num = getWeightedRandomNumber();if (num !== null) {usedNumbers.value.push(num);currentNumber.value = num;if (allDrawn.value) {triggerCelebration();}} else {triggerCelebration();}};// 新增批量抽取函数const drawBatchNumbers = () => {if (allDrawn.value) {triggerCelebration();return;}const batch = [];const remaining = getAllNumbers().filter(n => !usedNumbers.value.includes(n));const actualSize = Math.min(batchSize.value, remaining.length);for (let i = 0; i < actualSize; i++) {const num = getWeightedRandomNumber();if (num !== null) {batch.push(num);usedNumbers.value.push(num);}}if (batch.length > 0) {currentNumber.value = batch.join(', ');// 批量抽取的特殊庆祝效果if (batch.length > 3) {triggerCelebration(`成功抽取${batch.length}人!`, batch.length * 100);} else if (allDrawn.value) {triggerCelebration();}} else {triggerCelebration();}};const triggerCelebration = (message = '所有学号已抽取完成!', duration = 3000) => {if (showCelebration.value) return;celebrationMessage.value = message;showCelebration.value = true;if (celebrationTimeout) clearTimeout(celebrationTimeout);celebrationTimeout = setTimeout(() => {showCelebration.value = false;}, duration);};const toggleContinuous = () => {if (isContinuous.value) {// 停止并记录当前数字为已使用cancelAnimationFrame(animationFrameId.value);const num = parseInt(currentNumber.value);if (!isNaN(num) && !usedNumbers.value.includes(num)) {usedNumbers.value.push(num);if (allDrawn.value) {triggerCelebration();}}} else {// 开始快速抽取,仅展示快闪,不记录const animate = () => {const num = getWeightedRandomNumber();if (num !== null) {currentNumber.value = num;}animationFrameId.value = requestAnimationFrame(animate);};animate();}isContinuous.value = !isContinuous.value;};const validateAndNavigate = () => {if (start.value > end.value) {alert('错误:起始学号不能大于结束学号');return;}if (start.value < 1 || end.value < 1) {alert('错误:学号不能小于1');return;}// 验证概率范围let totalWeight = 0;for (const range of probabilityRanges.value) {if (range.start > range.end) {alert(`错误:范围 ${range.start}-${range.end} 起始值不能大于结束值`);return;}if (range.start < start.value || range.end > end.value) {alert(`错误:范围 ${range.start}-${range.end} 超出学号范围`);return;}if (range.weight <= 0) {alert(`错误:范围 ${range.start}-${range.end} 权重必须大于0`);return;}totalWeight += range.weight;}if (probabilityRanges.value.length > 0 && totalWeight <= 0) {alert('错误:总权重必须大于0');return;}const params = new URLSearchParams({mode: mode.value,start: start.value,end: end.value});if (probabilityRanges.value.length > 0) {params.set('ranges', JSON.stringify(probabilityRanges.value));}window.location.search = params.toString();};const goBack = () => {window.location.search = '';};const resetUsedNumbers = () => {if (confirm('确定要重置已抽取记录吗?')) {usedNumbers.value = [];currentNumber.value = '—';}};const clearHistory = () => {usedNumbers.value = [];currentNumber.value = '—';};const addRange = () => {probabilityRanges.value.push({ start: start.value, end: end.value, weight: 50 });};const removeRange = (index) => {probabilityRanges.value.splice(index, 1);};const initializeParams = () => {const params = new URLSearchParams(window.location.search);if (params.has('start') && params.has('end') && params.has('mode')) {const startParam = parseInt(params.get('start'), 10);const endParam = parseInt(params.get('end'), 10);if (!isNaN(startParam)) start.value = startParam;if (!isNaN(endParam)) end.value = endParam;mode.value = params.get('mode');// 加载概率范围设置if (params.has('ranges')) {try {const ranges = JSON.parse(params.get('ranges'));if (Array.isArray(ranges)) {probabilityRanges.value = ranges.filter(r => r.start && r.end && r.weight && r.start <= r.end && r.weight > 0);}} catch (e) {console.error('解析概率范围失败:', e);}}isIndexPage.value = false;}};onMounted(() => {initializeParams();// 添加一个默认范围if (probabilityRanges.value.length === 0) {addRange();}});return {start,end,mode,currentNumber,isIndexPage,usedNumbers,isContinuous,showAdvanced,probabilityRanges,showCelebration,celebrationMessage,batchSize,showBatchSettings,allDrawn,getAllNumbers,getRandomNumber,getWeightedRandomNumber,drawNumber,drawBatchNumbers,toggleContinuous,validateAndNavigate,goBack,resetUsedNumbers,clearHistory,addRange,removeRange,getRandomColor,triggerCelebration};}}).mount('#app');</script>
</body>
</html>
🚀 后续改进方向(欢迎贡献)
- 支持导出抽取记录为 Excel 或 CSV。
- 多语言支持。
- 添加登录与数据持久化功能(如 localStorage)。
- 实现多人协作抽取(WebSocket)。
- 集成语音播报功能。
📝 结语
这款智能学号抽取系统不仅解决了课堂教学中的实际问题,也展示了现代前端技术在教育领域的应用潜力。如果你正在寻找一个简洁实用的教学辅助工具,或者希望学习如何构建一个交互式的 Vue.js 项目,这个系统将是一个不错的选择。
欢迎在评论区交流想法、提出建议或分享你的使用体验!