智能學號抽取系統 V5.4.3.2 —— Vue.js 實現的多功能課堂隨機抽簽工具
在教學或會議場景中,我們經常需要隨機抽取一個或多個學號/編號來決定發言者、答題者或者參與者。為了提高效率和公平性,我們可以使用一些智能化的小工具來實現這一過程。
今天介紹的這款 “智能學號抽取系統 V5.4.3.2” 是一款基于 Vue 3 的前端網頁應用,它不僅實現了基本的學號抽取功能,還加入了概率控制、批量抽取、字體自定義、慶祝動畫等高級功能,適用于教師、主持人、培訓師等多種角色。
🌐 在線預覽
你可以通過以下鏈接在線體驗該系統:
👉 在線預覽地址
無需下載或安裝,打開即可直接使用。
📂 代碼展示(完整HTML+Vue實現)
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>智能學號抽取系統V5.4.3.2</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"><style>/* 字體定義 */@font-face {font-family: 'KaiTi';src: local('KaiTi');}@font-face {font-family: 'XingKai';src: local('STXingkai');}@font-face {font-family: 'LiShu';src: local('LiSu');}@font-face {font-family: 'WeiShu';src: local('STXinwei');}: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);/* 字體變量 */--global-font: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--number-font: 'Arial';--number-size: 250pt;--number-weight: bold;}/* 應用全局字體 */body, button, input, select, textarea {font-family: var(--global-font);}body {display: flex;justify-content: center;align-items: center;min-height: 100vh;margin: 0;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);width: min(90%, 800px);text-align: center;position: relative;overflow: hidden;margin: 2rem 0;}/* 動態漸變頂部裝飾條 */.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, h2, h3 {color: var(--text-dark);margin-top: 0;}h1 {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;}/* 數字顯示區域 */.number-display {font-family: var(--number-font);font-size: var(--number-size);font-weight: var(--number-weight);text-align: center;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);line-height: 1;text-shadow: 0 2px 4px rgba(0,0,0,0.1);min-height: 200px;display: flex;align-items: center;justify-content: center;}.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;font-weight: 600;display: inline-flex;align-items: center;gap: 0.5rem;position: relative;overflow: hidden;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;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);}/* 按鈕按壓效果 */button:active {transform: translateY(4px) scale(0.98);box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;}/* 按鈕加載動畫 */.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; }}/* 歷史記錄 */.history {margin-top: 2rem;padding: 1rem;background: var(--bg-color);border-radius: 8px;max-height: 120px;overflow-y: auto;}.history-title {display: flex;justify-content: space-between;align-items: center;margin-bottom: 0.5rem;font-size: 0.9rem;color: var(--text-light);}.history-items {display: flex;flex-wrap: wrap;gap: 0.5rem;justify-content: center;}.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); }}/* 高級設置面板 */.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;}.settings-panel {padding: 0 1.5rem;background: #f5f7fa;border-radius: 8px;margin-top: 0.5rem;text-align: left;transition: all 0.3s ease;max-height: 0;overflow: hidden;opacity: 0;}.settings-panel.show {max-height: 1000px;opacity: 1;padding: 1.5rem;}/* 批量抽取面板 */.batch-panel {padding: 1rem;background: #f0f5ff;border-radius: 8px;margin-top: 1rem;display: none;animation: fadeIn 0.3s;}.batch-panel.show {display: block;}/* 不重復抽取選項 */.no-repeat-option {display: flex;align-items: center;margin: 1rem 0;justify-content: center;}/* 概率設置 */.probability-range {display: flex;align-items: center;margin-bottom: 0.8rem;flex-wrap: wrap;gap: 0.5rem;}.probability-range input {width: 60px;padding: 0.5rem;}/* 多抽效果 */.multi-draw-effect {position: fixed;top: 0;left: 0;width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;background: rgba(0,0,0,0.7);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;}/* 慶祝動畫 */.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);}/* 表單元素 */.form-group {margin: 1.5rem 0;display: flex;align-items: center;justify-content: center;}label {min-width: 100px;text-align: right;margin-right: 1rem;color: var(--text-dark);font-size: 1.1rem;font-weight: 500;}/* 漸變邊框輸入框 - 來自樣式0 */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,select: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;}input[type="number"]:hover,select:hover {background: linear-gradient(white, white) padding-box, linear-gradient(135deg, #ff0076, #1eaeff, #28ffbf, #ff0076) border-box;box-shadow: 0 0 5px rgba(255, 0, 118, 0.5), 0 0 20px rgba(30, 174, 255, 0.5);}select {appearance: none;background: linear-gradient(white, white) padding-box, linear-gradient(45deg, #ff7eb3, #65d9ff, #c7f464, #ff7eb3) border-box;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;}input[type="range"] {width: 200px;margin: 0 1rem;}input[type="checkbox"] {width: auto;margin-right: 0.5rem;}/* 動畫效果 */@keyframes fadeIn {from { opacity: 0; }to { opacity: 1; }}@keyframes zoomInOut {0% { transform: scale(1); }100% { transform: scale(1.2); }}@keyframes zoomIn {from { transform: scale(0.5); opacity: 0; }to { transform: scale(1); opacity: 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;}/* 響應式設計 */@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 {flex-direction: column;}button {width: 100%;}input[type="number"], select {width: 100%;}.probability-range {flex-direction: column;align-items: flex-start;}.probability-range input {width: 100% !important;}}</style>
</head>
<body><div id="app" class="container"><transition name="fade" mode="out-in"><div v-if="isSetupPage" key="setup"><h1><i class="fas fa-users"></i> 學號抽取設置</h1><div class="form-group"><label for="start">起始學號:</label><input type="number" v-model.number="start" id="start" min="1" @change="adjustProbabilityRanges" /></div><div class="form-group"><label for="end">結束學號:</label><input type="number" v-model.number="end" id="end" :min="start" @change="adjustProbabilityRanges" /></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="no-repeat-option"><input type="checkbox" id="noRepeat" v-model="noRepeat" style="margin-right: 0.5rem;"><label for="noRepeat" style="margin: 0;">不重復抽取</label></div><div class="button-group"><button class="primary" @click="validateAndNavigate"><i class="fas fa-play"></i><span>開始抽取</span></button></div><!-- 顯示高級設置按鈕 --><button class="toggle-advanced" @click="showAdvanced = !showAdvanced" style="margin: 1rem auto;"><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"><i class="fas fa-plus"></i> 添加范圍</button><!-- 字體設置 --><h3 style="margin-top: 1.5rem;"><i class="fas fa-font"></i> 字體設置</h3><div class="form-group"><label>全局字體:</label><select v-model="selectedGlobalFont"><option value="Inter">默認</option><option value="Arial">Arial</option><option value="KaiTi">楷體</option><option value="XingKai">行楷</option><option value="LiShu">隸書</option><option value="WeiShu">魏書</option></select></div><div class="form-group"><label>數字字體:</label><select v-model="selectedNumberFont"><option value="Arial">Arial</option><option value="KaiTi">楷體</option><option value="XingKai">行楷</option><option value="LiShu">隸書</option><option value="WeiShu">魏書</option></select></div><div class="form-group"><label>數字大小:{{ selectedNumberSize }}pt</label><input type="range" v-model.number="selectedNumberSize" min="100" max="300" step="10"></div><div class="form-group"><label>數字加粗:</label><input type="checkbox" v-model="selectedNumberBold"></div><button @click="applyFontSettings" class="primary" style="width: 100%;"><i class="fas fa-check"></i> 應用字體設置</button></div></div><div v-else key="main"><h1><i class="fas fa-random"></i> 學號抽取結果</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="primary" @click="multiDraw"><i class="fas fa-bolt"></i><span>連抽5次</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="batch-panel show"><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 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><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"><i class="fas fa-plus"></i> 添加范圍</button><!-- 字體設置 --><h3 style="margin-top: 1.5rem;"><i class="fas fa-font"></i> 字體設置</h3><div class="form-group"><label>全局字體:</label><select v-model="selectedGlobalFont"><option value="Inter">默認</option><option value="Arial">Arial</option><option value="KaiTi">楷體</option><option value="XingKai">行楷</option><option value="LiShu">隸書</option><option value="WeiShu">魏書</option></select></div><div class="form-group"><label>數字字體:</label><select v-model="selectedNumberFont"><option value="Arial">Arial</option><option value="KaiTi">楷體</option><option value="XingKai">行楷</option><option value="LiShu">隸書</option><option value="WeiShu">魏書</option></select></div><div class="form-group"><label>數字大小:{{ selectedNumberSize }}pt</label><input type="range" v-model.number="selectedNumberSize" min="100" max="300" step="10"></div><div class="form-group"><label>數字加粗:</label><input type="checkbox" v-model="selectedNumberBold"></div><button @click="applyFontSettings" class="primary" style="width: 100%;"><i class="fas fa-check"></i> 應用字體設置</button></div></div></div></transition><!-- 多抽效果展示 --><div class="multi-draw-effect" v-if="showMultiEffect"><div class="effect-number">{{ multiDrawNumber }}</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><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() {// 解析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,batchSize: params.has('batchSize') ? parseInt(params.get('batchSize')) : 1,globalFont: params.get('globalFont') || 'Inter',numberFont: params.get('numberFont') || 'Arial',numberSize: params.has('numberSize') ? parseInt(params.get('numberSize')) : 250,numberBold: params.has('numberBold') ? params.get('numberBold') === 'true' : true};};const urlParams = parseUrlParams();// 抽學號設置 - 使用URL參數或默認值const start = ref(urlParams.start);const end = ref(urlParams.end);const mode = ref(urlParams.mode);const noRepeat = ref(urlParams.noRepeat);const currentNumber = ref('—');const isSetupPage = 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;// 批量抽取相關 - 使用URL參數或默認值const batchSize = ref(urlParams.batchSize);const showBatchSettings = ref(false);// 多抽效果const showMultiEffect = ref(false);const multiDrawNumber = ref(0);// 字體設置 - 使用URL參數或默認值const selectedGlobalFont = ref(urlParams.globalFont);const selectedNumberFont = ref(urlParams.numberFont);const selectedNumberSize = ref(urlParams.numberSize);const selectedNumberBold = ref(urlParams.numberBold);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 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;});};// 更新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('batchSize', batchSize.value);params.set('globalFont', selectedGlobalFont.value);params.set('numberFont', selectedNumberFont.value);params.set('numberSize', selectedNumberSize.value);params.set('numberBold', selectedNumberBold.value);const newUrl = window.location.pathname + '?' + params.toString();window.history.replaceState(null, '', newUrl);};const drawNumber = () => {if (allDrawn.value) {triggerCelebration();return;}const num = getWeightedRandomNumber();if (num !== null) {if (noRepeat.value) {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);if (noRepeat.value) {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 multiDraw = () => {const available = getAllNumbers().filter(n => !usedNumbers.value.includes(n));const drawCount = Math.min(5, available.length);if (drawCount === 0) {alert('沒有可抽取的學號了!');return;}showMultiEffect.value = true;const results = [];let count = 0;const interval = setInterval(() => {if (count >= drawCount) {clearInterval(interval);setTimeout(() => {showMultiEffect.value = false;currentNumber.value = results.join(', ');if (noRepeat.value) {usedNumbers.value.push(...results);}if (allDrawn.value) {triggerCelebration();}}, 800);return;}const num = getWeightedRandomNumber();results.push(num);multiDrawNumber.value = num;count++;}, 300);};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) && noRepeat.value) {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;}isSetupPage.value = false;usedNumbers.value = [];currentNumber.value = '—';updateUrlParams();};const goBack = () => {isSetupPage.value = true;updateUrlParams();};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 applyFontSettings = () => {document.documentElement.style.setProperty('--global-font', selectedGlobalFont.value);document.documentElement.style.setProperty('--number-font', selectedNumberFont.value);document.documentElement.style.setProperty('--number-size', selectedNumberSize.value + 'pt');document.documentElement.style.setProperty('--number-weight', selectedNumberBold.value ? 'bold' : 'normal');updateUrlParams();};onMounted(() => {// 添加一個默認范圍if (probabilityRanges.value.length === 0) {addRange();}applyFontSettings();// 監聽重要參數變化并更新URLwatch([start, end, mode, noRepeat, batchSize, selectedGlobalFont, selectedNumberFont, selectedNumberSize, selectedNumberBold], () => {updateUrlParams();}, { deep: true });});// 監聽學號范圍變化,自動調整概率范圍watch([start, end], adjustProbabilityRanges);return {start,end,mode,noRepeat,currentNumber,isSetupPage,usedNumbers,isContinuous,showAdvanced,probabilityRanges,showCelebration,celebrationMessage,batchSize,showBatchSettings,showMultiEffect,multiDrawNumber,selectedGlobalFont,selectedNumberFont,selectedNumberSize,selectedNumberBold,allDrawn,getAllNumbers,getRandomNumber,getWeightedRandomNumber,drawNumber,drawBatchNumbers,multiDraw,toggleContinuous,validateAndNavigate,goBack,resetUsedNumbers,clearHistory,addRange,removeRange,applyFontSettings,getRandomColor,triggerCelebration,adjustProbabilityRanges};}}).mount('#app');</script>
</body>
</html>
📦 功能亮點
? 多種抽取模式
- 單次抽取:點擊按鈕后隨機抽取一個學號。
- 快速抽取:連續滾動顯示隨機學號,再次點擊停止并記錄當前結果。
- 連抽5次:一次性抽取5個學號并以逗號分隔展示。
- 批量抽取:設置抽取人數,一次抽取多人,結果以字符串形式展示。
? 不重復抽取
支持不重復抽取功能,避免同一個學號被多次抽中,適合需要公平性的場合。
? 概率權重控制
這是本系統的最大亮點之一:
- 可設置多個學號區間,并為每個區間分配不同的抽取概率權重。
- 系統根據這些權重進行加權隨機抽取,滿足不同場景下的偏好需求(例如更傾向于抽取某個特定范圍的學生)。
? 字體與樣式自定義
- 支持多種字體選擇(默認、Arial、楷體、行楷、隸書、魏書)。
- 可調整數字大小(滑動條控制)、是否加粗。
- 所有設置實時生效,提升用戶體驗。
? 歷史記錄展示
- 顯示已抽取的學號列表,最新抽取的學號會高亮顯示。
- 支持清除歷史記錄,方便重新開始一輪新的抽取。
? 慶祝動畫
當所有學號都被抽取完畢時,系統會自動觸發煙花慶祝動畫,并彈出提示信息:“所有學號已抽取完成!”
? URL 參數同步
所有設置都會通過 URLSearchParams
同步到瀏覽器地址欄中,用戶可以保存或分享當前狀態,便于下次恢復使用。
🧠 技術實現概述
該系統完全采用 HTML + CSS + Vue 3 Composition API 構建,未引入任何復雜框架,結構清晰,易于維護。
🔧 核心技術點:
- 使用 Vue 3 Composition API 進行響應式數據綁定。
- 利用
requestAnimationFrame
實現快速抽取動畫效果。 - 使用 CSS 變量動態控制全局樣式,實現字體和字號的實時更新。
- 通過
URLSearchParams
實現配置參數的持久化與共享。 - 使用
@keyframes
和transition
實現豐富的動畫效果(如按鈕懸停、加載動畫、慶祝動畫等)。
📐 頁面結構說明
整個頁面分為兩個主要視圖:
1?? 設置頁(Setup Page)
用于配置:
- 學號起始和結束范圍
- 抽取模式(單次/快速)
- 是否啟用不重復抽取
- 高級設置:概率區間、字體設置等
2?? 抽取頁(Main Page)
用于執行抽取操作,包括:
- 顯示當前抽取結果
- 控制按鈕(抽取、重置、返回設置等)
- 歷史記錄展示區域
- 高級設置面板(可折疊)
💡 適用場景
- 教師課堂上隨機提問學生
- 公司內部抽獎活動
- 培訓課程中的互動環節
- 游戲或活動中隨機選擇參與者
📌 小結
這款 “智能學號抽取系統 V5.4.3.2” 是一個集功能性、美觀性和交互性于一體的網頁小工具。它不僅解決了基礎的隨機抽取問題,還通過豐富的設置選項和動畫反饋提升了用戶的使用體驗。
如果你是教育工作者、培訓師或者活動組織者,這款工具將是你不可或缺的好幫手!