在线体验地址:https://ln0ia2k6.html2web.com/
DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>智能学号抽取系统V4.3.1title>
<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="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">
<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">Arialoption>
<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">Arialoption>
<option value="KaiTi">楷体option>
<option value="XingKai">行楷option>
<option value="LiShu">隶书option>
<option value="WeiShu">魏书option>
select>
div>
<div class="form-group">
<label>数字大小:{{ selectedNumberSize }}ptlabel>
<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 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><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">Arialoption>
<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">Arialoption>
<option value="KaiTi">楷体option>
<option value="XingKai">行楷option>
<option value="LiShu">隶书option>
<option value="WeiShu">魏书option>
select>
div>
<div class="form-group">
<label>数字大小:{{ selectedNumberSize }}ptlabel>
<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/[email protected]/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 noRepeat = ref(true);
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;
// 批量抽取相关
const batchSize = ref(1);
const showBatchSettings = ref(false);
// 多抽效果
const showMultiEffect = ref(false);
const multiDrawNumber = ref(0);
// 字体设置
const selectedGlobalFont = ref('Inter');
const selectedNumberFont = ref('Arial');
const selectedNumberSize = ref(250);
const selectedNumberBold = ref(true);
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;
});
};
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 = '—';
};
const goBack = () => {
isSetupPage.value = true;
};
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');
};
onMounted(() => {
// 添加一个默认范围
if (probabilityRanges.value.length === 0) {
addRange();
}
applyFontSettings();
});
// 监听学号范围变化,自动调整概率范围
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人),结果以逗号分隔展示,并自动记录到历史记录中。
可以为不同学号范围设置不同的抽取概率:
支持全局字体、数字字体、字号、是否加粗等设置:
本系统采用现代前端技术栈构建,结构清晰、易于扩展,适合用于教学演示、个人项目开发或二次开发。
index.html
├── 样式部分(CSS + 动画)
├── Vue 应用逻辑(setup 函数)
└── 页面结构(HTML)
const getAllNumbers = () => {
const numbers = [];
for (let i = start.value; i <= end.value; i++) {
numbers.push(i);
}
return numbers;
};
start
到 end
的完整学号列表。usedNumbers
数组判断是否重复抽取。const getWeightedRandomNumber = () => {
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;
};
:root {
--global-font: 'Inter', sans-serif;
--number-font: 'Arial';
--number-size: 250pt;
--number-weight: bold;
}
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');
};
@keyframes
实现了:
triggerCelebration('所有学号已抽取完成!');
角色 | 收益 |
---|---|
教师 | 快速高效进行课堂点名、提问、测验等环节 |
开发者 | 学习 Vue 3、CSS 动画、响应式设计、权重算法等实战技巧 |
学生 | 提升课堂参与感,增加互动乐趣 |
如果你觉得这个项目对你有帮助,请点赞、收藏并分享给更多需要的人!
如需源码分析、功能扩展、部署指导等内容,欢迎继续提问!