纯Html点名系统(就一个HTML),带语音播报功能(保护喉咙),零依赖、无需网络、浏览器打开即用(可以按F11全屏),专为教师课堂互动设计。
五大功能:
上课提问:随机抽取学生,支持权重记忆
顺序点名:按名单顺序循环
随机点名:完全随机,公平无偏见
快速连抽:连续抽取多名学生,适合小组活动
计时器模式:倒计时,增加课堂紧张感。
调用的是系统语音(谷歌浏览器可能需要授权),目前测试谷歌内核和火狐浏览器没有问题。右上角点击语音按钮可以关闭。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<title>高颜值语音点名器 v2.0</title>
<!-- Classroom Voice Roll-Call v2.0 | MIT License | 仅供教学场景 -->
<style>
:root {
/* 马卡龙主题(默认) */
--bg: linear-gradient(135deg, #ffeef8 0%, #e6f3ff 100%);
--card-bg: rgba(255, 255, 255, 0.9);
--primary: #ff6b6b;
--secondary: #ffd93d;
--accent: #4ecdc4;
--text: #2d3436;
--text-light: #636e72;
--border: rgba(255, 107, 107, 0.2);
--shadow: rgba(255, 107, 107, 0.15);
--success: #00b894;
--warning: #fdcb6e;
--error: #e17055;
--primary-rgb: 255, 107, 107;
--secondary-rgb: 255, 217, 61;
--accent-rgb: 78, 205, 196;
}
[data-theme="geek"] {
--bg: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%);
--card-bg: rgba(25, 25, 35, 0.95);
--primary: #00d4ff;
--secondary: #0099cc;
--accent: #ff6b35;
--text: #e8e8e8;
--text-light: #a0a0a0;
--border: rgba(0, 212, 255, 0.2);
--shadow: rgba(0, 0, 0, 0.5);
--primary-rgb: 0, 212, 255;
--secondary-rgb: 0, 153, 204;
--accent-rgb: 255, 107, 53;
}
[data-theme="purple"] {
--bg: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
--card-bg: rgba(255, 255, 255, 0.15);
--primary: #a855f7;
--secondary: #c084fc;
--accent: #f472b6;
--text: #ffffff;
--text-light: #e9d5ff;
--border: rgba(168, 85, 247, 0.4);
--shadow: rgba(102, 126, 234, 0.3);
--primary-rgb: 168, 85, 247;
--secondary-rgb: 192, 132, 252;
--accent-rgb: 244, 114, 182;
}
[data-theme="purple"] body {
background: var(--bg);
background-attachment: fixed;
background-size: 400% 400%;
animation: gradientShift 8s ease infinite;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
[data-theme="purple"] .drawer-header {
background: linear-gradient(135deg, rgba(168, 85, 247, 0.9), rgba(192, 132, 252, 0.9), rgba(244, 114, 182, 0.9));
backdrop-filter: blur(15px);
border-bottom: 1px solid rgba(168, 85, 247, 0.3);
}
[data-theme="purple"] .theme-btn--purple {
background: linear-gradient(135deg, #a855f7, #c084fc, #f472b6) !important;
box-shadow: 0 4px 15px rgba(168, 85, 247, 0.4);
transition: all 0.3s ease;
}
[data-theme="purple"] .theme-btn--purple:hover {
transform: scale(1.1) rotate(5deg);
box-shadow: 0 6px 20px rgba(168, 85, 247, 0.6);
}
[data-theme="warm"] {
--bg: linear-gradient(135deg, #fff5f5 0%, #ffe8e8 100%);
--card-bg: rgba(255, 255, 255, 0.9);
--primary: #ff6b6b;
--secondary: #ff5252;
--accent: #4ecdc4;
--text: #2d3436;
--text-light: #636e72;
--border: rgba(255, 107, 107, 0.2);
--shadow: rgba(255, 107, 107, 0.15);
--primary-rgb: 255, 107, 107;
--secondary-rgb: 255, 82, 82;
--accent-rgb: 78, 205, 196;
}
[data-theme="nature"] {
--bg: linear-gradient(135deg, #f0f9f0 0%, #e8f5e8 100%);
--card-bg: rgba(255, 255, 255, 0.9);
--primary: #4caf50;
--secondary: #388e3c;
--accent: #ff9800;
--text: #1b5e20;
--text-light: #4caf50;
--border: rgba(76, 175, 80, 0.2);
--shadow: rgba(76, 175, 80, 0.15);
--primary-rgb: 76, 175, 80;
--secondary-rgb: 56, 142, 60;
--accent-rgb: 255, 152, 0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: var(--bg);
min-height: 100vh;
color: var(--text);
overflow-x: hidden;
}
.container {
display: flex;
height: 100vh;
position: relative;
}
/* 顶部栏 */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background: var(--card-bg);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
box-shadow: 0 2px 10px var(--shadow);
z-index: 1000;
}
.logo {
font-size: 20px;
font-weight: bold;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header-controls {
display: flex;
gap: 15px;
align-items: center;
}
.voice-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
}
.voice-toggle:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--shadow);
}
.theme-selector {
display: flex;
gap: 5px;
padding: 5px;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 25px;
}
.theme-btn {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.3s ease;
}
.theme-btn.active {
border-color: var(--primary);
transform: scale(1.1);
}
/* 左侧名单抽屉 */
.drawer {
position: fixed;
left: 0;
top: 60px;
bottom: 0;
width: 320px;
background: var(--card-bg);
backdrop-filter: blur(20px);
box-shadow: 0 0 30px var(--shadow);
transform: translateX(-100%);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1001;
overflow-y: auto;
border-right: 1px solid var(--border);
}
.drawer.open {
transform: translateX(0);
box-shadow: 0 0 40px var(--shadow), 0 0 80px rgba(0, 0, 0, 0.1);
}
.drawer-header {
padding: 25px 20px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
position: sticky;
top: 0;
z-index: 1002;
backdrop-filter: blur(10px);
}
.drawer-header h3 {
font-size: 20px;
font-weight: 600;
margin: 0;
letter-spacing: 0.5px;
}
.drawer-content {
padding: 25px 20px;
background: linear-gradient(135deg, transparent 0%, rgba(255, 255, 255, 0.05) 100%);
}
.student-input {
width: 100%;
min-height: 250px;
padding: 15px;
border: 2px solid var(--border);
border-radius: 12px;
background: var(--card-bg);
color: var(--text);
resize: vertical;
font-size: 14px;
line-height: 1.6;
transition: all 0.3s ease;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
.student-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(var(--primary-rgb, 255, 107, 107), 0.1), inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
.student-input::placeholder {
color: var(--text-light);
font-style: italic;
}
.drawer-actions {
display: flex;
gap: 12px;
margin-top: 20px;
justify-content: center;
flex-wrap: wrap;
}
.drawer-actions .btn {
padding: 12px 20px;
border-radius: 25px;
font-size: 14px;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px var(--shadow);
border: none;
position: relative;
overflow: hidden;
}
.drawer-actions .btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px var(--shadow);
}
.drawer-actions .btn:active {
transform: translateY(0);
}
.drawer-actions .btn-primary {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
}
.drawer-actions .btn-secondary {
background: var(--card-bg);
color: var(--text);
border: 1px solid var(--border);
}
.drawer-actions .btn-secondary:hover {
background: var(--primary);
color: white;
border-color: var(--primary);
}
/* 右侧历史抽屉 */
.history {
position: fixed;
right: 0;
top: 60px;
bottom: 0;
width: 380px;
background: var(--card-bg);
backdrop-filter: blur(20px);
box-shadow: 0 0 30px var(--shadow);
transform: translateX(100%);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1001;
overflow-y: auto;
border-left: 1px solid var(--border);
}
.history.open {
transform: translateX(0);
box-shadow: 0 0 40px var(--shadow), 0 0 80px rgba(0, 0, 0, 0.1);
}
.history-header {
padding: 25px 20px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, var(--secondary), var(--accent));
color: white;
position: sticky;
top: 0;
z-index: 1002;
backdrop-filter: blur(10px);
}
.history-header h3 {
font-size: 20px;
font-weight: 600;
margin: 0;
letter-spacing: 0.5px;
}
#historyList {
padding: 25px 20px;
background: linear-gradient(135deg, transparent 0%, rgba(255, 255, 255, 0.05) 100%);
}
.history-item {
padding: 20px;
margin-bottom: 15px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--card-bg);
box-shadow: 0 2px 8px var(--shadow);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.history-item:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px var(--shadow);
border-color: var(--primary);
}
.history-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: linear-gradient(135deg, var(--primary), var(--accent));
border-radius: 2px;
}
.history-item-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
}
.history-name {
font-size: 16px;
font-weight: 600;
color: var(--text);
margin-bottom: 5px;
}
.history-mode {
font-size: 12px;
color: var(--primary);
background: rgba(var(--primary-rgb, 255, 107, 107), 0.1);
padding: 2px 8px;
border-radius: 10px;
margin-bottom: 5px;
display: inline-block;
}
.history-time {
font-size: 12px;
color: var(--text-light);
font-family: monospace;
white-space: nowrap;
}
/* 主舞台 */
.main {
flex: 1;
margin: 60px 0 180px 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
transition: all 0.3s ease;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
pointer-events: none;
}
.main .card {
pointer-events: auto;
}
.card {
background: var(--card-bg);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 80px;
text-align: center;
box-shadow: 0 15px 40px var(--shadow);
max-width: 600px;
width: 90%;
position: relative;
overflow: hidden;
transform: scale(1.1);
}
.card__name {
font-size: 56px;
font-weight: bold;
margin-bottom: 30px;
min-height: 70px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
word-wrap: break-word;
word-break: break-all;
line-height: 1.2;
max-height: 200px;
overflow: hidden;
}
.card__name.long-text {
font-size: 36px;
}
.card__name.very-long-text {
font-size: 24px;
}
.card__name.extra-long-text {
font-size: 18px;
}
.progress-bar {
height: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
margin: 20px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--accent));
transition: width 0.3s ease;
}
.timer-display {
font-size: 36px;
font-weight: bold;
color: var(--primary);
margin: 20px 0;
}
/* 控制面板 */
.controls {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--card-bg);
backdrop-filter: blur(10px);
padding: 20px;
box-shadow: 0 -2px 10px var(--shadow);
}
.control-row {
display: flex;
gap: 15px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 15px;
}
.mode-tabs {
display: flex;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 25px;
padding: 5px;
}
.mode-tab {
padding: 10px 20px;
border: none;
background: none;
color: var(--text);
cursor: pointer;
border-radius: 20px;
transition: all 0.3s ease;
}
.mode-tab.active {
background: var(--primary);
color: white;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-export:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.btn-clear:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
}
.btn-close:hover {
background: var(--border);
transform: scale(1.1);
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-secondary {
background: var(--text-light);
color: white;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--shadow);
}
.slider-container {
display: flex;
align-items: center;
gap: 10px;
min-width: 200px;
}
.slider {
flex: 1;
height: 4px;
background: var(--border);
border-radius: 2px;
outline: none;
-webkit-appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
background: var(--primary);
border-radius: 50%;
cursor: pointer;
}
/* 移动端触摸优化 */
@media (max-width: 768px) {
* {
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
body {
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
.slider::-webkit-slider-thumb {
width: 24px;
height: 24px;
}
.btn, .mode-tab {
min-height: 44px;
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}
.drawer-toggle {
min-width: 44px;
min-height: 44px;
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}
.voice-toggle, .theme-selector {
min-height: 36px;
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}
.theme-btn {
min-width: 36px;
min-height: 36px;
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}
.card {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.05);
}
.slider {
touch-action: pan-x;
}
/* 移动端动画优化 */
.card, .btn, .mode-tab {
will-change: transform;
}
/* 防止页面缩放 */
input, textarea, select {
font-size: 16px !important;
}
}
/* 弹幕效果 */
.barrage {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1001;
}
.barrage-item {
position: absolute;
background: var(--primary);
color: white;
padding: 15px 25px;
border-radius: 25px;
font-size: 18px;
font-weight: bold;
box-shadow: 0 4px 12px var(--shadow);
animation: flyIn 0.8s ease-out;
}
@keyframes flyIn {
from {
transform: translateX(100vw) translateY(-50px);
opacity: 0;
}
to {
transform: translateX(calc(100vw - 200px)) translateY(0);
opacity: 1;
}
}
/* 3D 翻转卡片 */
.card-3d {
perspective: 1000px;
}
.card-inner {
position: relative;
width: 100%;
height: 200px;
transition: transform 1.2s;
transform-style: preserve-3d;
}
.card-3d.flipping .card-inner {
transform: rotateY(180deg);
}
.card-face {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
font-weight: bold;
}
.card-back {
transform: rotateY(180deg);
background: var(--accent);
color: white;
}
/* 粒子背景 */
#particles-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
/* 抽屉切换按钮 */
.drawer-toggle {
position: fixed;
top: 50%;
transform: translateY(-50%);
background: var(--primary);
color: white;
border: none;
padding: 15px 12px;
cursor: pointer;
z-index: 998;
transition: all 0.3s ease;
font-size: 18px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
width: 45px;
height: 45px;
box-shadow: 0 2px 10px var(--shadow);
}
.drawer-toggle:hover {
transform: translateY(-50%) scale(1.1);
box-shadow: 0 4px 15px var(--shadow);
}
.drawer-toggle.left {
left: 0;
border-radius: 0 25px 25px 0;
}
.drawer-toggle.right {
right: 0;
border-radius: 25px 0 0 25px;
}
.drawer-toggle .icon {
display: inline-block;
line-height: 1;
text-align: center;
}
/* 移动端优化增强版 */
@media (max-width: 768px) {
:root {
--mobile-padding: 12px;
--mobile-radius: 12px;
--mobile-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
body {
font-size: 14px;
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overscroll-behavior-y: contain;
touch-action: manipulation;
}
/* 移动端安全区域适配 */
.header {
padding: max(12px, env(safe-area-inset-top)) var(--mobile-padding);
height: auto;
min-height: 60px;
flex-wrap: wrap;
gap: 8px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 0 0 var(--mobile-radius) var(--mobile-radius);
}
.logo {
font-size: 16px;
font-weight: 600;
}
.header-controls {
gap: 8px;
}
.voice-toggle {
padding: 6px 10px;
font-size: 12px;
border-radius: 16px;
}
.theme-selector {
padding: 3px;
border-radius: 20px;
}
.theme-btn {
width: 24px;
height: 24px;
border-radius: 50%;
transition: all 0.2s ease;
}
.theme-btn:active {
transform: scale(0.9);
}
/* 抽屉全屏优化 */
.drawer, .history {
width: 100%;
border-radius: 0;
top: 0;
bottom: 0;
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px);
z-index: 1001; /* 确保在最上层 */
}
.drawer-header, .history-header {
position: sticky;
top: 0;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px);
z-index: 1002;
padding: max(15px, env(safe-area-inset-top)) var(--mobile-padding) 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
}
.drawer-header h3, .history-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.drawer-header .btn, .history-header .btn {
width: auto;
min-width: 36px;
height: 36px;
padding: 8px;
font-size: 20px;
font-weight: bold;
border-radius: 50%;
background: rgba(0, 0, 0, 0.05);
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
margin: 0;
}
.drawer-header, .history-header {
padding: max(20px, env(safe-area-inset-top)) var(--mobile-padding) 15px;
font-size: 16px;
font-weight: 600;
border-bottom: 1px solid var(--border);
}
.drawer-content, .history-content {
padding: 0 var(--mobile-padding) max(20px, env(safe-area-inset-bottom));
}
.student-input {
width: 100%;
min-height: 180px;
padding: 12px;
border: 1px solid var(--border);
border-radius: var(--mobile-radius);
background: var(--card-bg);
color: var(--text);
font-size: 16px;
line-height: 1.4;
resize: vertical;
}
.main {
padding: var(--mobile-padding);
margin: 80px 0 220px;
padding-bottom: calc(200px + env(safe-area-inset-bottom));
}
/* 卡片移动端优化 */
.card {
padding: 30px 20px;
margin: 0 auto;
max-width: 95%;
border-radius: var(--mobile-radius);
box-shadow: var(--mobile-shadow);
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(8px);
transition: all 0.3s ease;
}
.card:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.card__name {
font-size: 28px;
font-weight: 600;
margin-bottom: 20px;
line-height: 1.3;
word-break: break-word;
hyphens: auto;
}
.card__name.long-text {
font-size: 22px;
}
.card__name.very-long-text {
font-size: 18px;
}
.card__name.extra-long-text {
font-size: 16px;
}
.timer-display {
font-size: 72px !important;
margin: 30px 0 !important;
font-weight: 800 !important;
font-family: 'Courier New', 'Consolas', 'Monaco', monospace !important;
letter-spacing: 4px !important;
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important;
line-height: 1 !important;
color: var(--primary) !important;
text-align: center !important;
min-height: 100px !important;
display: flex;
align-items: center;
justify-content: center;
}
/* 控制面板移动端优化 */
.controls {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 999;
max-height: 50vh;
overflow-y: auto;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
padding: max(15px, env(safe-area-inset-bottom)) var(--mobile-padding);
border-radius: var(--mobile-radius) var(--mobile-radius) 0 0;
}
.control-row {
flex-direction: column;
align-items: stretch;
gap: 10px;
margin-bottom: 10px;
}
.mode-tabs {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 6px;
padding: 8px;
background: rgba(0, 0, 0, 0.03);
border-radius: var(--mobile-radius);
border: none;
margin-bottom: 15px;
}
.mode-tab {
padding: 10px 8px;
font-size: 12px;
min-width: auto;
text-align: center;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
touch-action: manipulation;
}
.mode-tab.active {
transform: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.btn {
padding: 14px 20px;
font-size: 15px;
width: 100%;
justify-content: center;
border-radius: var(--mobile-radius);
font-weight: 600;
transition: all 0.15s ease;
min-height: 48px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
touch-action: manipulation;
}
.btn:active {
transform: scale(0.97);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
.slider-container {
flex-direction: column;
align-items: stretch;
min-width: auto;
gap: 8px;
background: rgba(0, 0, 0, 0.02);
padding: 12px;
border-radius: var(--mobile-radius);
}
.slider-container label {
text-align: center;
font-size: 13px;
font-weight: 500;
color: var(--text-light);
}
.slider {
height: 4px;
border-radius: 2px;
background: var(--border);
touch-action: pan-x;
}
.slider::-webkit-slider-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
}
/* 抽屉切换按钮移动端优化 */
.drawer-toggle {
width: 40px;
height: 40px;
padding: 10px;
font-size: 16px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
color: var(--primary);
box-shadow: var(--mobile-shadow);
backdrop-filter: blur(8px);
touch-action: manipulation;
}
.drawer-toggle.left {
left: var(--mobile-padding);
border-radius: 50%;
}
.drawer-toggle.right {
right: var(--mobile-padding);
border-radius: 50%;
}
/* 弹幕效果移动端优化 */
.barrage-item {
font-size: 14px;
padding: 8px 16px;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
font-weight: 500;
}
@keyframes flyIn {
from {
transform: translateX(100vw) translateY(-30px);
opacity: 0;
}
to {
transform: translateX(calc(100vw - 120px)) translateY(0);
opacity: 1;
}
}
/* 3D卡片移动端优化 */
.card-3d .card-inner {
height: 120px;
}
.card-face {
font-size: 20px;
font-weight: 600;
}
/* 防止双击缩放 */
.btn, .mode-tab, .drawer-toggle {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* 滚动条优化 */
.drawer, .history {
scrollbar-width: thin;
scrollbar-color: var(--primary) transparent;
}
.drawer::-webkit-scrollbar, .history::-webkit-scrollbar {
width: 4px;
}
.drawer::-webkit-scrollbar-thumb, .history::-webkit-scrollbar-thumb {
background-color: var(--primary);
border-radius: 2px;
}
/* 横屏模式优化 */
@media (max-height: 500px) and (orientation: landscape) {
.header {
min-height: 50px;
padding: 8px var(--mobile-padding);
}
.controls {
padding: 8px var(--mobile-padding);
}
.card {
padding: 20px 15px;
max-width: 80%;
}
.card__name {
font-size: 22px;
margin-bottom: 15px;
}
.timer-display {
font-size: 100px !important;
margin: 30px 0 !important;
letter-spacing: 6px !important;
min-height: 120px !important;
font-weight: 800 !important;
font-family: 'Courier New', 'Consolas', 'Monaco', monospace !important;
text-align: center !important;
color: var(--primary) !important;
}
}
}
/* iPhone安全区域适配 */
@Supports (padding-top: env(safe-area-inset-top)) {
:root {
--safe-top: env(safe-area-inset-top);
--safe-bottom: env(safe-area-inset-bottom);
--safe-left: env(safe-area-inset-left);
--safe-right: env(safe-area-inset-right);
}
.header {
padding-top: calc(12px + var(--safe-top));
}
.drawer, .history {
padding-top: var(--safe-top);
padding-bottom: var(--safe-bottom);
z-index: 1001;
}
.drawer-header, .history-header {
position: sticky;
top: 0;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px);
z-index: 1002;
padding: max(12px, env(safe-area-inset-top)) var(--mobile-padding) 12px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
}
.drawer-header h3, .history-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.drawer-header .btn, .history-header .btn {
width: auto;
min-width: 32px;
height: 32px;
padding: 6px;
font-size: 18px;
font-weight: bold;
border-radius: 50%;
background: rgba(0, 0, 0, 0.05);
color: var(--text);
display: flex;
align-items: center;
justify-content: center;
margin: 0;
}
.controls {
padding-bottom: calc(15px + var(--safe-bottom));
}
.main {
padding-top: calc(var(--safe-top) + 10px);
padding-bottom: calc(var(--safe-bottom) + 10px);
}
}
/* 中等屏幕优化 */
@media (max-width: 768px) and (min-width: 481px) {
.card {
padding: 100px 40px;
min-height: 400px;
height: auto;
max-height: 75vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 28px;
box-shadow:
0 12px 40px rgba(0, 0, 0, 0.15),
0 4px 12px rgba(0, 0, 0, 0.1);
}
.card__name {
font-size: 48px;
line-height: 1.5;
font-weight: 800;
margin-bottom: 0;
min-height: 120px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
letter-spacing: 2px;
}
.timer-display {
font-size: 80px !important;
margin: 25px 0 !important;
letter-spacing: 3px !important;
min-height: 100px !important;
font-weight: 800 !important;
font-family: 'Courier New', 'Consolas', 'Monaco', monospace !important;
text-align: center !important;
color: var(--primary) !important;
word-break: break-all;
overflow-wrap: break-word;
}
}
/* 超小屏手机优化 */
@media (max-width: 480px) {
:root {
--mobile-padding: 10px;
--mobile-radius: 10px;
}
body {
font-size: 13px;
-webkit-text-size-adjust: 100%;
}
.main {
padding: var(--mobile-padding);
margin: 0;
padding-bottom: calc(180px + env(safe-area-inset-bottom));
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100vh;
}
.controls {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 999;
max-height: 50vh;
overflow-y: auto;
background: var(--card-bg);
backdrop-filter: blur(20px);
box-shadow: 0 -4px 20px var(--shadow);
padding: max(12px, env(safe-area-inset-bottom)) var(--mobile-padding);
border-radius: var(--mobile-radius) var(--mobile-radius) 0 0;
border-top: 1px solid var(--border);
pointer-events: auto;
}
.mode-tabs {
position: relative;
z-index: 1000;
pointer-events: auto;
}
.mode-tab {
position: relative;
z-index: 1001;
pointer-events: auto;
}
.header {
padding: max(10px, env(safe-area-inset-top)) var(--mobile-padding);
min-height: 55px;
gap: 6px;
}
.logo {
font-size: 15px;
font-weight: 600;
}
.card {
padding: 80px 25px;
margin: 0 auto;
max-width: 92%;
border-radius: 24px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.12),
0 2px 8px rgba(0, 0, 0, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
min-height: 280px;
height: auto;
max-height: 70vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
}
.card__name {
font-size: 36px;
line-height: 1.4;
font-weight: 800;
margin-bottom: 0;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
width: 100%;
min-height: 100px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
letter-spacing: 1px;
word-break: break-word;
hyphens: auto;
padding: 0 10px;
}
.card__name.long-text {
font-size: 20px;
}
.card__name.very-long-text {
font-size: 17px;
}
.card__name.extra-long-text {
font-size: 14px;
}
.controls {
padding: max(12px, env(safe-area-inset-bottom)) var(--mobile-padding);
border-radius: var(--mobile-radius) var(--mobile-radius) 0 0;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 999;
max-height: 40vh;
overflow-y: auto;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
}
.mode-tabs {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
padding: 6px;
border-radius: var(--mobile-radius);
margin-bottom: 8px;
}
.mode-tab {
padding: 6px 4px;
font-size: 10px;
min-width: auto;
border-radius: 6px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-height: 36px;
display: flex;
align-items: center;
justify-content: center;
}
.btn {
padding: 10px 12px;
font-size: 13px;
border-radius: var(--mobile-radius);
min-height: 42px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.control-buttons {
display: flex;
gap: 8px;
justify-content: center;
flex-wrap: wrap;
}
.control-buttons .btn {
flex: 1;
min-width: 0;
max-width: 120px;
}
.slider-container {
padding: 10px;
border-radius: var(--mobile-radius);
gap: 6px;
}
.slider-container label {
font-size: 12px;
}
.voice-toggle, .theme-selector {
padding: 5px 8px;
font-size: 11px;
border-radius: 14px;
}
.theme-btn {
width: 20px;
height: 20px;
}
.drawer-toggle {
width: 36px;
height: 36px;
padding: 8px;
font-size: 12px;
border-radius: 50%;
}
.drawer-toggle.left {
left: var(--mobile-padding);
}
.drawer-toggle.right {
right: var(--mobile-padding);
}
.timer-display {
font-size: clamp(40px, 8vw, 56px);
margin: 15px 0;
font-weight: 800;
letter-spacing: 1px;
min-height: auto;
max-height: 120px;
line-height: 1.2;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
word-break: break-all;
overflow-wrap: break-word;
padding: 0 10px;
box-sizing: border-box;
overflow: hidden;
}
/* 超小屏手机 */
@media (max-width: 375px) {
.card {
padding: 60px 15px;
max-width: 95%;
border-radius: 20px;
}
.card__name {
font-size: 28px;
line-height: 1.3;
min-height: 80px;
}
.timer-display {
font-size: clamp(32px, 7vw, 44px);
letter-spacing: 0.5px;
margin: 12px 0;
max-height: 100px;
padding: 0 5px;
}
}
/* 极小屏手机 */
@media (max-width: 320px) {
.card {
padding: 50px 12px;
}
.card__name {
font-size: 24px;
}
.timer-display {
font-size: clamp(28px, 6vw, 38px);
margin: 10px 0;
max-height: 90px;
letter-spacing: 0;
}
}
.barrage-item {
font-size: 13px;
padding: 8px 14px;
border-radius: 14px;
}
.mode-options {
padding: 8px 6px;
border-radius: var(--mobile-radius);
display: flex;
flex-direction: column;
gap: 6px;
}
.mode-options .btn {
width: 100%;
max-width: none;
margin: 2px 0;
font-size: 12px;
padding: 8px 10px;
}
/* 手机端抽屉优化 */
.drawer {
width: 100%;
top: 0;
border-radius: 0;
box-shadow: 0 0 50px var(--shadow);
z-index: 1001;
}
.history {
width: 100%;
top: 0;
border-radius: 0;
box-shadow: 0 0 50px var(--shadow);
z-index: 1001;
}
.drawer-header, .history-header {
padding: max(15px, env(safe-area-inset-top)) var(--mobile-padding) 15px;
font-size: 18px;
min-height: 60px;
border-radius: 0;
}
.drawer-header h3, .history-header h3 {
font-size: 18px;
}
.drawer-content, #historyList {
padding: 20px var(--mobile-padding);
}
.student-input {
font-size: 16px;
padding: 12px;
min-height: 180px;
border-radius: 10px;
}
.drawer-actions {
flex-direction: column;
gap: 10px;
margin-top: 15px;
}
.drawer-actions .btn {
width: 100%;
padding: 15px;
font-size: 16px;
border-radius: 12px;
min-height: 48px;
}
.history-item {
padding: 15px;
margin-bottom: 12px;
border-radius: 10px;
}
.history-item-content {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.history-name {
font-size: 15px;
}
.history-time {
font-size: 11px;
}
/* 手机端控制面板优化 */
.controls {
padding: max(12px, env(safe-area-inset-bottom)) var(--mobile-padding);
max-height: 45vh;
}
.mode-tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
padding: 6px;
margin-bottom: 12px;
}
.mode-tab {
padding: 8px 4px;
font-size: 11px;
min-height: 36px;
display: flex;
align-items: center;
justify-content: center;
}
.mode-options {
padding: 8px 4px;
}
.mode-options > div {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
justify-content: center;
}
.mode-options .btn {
flex: 1;
min-width: 0;
padding: 10px 8px;
font-size: 13px;
min-height: 40px;
margin: 2px;
}
.mode-options .btn.btn-primary {
flex: 1.5;
}
.slider-container {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
margin: 4px 0;
}
.slider-container label {
font-size: 12px;
margin: 0;
flex: 0 0 auto;
}
.slider-container .slider {
flex: 1;
margin: 0 8px;
min-width: 60px;
}
.slider-container span {
font-size: 11px;
flex: 0 0 auto;
}
.control-row:last-child {
margin-bottom: 0;
}
.control-row:last-child .slider-container {
flex: 1;
margin: 0;
}
.control-row:last-child .btn {
flex: 0 0 auto;
width: auto;
min-width: 80px;
padding: 8px 12px;
font-size: 12px;
min-height: 36px;
}
.control-row:last-child > div {
display: flex;
gap: 6px;
align-items: center;
}
/* 横屏超小屏优化 */
@media (max-height: 450px) and (orientation: landscape) {
.header {
min-height: 45px;
padding: 6px var(--mobile-padding);
}
.card {
padding: 15px 10px;
max-width: 85%;
}
.card__name {
font-size: 18px;
margin-bottom: 10px;
}
.controls {
padding: 8px var(--mobile-padding);
}
.mode-tabs {
grid-template-columns: repeat(4, 1fr);
}
.btn {
padding: 8px 12px;
font-size: 12px;
min-height: 36px;
}
}
}
/* 无障碍 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Toast 提示 */
.toast {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: var(--text);
color: white;
padding: 15px 25px;
border-radius: 25px;
box-shadow: 0 4px 12px var(--shadow);
z-index: 2000;
animation: toastIn 0.3s ease;
}
@keyframes toastIn {
from {
transform: translateX(-50%) translateY(-20px);
opacity: 0;
}
to {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
}
</style>
</head>
<body>
<!-- 粒子背景 -->
<canvas id="particles-canvas"></canvas>
<!-- 顶部栏 -->
<header class="header">
<div class="logo">🎤 语音点名器 v2.0</div>
<div class="header-controls">
<div class="voice-toggle">
<span id="voiceIcon">🔊</span>
<span id="voiceText">语音开启</span>
</div>
<div class="theme-selector">
<button class="theme-btn theme-btn--macaron active" title="马卡龙主题" style="background: linear-gradient(135deg, #ff6b6b, #ffd93d);"></button>
<button class="theme-btn theme-btn--geek" title="极客蓝主题" style="background: linear-gradient(135deg, #0a0a0a, #1a1a2e);"></button>
<button class="theme-btn theme-btn--purple" title="梦幻紫主题" style="background: linear-gradient(135deg, #667eea, #764ba2);"></button>
<button class="theme-btn theme-btn--warm" title="温暖红主题" style="background: linear-gradient(135deg, #ff6b6b, #ff5252);"></button>
<button class="theme-btn theme-btn--nature" title="自然绿主题" style="background: linear-gradient(135deg, #4caf50, #388e3c);"></button>
</div>
</div>
</header>
<!-- 左侧名单抽屉 -->
<div class="drawer" id="studentDrawer">
<div class="drawer-header">
<h3>学生名单</h3>
<button class="btn btn-secondary">×</button>
</div>
<div class="drawer-content">
<textarea class="student-input" id="studentInput" placeholder="每行一个学生姓名
统一使用txt格式,每行一个学生姓名
支持中文编码,自动处理乱码
拖拽文件到此处也可导入
📋 导入模板示例:
张三
李四
王五
赵六"></textarea>
<div class="drawer-actions">
<button class="btn btn-primary">保存名单</button>
<button class="btn btn-import" style="background: linear-gradient(135deg, #4caf50, #45a049); color: white; border: none; border-radius: 20px; padding: 8px 16px; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 4px;">
📥 导入
</button>
<input type="file" id="importFile" accept=".csv,.txt" style="display: none;">
<button class="btn btn-secondary">清空</button>
<button class="btn btn-secondary">导出TXT</button>
<button class="btn btn-secondary" style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; border-radius: 20px; padding: 8px 16px; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 4px;">
📋 模板
</button>
</div>
<div style="margin-top: 15px;">
<small>当前学生:<span id="studentCount">0</span>人</small>
</div>
</div>
</div>
<!-- 右侧历史抽屉 -->
<div class="history" id="historyDrawer">
<div class="drawer-header">
<h3>历史记录</h3>
<div style="display: flex; gap: 8px; align-items: center;">
<button class="btn btn-export" style="font-size: 13px; padding: 8px 14px; background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; border: none; border-radius: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: all 0.3s ease; min-height: 32px; display: flex; align-items: center; gap: 4px;">
📊 导出CSV
</button>
<button class="btn btn-clear" style="font-size: 13px; padding: 8px 14px; background: linear-gradient(135deg, #ff6b6b, #ff8e53); color: white; border: none; border-radius: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: all 0.3s ease; min-height: 32px; display: flex; align-items: center; gap: 4px;">
🗑️ 清空
</button>
<button class="btn btn-close" style="font-size: 18px; padding: 4px 8px; background: none; color: var(--text-light); border: none; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease;">×</button>
</div>
</div>
<div id="historyList" style="padding: 20px;"></div>
</div>
<!-- 主舞台 -->
<main class="main">
<div class="card" id="mainCard">
<div id="displayArea">
<div class="card__name" id="displayName">准备开始</div>
<div class="progress-bar" id="progressBar" style="display: none;">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="timer-display" id="timerDisplay" style="display: none; font-size: 120px; font-weight: 800; font-family: 'Courier New', monospace; letter-spacing: 8px; color: var(--primary);">00:30</div>
</div>
<!-- 3D 翻转卡片(隐藏) -->
<div class="card-3d" id="card3d" style="display: none;">
<div class="card-inner">
<div class="card-face card-front">准备开始</div>
<div class="card-face card-back" id="cardBack">姓名</div>
</div>
</div>
</div>
</main>
<!-- 弹幕容器 -->
<div class="barrage" id="barrageContainer"></div>
<!-- 控制面板 -->
<div class="controls">
<div class="control-row">
<div class="mode-tabs">
<button class="mode-tab active" data-mode="question">上课提问</button>
<button class="mode-tab" data-mode="sequence">顺序点名</button>
<button class="mode-tab" data-mode="random">随机点名</button>
<button class="mode-tab" data-mode="quick">快速连抽</button>
<button class="mode-tab" data-mode="timer">计时器</button>
</div>
</div>
<div class="control-row" id="controlOptions">
<!-- 上课提问模式选项 -->
<div id="questionOptions" class="mode-options">
<button class="btn btn-primary">随机提问</button>
<label>
<input type="checkbox" id="enableQuestionParticles" checked> 粒子背景
</label>
</div>
<!-- 顺序模式选项 -->
<div id="sequenceOptions" class="mode-options" style="display: none;">
<div class="slider-container">
<label>间隔时间:</label>
<input type="range" class="slider" id="intervalSlider" min="1" max="60" value="1">
<span id="intervalValue">1</span>秒
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
<button class="btn btn-primary">开始</button>
<button class="btn btn-secondary">暂停</button>
<button class="btn btn-secondary">下一位</button>
<button class="btn btn-secondary">上一位</button>
</div>
</div>
<!-- 随机模式选项 -->
<div id="randomOptions" class="mode-options" style="display: none;">
<div style="display: flex; gap: 6px; flex-wrap: wrap; align-items: center;">
<button class="btn btn-primary">开始</button>
<label style="display: flex; align-items: center; font-size: 12px; margin: 0;">
<input type="checkbox" id="enable3d"> 3D效果
</label>
<label style="display: flex; align-items: center; font-size: 12px; margin: 0;">
<input type="checkbox" id="enableParticles" checked> 粒子
</label>
</div>
</div>
<!-- 连抽模式选项 -->
<div id="quickOptions" class="mode-options" style="display: none;">
<div class="slider-container">
<label>人数:</label>
<input type="range" class="slider" id="quickCountSlider" min="1" max="10" value="3">
<span id="quickCountValue">3</span>人
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
<button class="btn btn-primary">开始</button>
<button class="btn btn-secondary">重置</button>
</div>
</div>
<!-- 计时器模式选项 -->
<div id="timerOptions" class="mode-options" style="display: none;">
<div class="slider-container">
<label>时间:</label>
<select class="btn" id="timerSelect" style="flex: 1; min-width: 0; font-size: 12px; padding: 6px 8px;">
<option value="30">30秒</option>
<option value="60">1分钟</option>
<option value="90">90秒</option>
<option value="120">2分钟</option>
<option value="custom">自定义</option>
</select>
<input type="number" id="customTimer" placeholder="秒" style="display: none; width: 60px; font-size: 12px; padding: 6px;" min="1" max="3600">
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
<button class="btn btn-primary" id="startTimerBtn">开始</button>
<button class="btn btn-secondary" id="pauseTimerBtn" style="display: none;">暂停</button>
<button class="btn btn-secondary">重置</button>
</div>
</div>
</div>
<div class="control-row">
<div style="display: flex; gap: 6px; align-items: center; flex-wrap: wrap;">
<div class="slider-container" style="flex: 1; margin: 0; min-width: 120px;">
<label style="font-size: 12px; margin: 0;">语速</label>
<input type="range" class="slider" id="speechRateSlider" min="0.5" max="2" step="0.1" value="1" style="margin: 0 4px;">
<span id="speechRateValue" style="font-size: 11px;">1.0×</span>
</div>
<button class="btn btn-secondary" style="flex: 0 0 auto; min-width: 60px; padding: 8px 10px; font-size: 12px;">测试</button>
<button class="btn btn-secondary" style="flex: 0 0 auto; min-width: 60px; padding: 8px 10px; font-size: 12px;">重置</button>
</div>
</div>
</div>
<!-- 抽屉切换按钮 -->
<button class="drawer-toggle left" aria-label="打开学生名单">
<span class="icon">☰</span>
</button>
<button class="drawer-toggle right" aria-label="打开历史记录">
<span class="icon">📊</span>
</button>
<script>
// 全局状态管理
const app = {
students: [],
currentIndex: 0,
history: [],
weights: {},
isRunning: false,
currentMode: 'question',
intervalId: null,
timerId: null,
remainingTime: 0,
speechEnabled: true,
speechRate: 1,
voiceMode: {
sequence: { interval: 1 },
random: { enable3d: true, enableParticles: true },
quick: { count: 3 },
timer: { duration: 30 }
}
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
loadData();
initEventListeners();
initParticles();
// 初始化默认模式为上课提问,确保手机端正确显示
switchMode('question');
updateDisplay();
});
// 数据持久化
function loadData() {
const savedStudents = localStorage.getItem('students');
if (savedStudents) {
app.students = JSON.parse(savedStudents);
}
const savedHistory = localStorage.getItem('history');
if (savedHistory) {
app.history = JSON.parse(savedHistory);
}
const savedWeights = localStorage.getItem('weights');
if (savedWeights) {
app.weights = JSON.parse(savedWeights);
}
const savedSettings = localStorage.getItem('settings');
if (savedSettings) {
const settings = JSON.parse(savedSettings);
app.speechEnabled = settings.speechEnabled ?? true;
app.speechRate = settings.speechRate ?? 1;
app.voiceMode = { ...app.voiceMode, ...settings.voiceMode };
}
updateStudentCount();
updateHistoryList();
}
function saveData() {
localStorage.setItem('students', JSON.stringify(app.students));
localStorage.setItem('history', JSON.stringify(app.history));
localStorage.setItem('weights', JSON.stringify(app.weights));
localStorage.setItem('settings', JSON.stringify({
speechEnabled: app.speechEnabled,
speechRate: app.speechRate,
voiceMode: app.voiceMode
}));
}
// 学生名单管理
function saveStudents() {
const input = document.getElementById('studentInput').value;
const lines = input.split('\n').map(line => line.trim()).filter(line => line);
app.students = [...new Set(lines)];
// 初始化权重
app.students.forEach(name => {
if (!app.weights[name]) {
app.weights[name] = 1;
}
});
updateStudentCount();
saveData();
showToast('名单已保存');
}
function clearStudents() {
document.getElementById('studentInput').value = '';
app.students = [];
app.weights = {};
updateStudentCount();
saveData();
showToast('名单已清空');
}
function exportStudents() {
// 导出为纯名字的格式
const txt = app.students.join('\n');
downloadFile(txt, '学生名单.txt', 'text/plain');
}
function downloadTemplate() {
const template = `# 学生名单导入模板
# 统一使用txt格式,每行一个学生姓名
# 每行必须是一个有效的学生姓名
# 示例:纯名字格式(推荐)
张三
李四
王五
赵六
孙七
周八
吴九
郑十
钱十一
孙十二
# 注意:
# 1. 每行一个学生姓名
# 2. 不要有空行
# 3. 不要有特殊字符
# 4. 支持中文姓名`;
downloadFile(template, '学生名单模板.txt', 'text/plain');
}
function importStudents() {
document.getElementById('importFile').click();
}
function handleFileImport(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
let content = e.target.result;
// 处理可能的编码问题
try {
// 如果检测到乱码,尝试重新解码
if (content.indexOf('�') !== -1 || content.indexOf('\uFFFD') !== -1) {
// 使用GB2312编码重新读取(针对中文Windows系统)
const decoder = new TextDecoder('gb2312');
const bytes = new Uint8Array(e.target.result);
content = decoder.decode(bytes);
}
} catch (error) {
console.warn('编码处理失败,使用原始内容');
}
// 智能编码检测和处理
const lines = content
.replace(/\r\n/g, '\n') // 统一换行符
.replace(/\r/g, '\n') // 处理Mac换行符
.split('\n')
.map(line => {
// 移除BOM标记和空白字符
return line.replace(/^\uFEFF/, '').trim();
})
.filter(line => line && !line.startsWith('#'));
if (lines.length > 0) {
console.log('开始处理导入数据,共', lines.length, '行');
// 处理纯名字格式
const names = [];
for (let line of lines) {
line = line.trim();
console.log('处理行:', line);
if (!line || line.startsWith('#')) {
console.log('跳过空行或注释:', line);
continue;
}
// 直接使用整行作为姓名
let name = line;
// 移除可能的学号前缀,但保留中文姓名
const hasChinese = /[\u4e00-\u9fa5]/.test(line);
if (hasChinese) {
// 如果包含中文,直接使用整行
name = line;
} else {
// 如果没有中文,移除可能的数字前缀
name = line.replace(/^[0-9A-Za-z]+/, '').trim();
if (!name) name = line;
}
if (name && name.length > 0) {
names.push(name);
console.log('添加姓名:', name);
}
}
console.log('解析完成,共', names.length, '个姓名');
if (names.length === 0) {
console.log('未找到有效姓名');
showToast('未找到有效的学生姓名');
return;
}
// 合并到现有名单
const newStudents = [...new Set([...app.students, ...names])];
const addedCount = newStudents.length - app.students.length;
app.students = newStudents;
console.log('导入前学生数:', app.students.length - addedCount);
console.log('新增学生数:', addedCount);
console.log('导入后学生数:', app.students.length);
// 初始化权重
app.weights = {};
app.students.forEach(name => {
app.weights[name] = 1;
});
updateStudentCount();
saveData();
showToast(`成功导入 ${addedCount} 个新学生,共 ${names.length} 个`);
} else {
console.log('文件为空');
showToast('文件为空或格式不正确');
}
};
reader.onerror = function() {
showToast('文件读取失败,请检查文件格式');
};
// 使用UTF-8编码读取
reader.readAsText(file, 'UTF-8');
// 清空文件输入框,允许重复导入同一文件
event.target.value = '';
}
function updateStudentCount() {
document.getElementById('studentCount').textContent = app.students.length;
document.getElementById('studentInput').value = app.students.join('\n');
}
// 历史记录
function addHistory(name, mode) {
const record = {
name,
mode,
time: new Date().toLocaleString('zh-CN'),
timestamp: Date.now()
};
app.history.unshift(record);
if (app.history.length > 100) {
app.history = app.history.slice(0, 100);
}
updateHistoryList();
saveData();
}
function updateHistoryList() {
const container = document.getElementById('historyList');
container.innerHTML = '';
app.history.forEach(record => {
const item = document.createElement('div');
item.className = 'history-item';
item.innerHTML = `
<div>
<div>${record.name}</div>
<div class="history-time">${record.time}</div>
</div>
<small>${record.mode}</small>
`;
container.appendChild(item);
});
}
function clearHistory() {
app.history = [];
updateHistoryList();
saveData();
showToast('历史已清空');
}
function exportHistory() {
const csv = app.history.map(h => `${h.time},${h.name},${h.mode}`).join('\n');
downloadFile('时间,姓名,模式\n' + csv, 'history.csv', 'text/csv');
}
// 语音功能
function speak(text, onEnd = null) {
if (!app.speechEnabled) return;
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = app.speechRate;
utterance.lang = navigator.language.startsWith('zh') ? 'zh-CN' : 'en-US';
if (onEnd) {
utterance.onend = onEnd;
}
try {
speechSynthesis.speak(utterance);
} catch (error) {
showToast('语音播放失败,请检查浏览器设置');
// 文本闪烁提示
const display = document.getElementById('displayName');
display.style.animation = 'pulse 0.5s ease 3';
}
}
function testSpeech() {
speak('测试语音');
}
function toggleVoice() {
app.speechEnabled = !app.speechEnabled;
const icon = document.getElementById('voiceIcon');
const text = document.getElementById('voiceText');
if (app.speechEnabled) {
icon.textContent = '🔊';
text.textContent = '语音开启';
} else {
icon.textContent = '🔇';
text.textContent = '语音关闭';
}
saveData();
}
// 点名功能
function switchMode(mode) {
console.log('切换到模式:', mode);
app.currentMode = mode;
// 更新标签页
document.querySelectorAll('.mode-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.mode === mode);
});
// 显示对应选项
document.querySelectorAll('.mode-options').forEach(options => {
options.style.display = 'none';
});
const optionsMap = {
sequence: 'sequenceOptions',
random: 'randomOptions',
quick: 'quickOptions',
question: 'questionOptions',
timer: 'timerOptions'
};
if (optionsMap[mode]) {
document.getElementById(optionsMap[mode]).style.display = 'block';
}
// 强制重置所有显示状态 - 修复计时器显示问题
if (mode === 'timer') {
// 计时器模式:显示计时器,隐藏姓名显示
document.getElementById('timerDisplay').style.display = 'block';
document.getElementById('displayName').style.display = 'none';
} else {
// 其他所有模式:隐藏计时器,显示姓名显示
document.getElementById('timerDisplay').style.display = 'none';
document.getElementById('displayName').style.display = 'block';
// 强制停止计时器
if (app.isRunning) {
app.isRunning = false;
if (app.timerId) {
clearTimeout(app.timerId);
app.timerId = null;
}
}
// 重置计时器状态
app.remainingTime = 0;
// 重置显示
updateDisplay('准备开始');
}
}
// 顺序点名
function startSequence() {
if (app.students.length === 0) {
showToast('请先添加学生名单');
toggleDrawer();
return;
}
app.isRunning = true;
const interval = parseInt(document.getElementById('intervalSlider').value);
function next() {
if (!app.isRunning) return;
const name = app.students[app.currentIndex];
updateDisplay(name);
addHistory(name, '顺序点名');
speak(name, () => {
// 检查是否到达最后一个学生
if (app.currentIndex >= app.students.length - 1) {
// 到达最后一个学生,自动停止
app.isRunning = false;
updateDisplay('点名完成');
showToast('顺序点名已完成');
return;
}
// 继续下一个学生
app.currentIndex = app.currentIndex + 1;
if (app.isRunning) {
setTimeout(next, interval * 1000);
}
});
}
next();
}
function pauseSequence() {
app.isRunning = false;
updateDisplay('已暂停');
}
function nextStudent() {
if (app.students.length === 0) return;
app.currentIndex = (app.currentIndex + 1) % app.students.length;
updateDisplay(app.students[app.currentIndex]);
}
function prevStudent() {
if (app.students.length === 0) return;
app.currentIndex = (app.currentIndex - 1 + app.students.length) % app.students.length;
updateDisplay(app.students[app.currentIndex]);
}
// 随机点名
function startRandom() {
if (app.students.length === 0) {
showToast('请先添加学生名单');
toggleDrawer();
return;
}
const candidates = app.students.filter(name => app.weights[name] > 0);
if (candidates.length === 0) {
showToast('所有学生都已点到');
return;
}
app.isRunning = true;
// 权重选择
const totalWeight = candidates.reduce((sum, name) => sum + app.weights[name], 0);
let random = Math.random() * totalWeight;
let selected = candidates[0];
for (const name of candidates) {
random -= app.weights[name];
if (random <= 0) {
selected = name;
break;
}
}
// 3D动画
const enable3d = document.getElementById('enable3d').checked;
if (enable3d) {
document.getElementById('mainCard').style.display = 'none';
document.getElementById('card3d').style.display = 'block';
document.querySelector('.card-3d').classList.add('flipping');
setTimeout(() => {
document.getElementById('cardBack').textContent = selected;
}, 600);
setTimeout(() => {
document.querySelector('.card-3d').classList.remove('flipping');
setTimeout(() => {
document.getElementById('mainCard').style.display = 'block';
document.getElementById('card3d').style.display = 'none';
finishRandom(selected);
}, 300);
}, 1200);
} else {
finishRandom(selected);
}
if (document.getElementById('enableParticles').checked) {
createParticles();
}
}
function finishRandom(name) {
updateDisplay(name);
addHistory(name, '随机点名');
app.weights[name] = 1 / (getCallCount(name) + 1);
speak(name, () => {
app.isRunning = false;
});
}
// 上课提问
function askQuestion() {
if (app.students.length === 0) {
showToast('请先添加学生名单');
toggleDrawer();
return;
}
if (app.isRunning) {
return;
}
app.isRunning = true;
// 动态闪现效果
let flashCount = 0;
const maxFlashes = 20; // 2秒内闪现20次,每次100ms
const flashInterval = setInterval(() => {
// 随机显示一个学生名字
const randomIndex = Math.floor(Math.random() * app.students.length);
const randomStudent = app.students[randomIndex];
updateDisplay(randomStudent);
flashCount++;
// 2秒后停止闪现并确定最终结果
if (flashCount >= maxFlashes) {
clearInterval(flashInterval);
// 最终随机选择一个学生
const finalIndex = Math.floor(Math.random() * app.students.length);
const selectedStudent = app.students[finalIndex];
// 显示最终选中的学生
updateDisplay(selectedStudent);
addHistory(selectedStudent, '上课提问');
// 播报学生姓名
speak(selectedStudent, () => {
app.isRunning = false;
});
}
}, 100); // 每100ms切换一次
// 粒子效果
if (document.getElementById('enableQuestionParticles').checked) {
createParticles();
}
}
// 快速连抽
function startQuickDraw() {
if (app.students.length === 0) {
showToast('请先添加学生名单');
toggleDrawer();
return;
}
if (app.isRunning) return;
const count = parseInt(document.getElementById('quickCountSlider').value);
const candidates = app.students.filter(name => app.weights[name] > 0);
if (candidates.length < count) {
showToast(`可选学生不足 ${count} 人`);
return;
}
app.isRunning = true;
// 开始动态闪现效果
let flashCount = 0;
const maxFlashes = 20; // 2秒内闪现20次
const flashInterval = 100; // 每100毫秒闪现一次
const flashTimer = setInterval(() => {
// 随机显示一些名字进行闪现
const flashNames = [];
for (let i = 0; i < count; i++) {
const randomName = candidates[Math.floor(Math.random() * candidates.length)];
flashNames.push(randomName);
}
updateDisplay(flashNames.join('、'));
flashCount++;
if (flashCount >= maxFlashes) {
clearInterval(flashTimer);
// 闪现结束,进行最终选择
const selected = selectMultipleStudents(candidates, count);
// 显示所有选中的名字
const allNames = selected.join('、');
updateDisplay(allNames);
// 逐个播报和记录
selected.forEach((name, index) => {
setTimeout(() => {
addHistory(name, '快速连抽');
app.weights[name] = 1 / (getCallCount(name) + 1);
speak(name);
// 如果是最后一个,结束运行状态
if (index === selected.length - 1) {
setTimeout(() => {
app.isRunning = false;
}, 1000);
}
}, index * 800);
});
}
}, flashInterval);
}
function selectMultipleStudents(candidates, count) {
const selected = [];
const tempCandidates = [...candidates];
for (let i = 0; i < count && tempCandidates.length > 0; i++) {
const totalWeight = tempCandidates.reduce((sum, name) => sum + app.weights[name], 0);
let random = Math.random() * totalWeight;
let selectedIndex = 0;
for (let j = 0; j < tempCandidates.length; j++) {
random -= app.weights[tempCandidates[j]];
if (random <= 0) {
selectedIndex = j;
break;
}
}
selected.push(tempCandidates[selectedIndex]);
tempCandidates.splice(selectedIndex, 1);
}
return selected;
}
function resetWeights() {
if (app.isRunning) {
showToast('请等待当前操作完成后再重置权重');
return;
}
app.students.forEach(name => {
app.weights[name] = 1;
});
saveData();
showToast('权重已重置');
}
function getCallCount(name) {
return app.history.filter(h => h.name === name).length;
}
// 计时器
function startTimer() {
const select = document.getElementById('timerSelect');
let duration = parseInt(select.value);
if (select.value === 'custom') {
duration = parseInt(document.getElementById('customTimer').value);
if (!duration || duration < 1) {
showToast('请输入有效的时间');
return;
}
}
// 如果是从暂停状态继续,不重置时间
if (!app.isRunning && app.remainingTime <= 0) {
app.remainingTime = duration;
}
app.isRunning = true;
// 更新按钮显示
document.getElementById('startTimerBtn').style.display = 'none';
document.getElementById('pauseTimerBtn').style.display = 'inline-block';
document.getElementById('timerDisplay').style.display = 'block';
document.getElementById('displayName').style.display = 'none';
function updateTimer() {
if (!app.isRunning) return;
const minutes = Math.floor(app.remainingTime / 60);
const seconds = app.remainingTime % 60;
const timeText = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
// 先更新显示,确保视觉和语音同步
document.getElementById('timerDisplay').textContent = timeText;
// 语音播报逻辑 - 调整播报时机,确保准确性
if (app.speechEnabled) {
const currentTime = app.remainingTime;
// 精确播报时机:在当前时间点的下一秒开始播报
setTimeout(() => {
// 最后10秒逐秒播报
if (currentTime <= 10 && currentTime > 0) {
speak(`${currentTime}`);
}
// 每10秒播报(最后30秒内,不包括逐秒播报的区间)
else if (currentTime <= 30 && currentTime % 10 === 0 && currentTime > 10) {
speak(`${currentTime}秒`);
}
// 每分钟播报(超过1分钟时)
else if (currentTime > 60 && currentTime % 60 === 0) {
speak(`${minutes}分钟${seconds > 0 ? seconds + '秒' : ''}剩余`);
}
}, 100); // 延迟100ms确保显示更新完成
}
if (app.remainingTime <= 0) {
setTimeout(() => {
speak('时间到!');
}, 100);
pauseTimer();
return;
}
app.remainingTime--;
app.timerId = setTimeout(updateTimer, 1000);
}
updateTimer();
}
function pauseTimer() {
app.isRunning = false;
if (app.timerId) {
clearTimeout(app.timerId);
app.timerId = null;
}
// 如果还有剩余时间,显示暂停状态
if (app.remainingTime > 0) {
const minutes = Math.floor(app.remainingTime / 60);
const seconds = app.remainingTime % 60;
const timeText = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
// 保持计时器显示,但添加暂停标识
document.getElementById('timerDisplay').textContent = timeText + ' (已暂停)';
document.getElementById('timerDisplay').style.display = 'block';
document.getElementById('displayName').style.display = 'none';
// 更新按钮状态
document.getElementById('startTimerBtn').style.display = 'inline-block';
document.getElementById('pauseTimerBtn').style.display = 'none';
} else {
// 时间到了才显示计时结束
document.getElementById('timerDisplay').style.display = 'none';
document.getElementById('displayName').style.display = 'block';
updateDisplay('计时结束');
// 重置按钮状态
document.getElementById('startTimerBtn').style.display = 'inline-block';
document.getElementById('pauseTimerBtn').style.display = 'none';
}
}
function togglePauseTimer() {
if (app.isRunning) {
pauseTimer();
if (app.speechEnabled) {
speak('计时已暂停');
}
} else {
// 继续计时
if (app.speechEnabled) {
speak('继续计时');
}
startTimer();
}
}
function resetTimer() {
app.isRunning = false;
if (app.timerId) {
clearTimeout(app.timerId);
app.timerId = null;
}
// 重置显示
document.getElementById('timerDisplay').style.display = 'none';
document.getElementById('displayName').style.display = 'block';
updateDisplay('准备开始');
// 重置按钮状态
document.getElementById('startTimerBtn').style.display = 'inline-block';
document.getElementById('pauseTimerBtn').style.display = 'none';
// 语音播报
if (app.speechEnabled) {
speak('计时器已重置');
}
}
// UI 更新
function updateDisplay(text) {
const displayElement = document.getElementById('displayName');
const displayText = text || '准备开始';
displayElement.textContent = displayText;
// 移除所有字体大小类
displayElement.classList.remove('long-text', 'very-long-text', 'extra-long-text');
// 根据文本长度添加相应的字体大小类
const textLength = displayText.length;
if (textLength > 30) {
displayElement.classList.add('extra-long-text');
} else if (textLength > 20) {
displayElement.classList.add('very-long-text');
} else if (textLength > 10) {
displayElement.classList.add('long-text');
}
}
// 抽屉控制
function toggleDrawer() {
document.getElementById('studentDrawer').classList.toggle('open');
}
function closeDrawer() {
document.getElementById('studentDrawer').classList.remove('open');
}
function toggleHistory() {
document.getElementById('historyDrawer').classList.toggle('open');
}
function closeHistory() {
document.getElementById('historyDrawer').classList.remove('open');
}
// 主题切换
function switchTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
saveData();
}
// 动画效果
function createBarrage(text) {
const item = document.createElement('div');
item.className = 'barrage-item';
item.textContent = text;
const top = Math.random() * (window.innerHeight - 200) + 100;
item.style.top = `${top}px`;
document.getElementById('barrageContainer').appendChild(item);
setTimeout(() => {
item.remove();
}, 3000);
}
function createParticles() {
const canvas = document.getElementById('particles-canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const particles = [];
const particleCount = 50;
for (let i = 0; i < particleCount; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2,
size: Math.random() * 3 + 1,
color: `hsl(${Math.random() * 360}, 70%, 50%)`
});
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = p.color;
ctx.fill();
});
if (app.isRunning) {
requestAnimationFrame(animate);
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
animate();
}
function initParticles() {
const canvas = document.getElementById('particles-canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// 简单的背景粒子
function drawBackground() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < 20; i++) {
ctx.beginPath();
ctx.arc(
Math.random() * canvas.width,
Math.random() * canvas.height,
Math.random() * 2 + 1,
0,
Math.PI * 2
);
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fill();
}
}
drawBackground();
}
// 事件监听器
function initEventListeners() {
// 滑块事件
document.getElementById('intervalSlider').addEventListener('input', (e) => {
document.getElementById('intervalValue').textContent = e.target.value;
app.voiceMode.sequence.interval = parseInt(e.target.value);
});
document.getElementById('quickCountSlider').addEventListener('input', (e) => {
document.getElementById('quickCountValue').textContent = e.target.value;
app.voiceMode.quick.count = parseInt(e.target.value);
});
document.getElementById('speechRateSlider').addEventListener('input', (e) => {
document.getElementById('speechRateValue').textContent = e.target.value;
app.speechRate = parseFloat(e.target.value);
saveData();
});
document.getElementById('timerSelect').addEventListener('change', (e) => {
const customInput = document.getElementById('customTimer');
customInput.style.display = e.target.value === 'custom' ? 'inline-block' : 'none';
});
// 手机端触摸支持 - 防止触摸事件被阻止
document.querySelectorAll('.mode-tab').forEach(tab => {
tab.addEventListener('touchstart', (e) => {
e.preventDefault();
const mode = e.target.dataset.mode;
switchMode(mode);
});
});
// 文件拖拽
const studentInput = document.getElementById('studentInput');
studentInput.addEventListener('dragover', (e) => {
e.preventDefault();
studentInput.style.borderColor = 'var(--primary)';
});
studentInput.addEventListener('dragleave', () => {
studentInput.style.borderColor = 'var(--border)';
});
studentInput.addEventListener('drop', (e) => {
e.preventDefault();
studentInput.style.borderColor = 'var(--border)';
const file = e.dataTransfer.files[0];
if (file && file.type === 'text/csv') {
const reader = new FileReader();
reader.onload = (e) => {
studentInput.value = e.target.result.split('\n').join('\n');
};
reader.readAsText(file);
}
});
// 键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
switch(e.key) {
case ' ':
e.preventDefault();
handleStartPause();
break;
case 'n':
case 'N':
nextStudent();
break;
case 'r':
case 'R':
if (app.currentMode === 'random') startRandom();
break;
case 'b':
case 'B':
if (app.currentMode === 'quick') startQuickDraw();
break;
case 't':
case 'T':
// 只在非手机端启用T键切换计时器
if (window.innerWidth > 768) {
switchMode('timer');
}
break;
case 'c':
case 'C':
if (confirm('确定要清空历史记录吗?')) {
clearHistory();
}
break;
}
});
// Konami 彩蛋
let konamiCode = [];
const konamiSequence = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'KeyB', 'KeyA'];
document.addEventListener('keydown', (e) => {
konamiCode.push(e.code);
if (konamiCode.length > konamiSequence.length) {
konamiCode = konamiCode.slice(1);
}
if (konamiCode.join('') === konamiSequence.join('')) {
triggerEasterEgg();
konamiCode = [];
}
});
}
function handleStartPause() {
switch(app.currentMode) {
case 'sequence':
if (app.isRunning) {
pauseSequence();
} else {
startSequence();
}
break;
case 'random':
if (!app.isRunning) startRandom();
break;
case 'quick':
if (!app.isRunning) startQuickDraw();
break;
case 'timer':
if (app.isRunning) {
pauseTimer();
} else {
startTimer();
}
break;
}
}
function triggerEasterEgg() {
showToast('🌈 彩蛋触发!彩虹粒子启动!');
// 创建彩虹粒子
const colors = ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3'];
const canvas = document.getElementById('particles-canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let frame = 0;
function rainbowEffect() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < app.students.length; i++) {
const x = (canvas.width / app.students.length) * i;
const y = canvas.height / 2 + Math.sin(frame * 0.1 + i) * 100;
ctx.font = '24px Arial';
ctx.fillStyle = colors[i % colors.length];
ctx.fillText(app.students[i], x, y);
}
frame++;
if (frame < 300) {
requestAnimationFrame(rainbowEffect);
} else {
initParticles();
}
}
rainbowEffect();
// 控制台 ASCII Art
console.log(`
╔═╗┌─┐┌┐ ┌─┐┬ ┬
║ ├┤ ├┴┐│ │└┬┘
╚═╝└─┘└─┘└─┘ ┴
`);
}
// 工具函数
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function resetAll() {
if (confirm('确定要重置所有数据吗?')) {
app.students = [];
app.history = [];
app.weights = {};
app.currentIndex = 0;
app.isRunning = false;
if (app.intervalId) clearInterval(app.intervalId);
if (app.timerId) clearTimeout(app.timerId);
updateDisplay('准备开始');
updateStudentCount();
updateHistoryList();
saveData();
showToast('已重置全部数据');
}
}
</script>
<!-- 倾企企服版权所有 - 加密版权信息 -->
<div id="copyright" style="position: fixed; bottom: 2px; right: 5px; font-size: 8px; color: #ccc; user-select: none; pointer-events: none; opacity: 0.6; z-index: 9999;" contenteditable="false" draggable="false">
倾企企服版权所有
</div>
<script>
// 版权信息保护脚本
(function() {
const copyrightDiv = document.getElementById('copyright');
// 防止右键菜单
copyrightDiv.addEventListener('contextmenu', function(e) {
e.preventDefault();
return false;
});
// 防止选择文字
copyrightDiv.addEventListener('selectstart', function(e) {
e.preventDefault();
return false;
});
// 防止复制
copyrightDiv.addEventListener('copy', function(e) {
e.preventDefault();
return false;
});
// 防止拖拽
copyrightDiv.addEventListener('dragstart', function(e) {
e.preventDefault();
return false;
});
// 使用CSS进一步防止编辑
copyrightDiv.style.setProperty('-webkit-user-select', 'none', 'important');
copyrightDiv.style.setProperty('-moz-user-select', 'none', 'important');
copyrightDiv.style.setProperty('-ms-user-select', 'none', 'important');
})();
</script>
</body>
</html>

