Files
online-attendance-lottery/index.html

2093 lines
60 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线点名抽奖</title>
<style>
/* ===== CSS Variables & Reset ===== */
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-card: #0f3460;
--text-primary: #e0e0e0;
--text-secondary: #a0a0b0;
--accent: #e94560;
--accent-hover: #ff6b81;
--success: #2ecc71;
--warning: #f39c12;
--border-radius: 12px;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
--transition: all 0.3s ease;
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
line-height: 1.6;
}
/* ===== Layout ===== */
#app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* ===== Header ===== */
.header {
text-align: center;
padding: 20px 0;
border-bottom: 1px solid rgba(255,255,255,0.08);
margin-bottom: 24px;
}
.header h1 {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
letter-spacing: 1px;
}
.pool-count {
font-size: 14px;
color: var(--text-secondary);
}
.pool-count b {
color: var(--accent);
font-size: 16px;
}
/* ===== Main Grid ===== */
.main {
display: flex;
flex-direction: column;
gap: 20px;
}
@media (min-width: 768px) {
.main {
flex-direction: row;
}
}
@media (min-width: 1200px) {
.main {
gap: 32px;
}
}
/* ===== Panel ===== */
.panel {
background: var(--bg-secondary);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
}
.panel h2 {
font-size: 18px;
margin-bottom: 16px;
color: var(--text-primary);
}
#namePanel {
flex: 1;
min-width: 0;
}
#lotteryPanel {
flex: 1.2;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
}
/* ===== Textarea ===== */
textarea {
width: 100%;
min-height: 100px;
padding: 12px;
border: 1px solid rgba(255,255,255,0.12);
border-radius: 8px;
background: var(--bg-card);
color: var(--text-primary);
font-size: 14px;
resize: vertical;
outline: none;
transition: var(--transition);
font-family: inherit;
}
textarea:focus {
border-color: var(--accent);
}
/* ===== Button Row ===== */
.btn-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 12px 0;
}
button, .file-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 18px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
font-family: inherit;
text-decoration: none;
}
button {
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid rgba(255,255,255,0.12);
}
button:hover {
background: rgba(255,255,255,0.08);
}
.file-btn {
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid rgba(255,255,255,0.12);
}
.file-btn:hover {
background: rgba(255,255,255,0.08);
}
/* ===== Name List ===== */
.name-list {
max-height: 200px;
overflow-y: auto;
margin-top: 12px;
padding: 8px;
background: var(--bg-card);
border-radius: 8px;
font-size: 13px;
color: var(--text-secondary);
line-height: 1.8;
}
.name-list:empty {
display: none;
}
.name-list .name-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
margin: 2px;
border-radius: 4px;
background: rgba(255,255,255,0.06);
color: var(--text-secondary);
position: relative;
}
.name-list .name-tag .delete-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
font-size: 11px;
line-height: 1;
color: #888;
background: transparent;
border: none;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
padding: 0;
font-weight: bold;
}
.name-list .name-tag .delete-btn:hover {
color: #ff4444;
background: rgba(255, 68, 68, 0.15);
}
.name-list .name-tag .delete-btn:active {
transform: scale(0.85);
}
.name-list.disabled .name-tag .delete-btn {
display: none;
}
/* ===== Mode Switch ===== */
.mode-switch {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 20px;
font-size: 14px;
}
.mode-switch label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
color: var(--text-secondary);
}
.mode-switch input[type="radio"] {
accent-color: var(--accent);
width: 16px;
height: 16px;
cursor: pointer;
}
/* ===== Roll Area ===== */
.roll-area {
text-align: center;
padding: 30px 20px;
margin: 10px 0 20px;
width: 100%;
}
.avatar-display {
width: 100px;
height: 100px;
border-radius: 50%;
background: var(--bg-card);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
transition: var(--transition);
border: 3px solid rgba(255,255,255,0.1);
}
.avatar-text {
font-size: 32px;
font-weight: 700;
color: #fff;
text-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.roll-name {
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
min-height: 36px;
transition: opacity 0.08s, transform 0.08s;
}
.roll-name.highlight {
color: var(--accent);
transform: scale(1.15);
text-shadow: 0 0 20px rgba(233, 69, 96, 0.5);
}
/* ===== Start Button ===== */
.btn-start {
padding: 14px 48px;
font-size: 18px;
font-weight: 700;
background: linear-gradient(135deg, var(--accent), #c0392b);
color: #fff;
border: none;
border-radius: 50px;
cursor: pointer;
transition: var(--transition);
box-shadow: 0 4px 15px rgba(233, 69, 96, 0.4);
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
}
.btn-start:hover {
background: linear-gradient(135deg, var(--accent-hover), var(--accent));
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(233, 69, 96, 0.5);
}
.btn-start:active {
transform: translateY(0);
}
.btn-start:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-start.pressing {
transform: scale(0.95);
box-shadow: 0 2px 10px rgba(233, 69, 96, 0.3);
}
/* ===== Result Area ===== */
.result-area {
text-align: center;
margin-top: 20px;
animation: fadeInUp 0.5s ease;
}
.result-area.hidden {
display: none;
}
.result-text {
font-size: 20px;
font-weight: 600;
color: var(--success);
}
.result-text span {
color: var(--accent);
font-size: 24px;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ===== Fireworks Canvas ===== */
#fireworksCanvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
}
/* ===== History Panel ===== */
#historyPanel {
margin-top: 20px;
}
#historyTable {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
#historyTable thead th {
padding: 10px 12px;
text-align: left;
color: var(--text-secondary);
font-weight: 600;
border-bottom: 2px solid rgba(255,255,255,0.1);
}
#historyTable tbody td {
padding: 10px 12px;
border-bottom: 1px solid rgba(255,255,255,0.05);
color: var(--text-primary);
}
#historyTable tbody tr:hover {
background: rgba(255,255,255,0.03);
}
#historyBody:empty::after {
content: '暂无记录';
display: block;
text-align: center;
color: var(--text-secondary);
padding: 20px;
font-size: 14px;
}
/* ===== Backdoor Overlay ===== */
.backdoor-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.backdoor-overlay.hidden {
display: none;
}
.backdoor-modal {
background: var(--bg-secondary);
border-radius: var(--border-radius);
padding: 28px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
box-shadow: var(--shadow);
}
.backdoor-modal h2 {
font-size: 20px;
margin-bottom: 20px;
text-align: center;
}
.backdoor-section {
margin-bottom: 20px;
}
.backdoor-section h3 {
font-size: 15px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.backdoor-section textarea {
min-height: 60px;
margin-bottom: 8px;
}
.checklist {
max-height: 150px;
overflow-y: auto;
margin-bottom: 8px;
padding: 8px;
background: var(--bg-card);
border-radius: 8px;
}
.checklist label {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
font-size: 13px;
color: var(--text-primary);
cursor: pointer;
}
.checklist input[type="checkbox"] {
accent-color: var(--accent);
width: 16px;
height: 16px;
cursor: pointer;
}
.backdoor-section button {
background: var(--accent);
color: #fff;
border: none;
padding: 8px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.backdoor-section button:hover {
background: var(--accent-hover);
}
.btn-close {
display: block;
width: 100%;
margin-top: 16px;
padding: 10px;
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.btn-close:hover {
background: rgba(255,255,255,0.08);
}
/* ===== Probability Settings ===== */
.probability-hint {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 12px;
line-height: 1.5;
}
.probability-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 8px;
margin-bottom: 8px;
}
.probability-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.probability-row:last-child {
border-bottom: none;
}
.probability-row .person-name {
flex: 0 0 80px;
font-size: 14px;
color: var(--text-primary);
}
.probability-row .weight-input {
width: 56px;
padding: 4px 6px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
background: var(--bg-secondary);
color: var(--text-primary);
text-align: center;
font-size: 13px;
font-family: inherit;
outline: none;
transition: var(--transition);
}
.probability-row .weight-input:focus {
border-color: var(--accent);
}
.weight-quick-btns {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.weight-quick-btns button {
padding: 2px 8px;
font-size: 12px;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 4px;
background: var(--bg-card);
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition);
font-family: inherit;
}
.weight-quick-btns button:hover {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.weight-quick-btns button.active {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.probability-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.probability-actions button {
flex: 1;
background: var(--accent);
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
font-family: inherit;
}
.probability-actions button:hover {
background: var(--accent-hover);
}
.probability-actions button.secondary {
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid rgba(255,255,255,0.12);
}
.probability-actions button.secondary:hover {
background: rgba(255,255,255,0.08);
}
/* ===== Winner Popup ===== */
.winner-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.75);
z-index: 10001;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
.winner-overlay.hidden {
display: none;
}
.winner-modal {
background: var(--bg-secondary);
border-radius: 20px;
padding: 40px 50px;
text-align: center;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
animation: winnerPopIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
max-width: 90vw;
}
@keyframes winnerPopIn {
0% {
opacity: 0;
transform: scale(0.5) translateY(30px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.winner-modal .winner-close {
position: absolute;
top: 12px;
right: 16px;
font-size: 24px;
color: var(--text-secondary);
background: none;
border: none;
cursor: pointer;
transition: var(--transition);
padding: 4px 8px;
border-radius: 50%;
font-family: inherit;
}
.winner-modal .winner-close:hover {
color: var(--accent);
background: rgba(255,255,255,0.08);
}
.winner-modal .winner-congrats {
font-size: 28px;
font-weight: 700;
color: #FFD700;
margin-bottom: 20px;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.4);
}
.winner-modal .winner-avatar-large {
width: 120px;
height: 120px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
font-size: 40px;
font-weight: 700;
color: #fff;
text-shadow: 0 2px 6px rgba(0,0,0,0.3);
border: 4px solid rgba(255,255,255,0.2);
box-shadow: 0 0 30px rgba(255, 215, 0, 0.3);
}
.winner-modal .winner-name-large {
font-size: 32px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.winner-modal .winner-label {
font-size: 14px;
color: var(--text-secondary);
}
/* ===== Horizontal Scroll (Slot Machine) ===== */
.scroll-container {
width: 100%;
overflow: hidden;
position: relative;
height: 40px;
margin: 8px 0;
}
.scroll-container::before,
.scroll-container::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
width: 40px;
z-index: 2;
pointer-events: none;
}
.scroll-container::before {
left: 0;
background: linear-gradient(to right, var(--bg-secondary), transparent);
}
.scroll-container::after {
right: 0;
background: linear-gradient(to left, var(--bg-secondary), transparent);
}
.scroll-track {
display: flex;
align-items: center;
height: 100%;
will-change: transform;
}
.scroll-item {
flex-shrink: 0;
padding: 0 16px;
font-size: 22px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
opacity: 0.5;
transition: opacity 0.1s;
}
.scroll-item.active {
opacity: 1;
color: var(--accent);
}
/* ===== Scrollbar ===== */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.15);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.25);
}
/* ===== Utility ===== */
.hidden {
display: none !important;
}
</style>
</head>
<body>
<div id="app">
<!-- 顶部标题 -->
<header class="header">
<h1>🎯 在线点名抽奖</h1>
<span class="pool-count">当前名单: <b id="poolCount">0</b></span>
</header>
<!-- 主内容区 -->
<main class="main">
<!-- 左侧:名单管理 -->
<section id="namePanel" class="panel">
<h2>📋 名单管理</h2>
<textarea id="nameInput" placeholder="输入名字,用逗号、换行、分号等分隔..."></textarea>
<div class="btn-row">
<button id="btnAdd">添加名单</button>
<label class="file-btn">
📄 导入 TXT
<input type="file" id="fileTXT" accept=".txt" hidden>
</label>
<label class="file-btn">
📊 导入 CSV
<input type="file" id="fileCSV" accept=".csv,.tsv" hidden>
</label>
</div>
<div id="nameList" class="name-list"></div>
</section>
<!-- 右侧:抽奖区 -->
<section id="lotteryPanel" class="panel">
<h2>🎰 抽奖</h2>
<!-- 模式切换 -->
<div class="mode-switch">
<label><input type="radio" name="mode" value="single" checked> 单次模式(抽中移除)</label>
<label><input type="radio" name="mode" value="repeat"> 重复模式(抽中保留)</label>
</div>
<!-- 名字滚动展示区 -->
<div id="rollArea" class="roll-area">
<div id="avatarDisplay" class="avatar-display">
<div id="avatarText" class="avatar-text">?</div>
</div>
<div id="scrollContainer" class="scroll-container hidden">
<div id="scrollTrack" class="scroll-track"></div>
</div>
<div id="rollName" class="roll-name">等待开始</div>
</div>
<!-- 抽奖按钮 -->
<button id="btnStart" class="btn-start">🎲 开始抽奖</button>
<!-- 中奖结果 -->
<div id="resultArea" class="result-area hidden">
<div class="result-text">🎉 本次幸运儿是:<span id="winnerName"></span></div>
</div>
<!-- Canvas 烟花 -->
<canvas id="fireworksCanvas"></canvas>
</section>
</main>
<!-- 底部:历史记录 -->
<section id="historyPanel" class="panel">
<h2>📜 历史记录</h2>
<table id="historyTable">
<thead><tr><th>轮次</th><th>中奖人</th><th>时间</th></tr></thead>
<tbody id="historyBody"></tbody>
</table>
</section>
<!-- 后门面板(默认隐藏) -->
<div id="backdoorPanel" class="backdoor-overlay hidden">
<div class="backdoor-modal">
<h2>⚙️ 高级设置</h2>
<div class="backdoor-section">
<h3>必中名单</h3>
<textarea id="mustWinInput" placeholder="输入必中名字(逗号分隔)..."></textarea>
<div id="mustWinChecklist" class="checklist"></div>
<button id="btnConfirmMustWin">确认</button>
</div>
<div class="backdoor-section">
<h3>🎲 概率设置</h3>
<p class="probability-hint">权重值 0-100数值越大被抽中的概率越高。默认权重为 1设为 0 则不参与抽奖。</p>
<div id="probabilityList" class="probability-list"></div>
<div class="probability-actions">
<button id="btnResetAllWeights" class="secondary">重置全部为 1</button>
<button id="btnConfirmProbabilities">确认</button>
</div>
</div>
<div class="backdoor-section">
<h3>排除名单</h3>
<textarea id="excludeInput" placeholder="输入排除名字(逗号分隔)..."></textarea>
<div id="excludeChecklist" class="checklist"></div>
<button id="btnConfirmExclude">确认</button>
</div>
<button id="btnCloseBackdoor" class="btn-close">关闭</button>
</div>
</div>
<!-- 中奖弹窗(默认隐藏) -->
<div id="winnerOverlay" class="winner-overlay hidden">
<div class="winner-modal">
<button id="winnerClose" class="winner-close"></button>
<div class="winner-congrats">🎉 恭喜中奖!🎉</div>
<div id="winnerAvatarLarge" class="winner-avatar-large">?</div>
<div id="winnerNameLarge" class="winner-name-large"></div>
<div class="winner-label">本次幸运儿</div>
</div>
</div>
</div>
<script>
(function() {
'use strict';
// ===== 共享状态 =====
const state = {
pool: [], // 当前抽奖池:[{ id, name, color, avatar, weight }]
history: [], // 历史记录(内存,页面关闭清空)
totalImported: 0, // 累计导入总人数
// 抽奖引擎
mode: 'single', // 'single' | 'repeat'
isDrawing: false, // 是否正在抽奖动画中
winner: null, // 当前中奖人
// 后门
backdoor: {
enabled: false,
mustWinList: [], // 必中名单(名字字符串数组)
excludeList: [], // 排除名单(名字字符串数组)
probabilities: {}, // { name: weight },权重值 0-100
},
// 动画
round: 0, // 当前轮次
};
// ===== 工具函数 =====
const Utils = {
/** 生成类 UUID 的唯一标识 */
generateId: function() {
return 'xxxx-xxxx-xxxx'.replace(/x/g, function() {
return ((Math.random() * 16) | 0).toString(16);
});
},
/** 名字哈希颜色HSL 保证舒适度) */
hashColor: function(name) {
let hash = 0;
for (var i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
var hue = Math.abs(hash) % 360;
return 'hsl(' + hue + ', 65%, 55%)';
},
/** 取名字后两个字(单字名取最后一个字) */
getAvatarText: function(name) {
var chars = name.trim();
if (chars.length <= 2) return chars;
return chars.slice(-2);
},
/** 格式化时间 */
formatTime: function(date) {
var d = date || new Date();
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
return d.getFullYear() + '-' +
pad(d.getMonth() + 1) + '-' +
pad(d.getDate()) + ' ' +
pad(d.getHours()) + ':' +
pad(d.getMinutes()) + ':' +
pad(d.getSeconds());
},
/** 解析名字文本:按分隔符拆分,去空、去重 */
parseNames: function(text) {
// 支持逗号、分号、顿号、换行、制表符等分隔
var names = text.split(/[,\s;,,、\n\r\t]+/);
// 去空、去前后空格
names = names.map(function(n) { return n.trim(); }).filter(function(n) { return n.length > 0; });
// 去重
var seen = {};
var unique = [];
for (var i = 0; i < names.length; i++) {
var key = names[i];
if (!seen[key]) {
seen[key] = true;
unique.push(names[i]);
}
}
return unique;
}
};
// ===== 名单管理模块 =====
const NameManager = {
/** 解析文本输入 */
parseInput: function(text) {
return Utils.parseNames(text);
},
/** 导入 TXT 文件 */
importTXT: function(file) {
var reader = new FileReader();
var self = this;
reader.onload = function(e) {
var text = e.target.result;
var names = self.parseInput(text);
self.addToPool(names);
};
reader.readAsText(file, 'UTF-8');
},
/** 导入 CSV 文件 */
importCSV: function(file) {
var reader = new FileReader();
var self = this;
reader.onload = function(e) {
var text = e.target.result;
// CSV 按行或逗号分隔parseNames 统一处理
var names = self.parseInput(text);
self.addToPool(names);
};
reader.readAsText(file, 'UTF-8');
},
/** 将名字列表转为人员对象,加入抽奖池,去重 */
addToPool: function(names) {
var existingNames = {};
for (var i = 0; i < state.pool.length; i++) {
existingNames[state.pool[i].name] = true;
}
var added = 0;
for (var j = 0; j < names.length; j++) {
var name = names[j];
if (!existingNames[name]) {
existingNames[name] = true;
state.pool.push(this.generatePerson(name));
added++;
}
}
state.totalImported += added;
this.updatePoolCount();
this.renderNameList();
},
/** 为单个名字生成人员对象 */
generatePerson: function(name) {
return {
id: Utils.generateId(),
name: name,
color: Utils.hashColor(name),
avatar: Utils.getAvatarText(name),
weight: 1 // v1.1.0: 默认权重 1
};
},
/** 获取当前抽奖池人数 */
getPoolCount: function() {
return state.pool.length;
},
/** 更新池人数显示 */
updatePoolCount: function() {
var el = document.getElementById('poolCount');
if (el) el.textContent = state.pool.length;
},
/** 渲染已导入名单列表 */
renderNameList: function() {
var container = document.getElementById('nameList');
if (!container) return;
container.innerHTML = '';
// 抽奖中禁用删除
if (state.isDrawing) {
container.classList.add('disabled');
} else {
container.classList.remove('disabled');
}
var fragment = document.createDocumentFragment();
for (var i = 0; i < state.pool.length; i++) {
var person = state.pool[i];
var span = document.createElement('span');
span.className = 'name-tag';
var nameText = document.createTextNode(person.name);
span.appendChild(nameText);
// 删除按钮
var delBtn = document.createElement('button');
delBtn.className = 'delete-btn';
delBtn.textContent = '×';
delBtn.title = '删除 ' + person.name;
delBtn.setAttribute('data-name', person.name);
if (state.isDrawing) {
delBtn.disabled = true;
}
span.appendChild(delBtn);
fragment.appendChild(span);
}
container.appendChild(fragment);
},
/** 按名字删除人员v1.1.0 新增) */
removeByName: function(name) {
if (state.isDrawing) return; // 抽奖中禁止删除
// 从池中移除
var idx = -1;
for (var i = 0; i < state.pool.length; i++) {
if (state.pool[i].name === name) {
idx = i;
break;
}
}
if (idx === -1) return;
state.pool.splice(idx, 1);
// 同步清理必中名单
var mwIdx = state.backdoor.mustWinList.indexOf(name);
if (mwIdx !== -1) state.backdoor.mustWinList.splice(mwIdx, 1);
// 同步清理排除名单
var exIdx = state.backdoor.excludeList.indexOf(name);
if (exIdx !== -1) state.backdoor.excludeList.splice(exIdx, 1);
// 同步清理概率配置
delete state.backdoor.probabilities[name];
this.updatePoolCount();
this.renderNameList();
}
};
// ===== 抽奖引擎模块 =====
const LotteryEngine = {
/** 根据模式和后门设置从池中选出中奖人v1.1.0 加权随机) */
pickOne: function() {
// 1. 候选池 = state.pool 副本
var candidates = state.pool.slice();
// 2. 过滤排除名单 + 权重为 0 的人
var self = this;
candidates = this.applyExclude(candidates);
candidates = candidates.filter(function(person) {
var w = self.getWeight(person.name);
return w > 0;
});
// 3. 若必中名单非空,取交集(最高优先级,等概率)
if (state.backdoor.mustWinList.length > 0) {
var intersection = [];
for (var i = 0; i < candidates.length; i++) {
for (var j = 0; j < state.backdoor.mustWinList.length; j++) {
if (candidates[i].name === state.backdoor.mustWinList[j]) {
intersection.push(candidates[i]);
break;
}
}
}
if (intersection.length > 0) {
return intersection[Math.floor(Math.random() * intersection.length)];
}
}
// 4. 加权随机(无必中名单或必中名单不匹配时)
if (candidates.length === 0) return null;
var totalWeight = 0;
for (var k = 0; k < candidates.length; k++) {
totalWeight += self.getWeight(candidates[k].name);
}
if (totalWeight === 0) {
// 兜底:所有人权重都为 0 时等概率
return candidates[Math.floor(Math.random() * candidates.length)];
}
// 加权随机选择
var r = Math.random() * totalWeight;
var cumulative = 0;
for (var m = 0; m < candidates.length; m++) {
cumulative += self.getWeight(candidates[m].name);
if (r < cumulative) {
return candidates[m];
}
}
// 兜底
return candidates[candidates.length - 1];
},
/** 获取某人权重v1.1.0 */
getWeight: function(name) {
// 优先从 probabilities 读取,其次 person.weight默认 1
if (state.backdoor.probabilities.hasOwnProperty(name)) {
return state.backdoor.probabilities[name];
}
for (var i = 0; i < state.pool.length; i++) {
if (state.pool[i].name === name) {
return state.pool[i].weight || 1;
}
}
return 1;
},
/** 过滤掉排除名单中的人 */
applyExclude: function(pool) {
if (state.backdoor.excludeList.length === 0) return pool;
return pool.filter(function(person) {
for (var i = 0; i < state.backdoor.excludeList.length; i++) {
if (person.name === state.backdoor.excludeList[i]) return false;
}
return true;
});
},
/** 单次模式:从池中移除中奖人 */
removeWinner: function(id) {
for (var i = 0; i < state.pool.length; i++) {
if (state.pool[i].id === id) {
state.pool.splice(i, 1);
break;
}
}
NameManager.updatePoolCount();
NameManager.renderNameList();
},
/** 重复模式:保留在池中(无需操作) */
keepWinner: function(id) {
// 不做任何事
}
};
// ===== 动画模块 =====
const AnimationEngine = {
rAFId: null,
lastTickTime: 0,
/** 名字滚动动画:水平老虎机效果 → 减速 → 停止在 winner */
startRollAnimation: function(winnerPerson, callback) {
var self = this;
var startTime = performance.now();
var duration = 3500; // 总时长 3.5 秒
var rollNameEl = document.getElementById('rollName');
var avatarTextEl = document.getElementById('avatarText');
var avatarDisplayEl = document.getElementById('avatarDisplay');
var scrollContainerEl = document.getElementById('scrollContainer');
var scrollTrackEl = document.getElementById('scrollTrack');
// 移除 highlight
rollNameEl.classList.remove('highlight');
// 构建滚动轨道:重复多份名单以产生连续滚动效果
var allNames = [];
for (var i = 0; i < 8; i++) {
for (var j = 0; j < state.pool.length; j++) {
allNames.push(state.pool[j]);
}
}
// 清空并填充轨道
scrollTrackEl.innerHTML = '';
scrollTrackEl.style.transform = 'translateX(0px)';
for (var k = 0; k < allNames.length; k++) {
var item = document.createElement('span');
item.className = 'scroll-item';
item.textContent = allNames[k].name;
scrollTrackEl.appendChild(item);
}
// 显示滚动容器,隐藏静态名字
scrollContainerEl.classList.remove('hidden');
rollNameEl.textContent = '';
// 测量轨道总宽度
var totalWidth = scrollTrackEl.scrollWidth;
var containerWidth = scrollContainerEl.clientWidth;
// 缓动函数
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
var lastItemIndex = -1;
function tick(now) {
var elapsed = now - startTime;
var progress = Math.min(elapsed / duration, 1);
var easedProgress = easeOutCubic(progress);
// 总位移 = 轨道宽度 - 容器宽度(滚动到末尾附近)
var maxScroll = totalWidth - containerWidth;
// 最终停在 winner 附近
var targetScroll = maxScroll * 0.85;
var currentScroll = targetScroll * easedProgress;
scrollTrackEl.style.transform = 'translateX(' + (-currentScroll) + 'px)';
// 高亮当前可见的中间项
var items = scrollTrackEl.querySelectorAll('.scroll-item');
var centerOffset = containerWidth / 2 + currentScroll;
var itemWidth = 100; // 估算每个名字宽度
var currentIndex = Math.floor(centerOffset / itemWidth) % items.length;
if (currentIndex !== lastItemIndex && currentIndex >= 0 && currentIndex < items.length) {
// 清除旧高亮
for (var m = 0; m < items.length; m++) {
items[m].classList.remove('active');
}
items[currentIndex].classList.add('active');
// 更新头像和名字
var personIdx = currentIndex % state.pool.length;
var currentPerson = state.pool[personIdx];
if (currentPerson) {
rollNameEl.textContent = currentPerson.name;
avatarTextEl.textContent = currentPerson.avatar;
avatarDisplayEl.style.background = currentPerson.color;
}
lastItemIndex = currentIndex;
}
if (progress < 1) {
self.rAFId = requestAnimationFrame(tick);
} else {
// 最终停在 winner
scrollContainerEl.classList.add('hidden');
rollNameEl.textContent = winnerPerson.name;
avatarTextEl.textContent = winnerPerson.avatar;
avatarDisplayEl.style.background = winnerPerson.color;
rollNameEl.classList.add('highlight');
callback(winnerPerson);
}
}
this.rAFId = requestAnimationFrame(tick);
},
/** 停止滚动动画 */
stopRollAnimation: function() {
if (this.rAFId) {
cancelAnimationFrame(this.rAFId);
this.rAFId = null;
}
},
/** 缓动函数easeOutCubic */
easeOutCubic: function(t) {
return 1 - Math.pow(1 - t, 3);
},
/** 获取滚动候选:最后 10% 逐渐偏向 winner */
getRollCandidate: function(winner, progress) {
if (state.pool.length === 0) return null;
if (progress > 0.9 && Math.random() < 0.3) {
return winner;
}
return state.pool[Math.floor(Math.random() * state.pool.length)];
},
/** 显示中奖弹窗v1.1.0 新增) */
showWinnerPopup: function(winnerPerson) {
var overlay = document.getElementById('winnerOverlay');
var avatarLarge = document.getElementById('winnerAvatarLarge');
var nameLarge = document.getElementById('winnerNameLarge');
avatarLarge.textContent = winnerPerson.avatar;
avatarLarge.style.background = winnerPerson.color;
nameLarge.textContent = winnerPerson.name;
overlay.classList.remove('hidden');
},
/** 关闭中奖弹窗 */
hideWinnerPopup: function() {
var overlay = document.getElementById('winnerOverlay');
overlay.classList.add('hidden');
},
/** Canvas 烟花粒子效果v1.1.0 改进:弹窗周围爆炸 + 5秒 + 更密集) */
fireFireworks: function() {
var canvas = document.getElementById('fireworksCanvas');
var ctx = canvas.getContext('2d');
var running = true;
var fireworks = [];
var spawnTimer = 0;
var endTime = performance.now() + 5000; // 持续 5 秒
// 获取弹窗位置作为爆炸中心
var popupEl = document.getElementById('winnerOverlay');
var popupRect = popupEl.getBoundingClientRect();
var centerX = popupRect.left + popupRect.width / 2;
var centerY = popupRect.top + popupRect.height / 2;
var spreadRadius = Math.max(popupRect.width, popupRect.height) * 0.8;
// 调整 canvas 尺寸
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
// 粒子类
function Particle(x, y, color) {
var angle = Math.random() * Math.PI * 2;
var speed = 2 + Math.random() * 8;
this.x = x;
this.y = y;
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
this.color = color;
this.alpha = 1;
this.size = 2 + Math.random() * 4;
this.life = 80 + Math.random() * 50;
this.maxLife = this.life;
this.gravity = 0.04;
this.friction = 0.98;
}
Particle.prototype.update = function() {
this.vx *= this.friction;
this.vy *= this.friction;
this.vy += this.gravity;
this.x += this.vx;
this.y += this.vy;
this.alpha = this.life / this.maxLife;
this.life--;
};
Particle.prototype.draw = function(ctx) {
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
};
// 烟花类v1.1.0:无上升阶段,直接爆炸)
function Firework(targetX, targetY, colors) {
this.targetX = targetX;
this.targetY = targetY;
this.colors = colors;
this.particles = [];
this.state = 'exploding';
// 初始爆炸生成更多粒子
for (var i = 0; i < 100 + Math.floor(Math.random() * 40); i++) {
var color = colors[Math.floor(Math.random() * colors.length)];
this.particles.push(new Particle(targetX, targetY, color));
}
}
Firework.prototype.update = function() {
if (this.state === 'exploding') {
for (var j = 0; j < this.particles.length; j++) {
this.particles[j].update();
}
this.particles = this.particles.filter(function(p) { return p.life > 0; });
if (this.particles.length === 0) {
this.state = 'dead';
}
}
};
Firework.prototype.draw = function(ctx) {
if (this.state === 'exploding') {
for (var i = 0; i < this.particles.length; i++) {
this.particles[i].draw(ctx);
}
}
};
// 随机颜色调色板
function randomColorPalette() {
var palettes = [
['#FF6B6B', '#FFA07A', '#FFD700'],
['#4ECDC4', '#45B7D1', '#96CEB4'],
['#DDA0DD', '#FF69B4', '#FFB6C1'],
['#F97F51', '#E056FD', '#686DE0'],
['#F9FBE7', '#A7C957', '#6CB4B0'],
['#FF6B6B', '#4ECDC4', '#FFD700'],
['#FF69B4', '#45B7D1', '#FFA07A']
];
return palettes[Math.floor(Math.random() * palettes.length)];
}
// 主循环
function loop() {
if (!running) return;
var now = performance.now();
if (now > endTime) {
running = false;
ctx.clearRect(0, 0, canvas.width, canvas.height);
return;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 每 150-400ms 生成一朵烟花
spawnTimer++;
if (spawnTimer > 3 + Math.random() * 5) {
// 在弹窗周围随机位置生成爆炸点
var angle = Math.random() * Math.PI * 2;
var radius = spreadRadius * (0.5 + Math.random() * 1.2);
var x = centerX + Math.cos(angle) * radius;
var y = centerY + Math.sin(angle) * radius;
// 限制在屏幕范围内
x = Math.max(50, Math.min(canvas.width - 50, x));
y = Math.max(50, Math.min(canvas.height - 50, y));
var colors = randomColorPalette();
fireworks.push(new Firework(x, y, colors));
spawnTimer = 0;
}
// 更新和绘制
fireworks = fireworks.filter(function(fw) { return fw.state !== 'dead'; });
for (var i = 0; i < fireworks.length; i++) {
fireworks[i].update();
fireworks[i].draw(ctx);
}
requestAnimationFrame(loop);
}
loop();
}
};
// ===== 历史记录模块 =====
const HistoryModule = {
/** 追加一条记录到 state.history */
addRecord: function(winner) {
state.round++;
state.history.push({
round: state.round,
winner: winner.name,
time: Utils.formatTime()
});
this.render();
},
/** 重新渲染底部历史记录列表 */
render: function() {
var tbody = document.getElementById('historyBody');
if (!tbody) return;
tbody.innerHTML = '';
var fragment = document.createDocumentFragment();
// 从最新到最旧显示
for (var i = state.history.length - 1; i >= 0; i--) {
var record = state.history[i];
var tr = document.createElement('tr');
var tdRound = document.createElement('td');
tdRound.textContent = record.round;
tr.appendChild(tdRound);
var tdWinner = document.createElement('td');
tdWinner.textContent = record.winner;
tr.appendChild(tdWinner);
var tdTime = document.createElement('td');
tdTime.textContent = record.time;
tr.appendChild(tdTime);
fragment.appendChild(tr);
}
tbody.appendChild(fragment);
},
/** 清空历史记录 */
clear: function() {
state.history = [];
state.round = 0;
this.render();
}
};
// ===== 后门模块 =====
const Backdoor = {
/** 打开后门面板 */
open: function() {
var panel = document.getElementById('backdoorPanel');
panel.classList.remove('hidden');
this.renderChecklists();
},
/** 关闭后门面板 */
close: function() {
var panel = document.getElementById('backdoorPanel');
panel.classList.add('hidden');
},
/** 渲染复选框列表(从当前池中勾选) */
renderChecklists: function() {
var mustWinChecklist = document.getElementById('mustWinChecklist');
var excludeChecklist = document.getElementById('excludeChecklist');
// 必中名单复选框
mustWinChecklist.innerHTML = '';
if (state.pool.length > 0) {
for (var i = 0; i < state.pool.length; i++) {
var person = state.pool[i];
var label = document.createElement('label');
var checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = person.name;
// 如果已在必中名单中,勾选
if (state.backdoor.mustWinList.indexOf(person.name) !== -1) {
checkbox.checked = true;
}
label.appendChild(checkbox);
label.appendChild(document.createTextNode(person.name));
mustWinChecklist.appendChild(label);
}
} else {
mustWinChecklist.innerHTML = '<span style="color:var(--text-secondary);font-size:12px;">抽奖池为空</span>';
}
// 排除名单复选框
excludeChecklist.innerHTML = '';
if (state.pool.length > 0) {
for (var j = 0; j < state.pool.length; j++) {
var p = state.pool[j];
var lbl = document.createElement('label');
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.value = p.name;
if (state.backdoor.excludeList.indexOf(p.name) !== -1) {
cb.checked = true;
}
lbl.appendChild(cb);
lbl.appendChild(document.createTextNode(p.name));
excludeChecklist.appendChild(lbl);
}
} else {
excludeChecklist.innerHTML = '<span style="color:var(--text-secondary);font-size:12px;">抽奖池为空</span>';
}
// v1.1.0: 渲染概率列表
this.renderProbabilityList();
},
/** 渲染概率设置列表v1.1.0 新增) */
renderProbabilityList: function() {
var list = document.getElementById('probabilityList');
if (!list) return;
list.innerHTML = '';
if (state.pool.length === 0) {
list.innerHTML = '<span style="color:var(--text-secondary);font-size:12px;">抽奖池为空</span>';
return;
}
for (var i = 0; i < state.pool.length; i++) {
var person = state.pool[i];
var row = document.createElement('div');
row.className = 'probability-row';
row.setAttribute('data-name', person.name);
// 获取当前权重
var currentWeight = 1;
if (state.backdoor.probabilities.hasOwnProperty(person.name)) {
currentWeight = state.backdoor.probabilities[person.name];
} else if (person.weight) {
currentWeight = person.weight;
}
// 人名
var nameSpan = document.createElement('span');
nameSpan.className = 'person-name';
nameSpan.textContent = person.name;
// 权重输入框
var weightInput = document.createElement('input');
weightInput.type = 'number';
weightInput.className = 'weight-input';
weightInput.min = 0;
weightInput.max = 100;
weightInput.value = currentWeight;
weightInput.setAttribute('data-name', person.name);
// 快捷按钮
var quickBtns = document.createElement('div');
quickBtns.className = 'weight-quick-btns';
var quickValues = [0, 1, 5, 10, 50, 100];
for (var q = 0; q < quickValues.length; q++) {
var w = quickValues[q];
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = w;
btn.setAttribute('data-weight', w);
if (w === currentWeight) {
btn.className = 'active';
}
(function(input, b, wVal) {
btn.addEventListener('click', function() {
input.value = wVal;
var siblings = quickBtns.querySelectorAll('button');
for (var s = 0; s < siblings.length; s++) {
siblings[s].className = '';
}
b.className = 'active';
});
})(weightInput, btn, w);
quickBtns.appendChild(btn);
}
// 输入框变化时更新快捷按钮高亮
weightInput.addEventListener('input', function() {
var v = parseInt(this.value) || 0;
var btns = quickBtns.querySelectorAll('button');
for (var b = 0; b < btns.length; b++) {
btns[b].className = (parseInt(btns[b].getAttribute('data-weight')) === v) ? 'active' : '';
}
});
row.appendChild(nameSpan);
row.appendChild(weightInput);
row.appendChild(quickBtns);
list.appendChild(row);
}
},
/** 保存概率设置v1.1.0 新增) */
saveProbabilities: function() {
var inputs = document.querySelectorAll('.weight-input');
var newProbs = {};
for (var i = 0; i < inputs.length; i++) {
var input = inputs[i];
var name = input.getAttribute('data-name');
var weight = Math.max(0, Math.min(100, parseInt(input.value) || 0));
if (weight !== 1) {
newProbs[name] = weight;
}
}
state.backdoor.probabilities = newProbs;
// 同步更新 pool 中每个人的 weight
for (var j = 0; j < state.pool.length; j++) {
var person = state.pool[j];
if (newProbs.hasOwnProperty(person.name)) {
person.weight = newProbs[person.name];
} else {
person.weight = 1;
}
}
},
/** 重置所有权重为 1v1.1.0 新增) */
resetAllWeights: function() {
state.backdoor.probabilities = {};
for (var i = 0; i < state.pool.length; i++) {
state.pool[i].weight = 1;
}
this.renderProbabilityList();
},
/** 确认必中名单 */
confirmMustWin: function() {
var names = [];
// 从文本输入解析
var input = document.getElementById('mustWinInput').value;
var parsed = Utils.parseNames(input);
for (var i = 0; i < parsed.length; i++) {
names.push(parsed[i]);
}
// 从复选框收集
var checkboxes = document.getElementById('mustWinChecklist').querySelectorAll('input[type="checkbox"]:checked');
for (var j = 0; j < checkboxes.length; j++) {
var val = checkboxes[j].value;
if (names.indexOf(val) === -1) {
names.push(val);
}
}
state.backdoor.mustWinList = names;
state.backdoor.enabled = names.length > 0 || state.backdoor.excludeList.length > 0;
// 清空输入
document.getElementById('mustWinInput').value = '';
},
/** 确认排除名单 */
confirmExclude: function() {
var names = [];
// 从文本输入解析
var input = document.getElementById('excludeInput').value;
var parsed = Utils.parseNames(input);
for (var i = 0; i < parsed.length; i++) {
names.push(parsed[i]);
}
// 从复选框收集
var checkboxes = document.getElementById('excludeChecklist').querySelectorAll('input[type="checkbox"]:checked');
for (var j = 0; j < checkboxes.length; j++) {
var val = checkboxes[j].value;
if (names.indexOf(val) === -1) {
names.push(val);
}
}
state.backdoor.excludeList = names;
state.backdoor.enabled = state.backdoor.mustWinList.length > 0 || names.length > 0;
// 清空输入
document.getElementById('excludeInput').value = '';
}
};
// ===== DOM 引用 =====
var DOM = {
nameInput: document.getElementById('nameInput'),
btnAdd: document.getElementById('btnAdd'),
fileTXT: document.getElementById('fileTXT'),
fileCSV: document.getElementById('fileCSV'),
nameList: document.getElementById('nameList'),
poolCount: document.getElementById('poolCount'),
btnStart: document.getElementById('btnStart'),
rollName: document.getElementById('rollName'),
avatarText: document.getElementById('avatarText'),
avatarDisplay: document.getElementById('avatarDisplay'),
resultArea: document.getElementById('resultArea'),
winnerName: document.getElementById('winnerName'),
backdoorPanel: document.getElementById('backdoorPanel'),
btnCloseBackdoor: document.getElementById('btnCloseBackdoor'),
btnConfirmMustWin: document.getElementById('btnConfirmMustWin'),
btnConfirmExclude: document.getElementById('btnConfirmExclude'),
fireworksCanvas: document.getElementById('fireworksCanvas'),
// v1.1.0 新增
btnConfirmProbabilities: document.getElementById('btnConfirmProbabilities'),
btnResetAllWeights: document.getElementById('btnResetAllWeights'),
winnerOverlay: document.getElementById('winnerOverlay'),
winnerClose: document.getElementById('winnerClose')
};
// ===== 事件绑定 =====
function initEvents() {
// 添加名单按钮
DOM.btnAdd.addEventListener('click', function() {
var text = DOM.nameInput.value;
if (!text.trim()) return;
var names = NameManager.parseInput(text);
if (names.length > 0) {
NameManager.addToPool(names);
DOM.nameInput.value = '';
}
});
// TXT 文件导入
DOM.fileTXT.addEventListener('change', function(e) {
var file = e.target.files[0];
if (!file) return;
NameManager.importTXT(file);
e.target.value = ''; // 重置 file input
});
// CSV 文件导入
DOM.fileCSV.addEventListener('change', function(e) {
var file = e.target.files[0];
if (!file) return;
NameManager.importCSV(file);
e.target.value = ''; // 重置 file input
});
// 模式切换
var modeRadios = document.querySelectorAll('input[name="mode"]');
for (var i = 0; i < modeRadios.length; i++) {
modeRadios[i].addEventListener('change', function() {
state.mode = this.value;
});
}
// 抽奖按钮 - 普通点击 / 长按后门
setupStartButton();
// 关闭后门
DOM.btnCloseBackdoor.addEventListener('click', function() {
Backdoor.close();
});
// 确认必中名单
DOM.btnConfirmMustWin.addEventListener('click', function() {
Backdoor.confirmMustWin();
});
// 确认排除名单
DOM.btnConfirmExclude.addEventListener('click', function() {
Backdoor.confirmExclude();
});
// 点击遮罩关闭后门
DOM.backdoorPanel.addEventListener('click', function(e) {
if (e.target === DOM.backdoorPanel) {
Backdoor.close();
}
});
// v1.1.0: 名单删除按钮(事件委托)
DOM.nameList.addEventListener('click', function(e) {
var btn = e.target.closest('.delete-btn');
if (btn && !btn.disabled) {
var name = btn.getAttribute('data-name');
if (name) {
NameManager.removeByName(name);
}
}
});
// v1.1.0: 概率设置确认
DOM.btnConfirmProbabilities.addEventListener('click', function() {
Backdoor.saveProbabilities();
});
// v1.1.0: 重置所有权重
DOM.btnResetAllWeights.addEventListener('click', function() {
Backdoor.resetAllWeights();
});
// v1.1.0: 关闭中奖弹窗
DOM.winnerClose.addEventListener('click', function() {
AnimationEngine.hideWinnerPopup();
});
// v1.1.0: 点击遮罩关闭中奖弹窗
DOM.winnerOverlay.addEventListener('click', function(e) {
if (e.target === DOM.winnerOverlay) {
AnimationEngine.hideWinnerPopup();
}
});
}
/** 设置抽奖按钮的点击/长按事件 */
function setupStartButton() {
var pressTimer = null;
var isLongPress = false;
function startPress(e) {
if (state.isDrawing) return;
// 阻止默认行为(防止移动端文本选择等)
if (e.cancelable) e.preventDefault();
isLongPress = false;
DOM.btnStart.classList.add('pressing');
pressTimer = setTimeout(function() {
isLongPress = true;
DOM.btnStart.classList.remove('pressing');
Backdoor.open();
pressTimer = null;
}, 3000);
}
function endPress(e) {
if (state.isDrawing) return;
if (pressTimer) {
clearTimeout(pressTimer);
pressTimer = null;
}
DOM.btnStart.classList.remove('pressing');
if (!isLongPress) {
startLottery();
}
isLongPress = false;
}
function leavePress() {
if (pressTimer) {
clearTimeout(pressTimer);
pressTimer = null;
}
DOM.btnStart.classList.remove('pressing');
}
// 鼠标事件
DOM.btnStart.addEventListener('mousedown', startPress);
DOM.btnStart.addEventListener('mouseup', endPress);
DOM.btnStart.addEventListener('mouseleave', leavePress);
// 触摸事件(移动端兼容)
DOM.btnStart.addEventListener('touchstart', startPress, { passive: false });
DOM.btnStart.addEventListener('touchend', function(e) {
e.preventDefault();
endPress(e);
});
DOM.btnStart.addEventListener('touchcancel', leavePress);
}
/** 开始抽奖 */
function startLottery() {
if (state.isDrawing) return;
// 检查抽奖池
var candidates = LotteryEngine.applyExclude(state.pool.slice());
if (candidates.length === 0) {
alert('抽奖池为空,请先添加名单!');
return;
}
state.isDrawing = true;
DOM.btnStart.disabled = true;
DOM.resultArea.classList.add('hidden');
NameManager.renderNameList(); // 禁用删除按钮
// 执行抽奖逻辑
var winner = LotteryEngine.pickOne();
if (!winner) {
state.isDrawing = false;
DOM.btnStart.disabled = false;
NameManager.renderNameList();
alert('无法选出中奖人,请检查名单或后门设置。');
return;
}
// 开始滚动动画
AnimationEngine.startRollAnimation(winner, function(finalWinner) {
// 动画结束
state.winner = finalWinner;
// 根据模式处理
if (state.mode === 'single') {
LotteryEngine.removeWinner(finalWinner.id);
} else {
LotteryEngine.keepWinner(finalWinner.id);
}
// 记录历史
HistoryModule.addRecord(finalWinner);
// 显示中奖弹窗
AnimationEngine.showWinnerPopup(finalWinner);
// 播放烟花(围绕弹窗)
AnimationEngine.fireFireworks();
// 恢复按钮
state.isDrawing = false;
DOM.btnStart.disabled = false;
NameManager.renderNameList(); // 恢复删除按钮
});
}
// ===== 初始化 =====
function init() {
initEvents();
HistoryModule.render();
NameManager.updatePoolCount();
}
// DOMContentLoaded 后启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
</body>
</html>