[特殊字符] 智能学号抽取系统 V4.3.1 —— 教育互动好帮手

智能学号抽取系统 V4.3.1 —— 教育互动好帮手

在线体验地址: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>

系统简介(面向用户)

“智能学号抽取系统”是一款专为课堂、考试、点名等教学场景设计的随机学号抽取工具。它不仅支持多种抽取模式,还提供了丰富的个性化设置功能,如字体样式、概率权重控制、批量抽取等,适用于教师、学生管理者以及各类需要随机选择人员的场景。

该系统无需安装,直接通过浏览器即可使用,界面美观、操作简单,是提升课堂互动效率的好助手!


主要功能亮点(面向用户)

✅ 多种抽取模式

  • 单次抽取(d):点击按钮随机抽取一个学号。
  • 快速抽取(s):连续滚动显示多个学号,再次点击按钮停止并确认结果。

✅ 不重复抽取

开启“不重复抽取”选项后,每个学号只会被抽取一次,避免重复提问或点名。

✅ 批量抽取

支持一次性抽取多人,可自定义抽取人数(例如5人),结果以逗号分隔展示,并自动记录到历史记录中。

✅ 自定义概率权重

可以为不同学号范围设置不同的抽取概率:

  • 示例:1 ~ 10号学号的抽取概率为20%,11~40号为80%。
  • 更加灵活地控制某些学号更容易被抽中。

✅ 字体与样式定制

支持全局字体、数字字体、字号、是否加粗等设置:

  • 可选字体包括:默认、楷体、行楷、隶书、魏书等。
  • 实时预览效果,视觉体验更佳。

✅ 历史记录追踪

  • 系统会自动记录所有已抽取的学号。
  • 最新抽取的学号会高亮显示,方便查看。

✅ 动画庆祝效果

  • 当所有学号都被抽取完毕时,会触发全屏粒子动画和提示信息。
  • 抽取成功时也有小动画反馈,增强用户体验。

✅ 移动端友好

  • 支持响应式布局,手机和平板也能流畅操作。

️ 使用说明(面向用户)

1. 设置页面

  • 输入起始和结束学号。
  • 选择抽取模式(单次 / 快速)。
  • 开启“不重复抽取”选项(推荐开启)。
  • 添加多个概率范围并设置对应权重。
  • 调整字体、字号、加粗等样式设置。
  • 点击“开始抽取”进入抽取页面。

2. 抽取页面

  • 点击“抽取学号”获取当前学号。
  • 启用“批量抽取”功能可一次性抽取多人。
  • 查看历史记录,了解哪些学号已被抽取。
  • 可随时返回设置页面重新配置。

示例操作流程

  1. 打开在线页面:https://ln0ia2k6.html2web.com/
  2. 设置起始学号为1,结束学号为40。
  3. 开启“不重复抽取”。
  4. 添加两个概率范围:
    • 1~10,权重20%
    • 11~40,权重80%
  5. 点击“开始抽取”,进入抽取页面。
  6. 点击“抽取学号”或“连抽5次”,观察结果。
  7. 查看历史记录,确认学号是否唯一。

开发者指南(面向开发者)

本系统采用现代前端技术栈构建,结构清晰、易于扩展,适合用于教学演示、个人项目开发或二次开发。

技术栈

  • 框架:Vue 3 Composition API
  • 样式库:纯 CSS + CSS Variables(无第三方 UI 框架)
  • 字体图标:Font Awesome CDN
  • 部署方式:HTML 单文件,可直接部署在任何静态服务器上

文件结构概览

index.html
├── 样式部分(CSS + 动画)
├── Vue 应用逻辑(setup 函数)
└── 页面结构(HTML)

核心功能实现解析(开发者视角)

1. 学号生成与抽取逻辑

const getAllNumbers = () => {
  const numbers = [];
  for (let i = start.value; i <= end.value; i++) {
    numbers.push(i);
  }
  return numbers;
};
  • 生成从 startend 的完整学号列表。
  • 结合 usedNumbers 数组判断是否重复抽取。

2. 权重抽取算法

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;
};
  • 构建“权重池”,将学号根据其权重多次加入池中。
  • 随机数落在哪个区间就抽取对应的学号。

3. 字体动态设置

: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');
};
  • 利用 CSS 变量实现字体动态切换。
  • 修改根元素变量即可全局生效。

4. 动画与庆祝效果

  • 使用 @keyframes 实现了:
    • 数字滚动动画
    • 加载动画
    • 粒子飘落动画
    • 全屏庆祝动画
  • 示例庆祝动画调用:
triggerCelebration('所有学号已抽取完成!');

可扩展性建议(开发者视角)

✅ 可添加功能建议

  • 学号导出为 Excel 或 CSV 文件
  • 数据本地存储(localStorage)
  • 抽取次数统计面板
  • 用户登录系统(区分不同班级/课程)
  • 支持语音播报抽取结果

✅ 可优化方向

  • 引入状态管理(Pinia/Vuex)
  • 组件化拆分(提高复用性)
  • 支持深色主题切换
  • 添加后台管理系统(Node.js + MongoDB)

总结(面向用户 & 开发者)

角色 收益
‍ 教师 快速高效进行课堂点名、提问、测验等环节
‍ 开发者 学习 Vue 3、CSS 动画、响应式设计、权重算法等实战技巧
学生 提升课堂参与感,增加互动乐趣

相关资源

  • 在线体验地址:https://ln0ia2k6.html2web.com/

如果你觉得这个项目对你有帮助,请点赞、收藏并分享给更多需要的人!


如需源码分析、功能扩展、部署指导等内容,欢迎继续提问!

你可能感兴趣的:(教育小程序,css,javascript,前端,html,vue)