好友
阅读权限10
听众
最后登录1970-1-1
|
原生JS+CSS的html抽奖系统
作者:jokecat
版本号:V2025-12-A17
一、系统包含文件:
1、index.html(抽奖页面)
2、config.json(配置文件)
3、namelist.txt(抽奖参与者总名单)
4、本使用说明(使用说明.md)
二、index.html(抽奖页面)说明
在浏览器中打开本文件,点击左侧转圈的动画或下面的文字“请上传配置文件开始抽奖”,将弹出上传界面,请分别上传编辑好的config.json文件和namelist.txt文件。
如不报错,即可开始抽奖。
最先出现的,是config.json中级别数字最大的奖项。
用户可以点击绿色的【开始抽奖】按钮,也可以按空格键开始本轮抽奖。一旦触发开始抽奖,系统将自动抽取完本轮的所有名额并显示在右侧栏的获奖名单中。此时,【开始抽奖】右侧的蓝色【下个奖项】按钮变为可用,用户可以点击【下个奖项】按钮或者按回车进入下个奖项的抽奖。
全部奖项都结束后,蓝色【下个奖项】按钮会变成紫色【显示获奖名单】按钮,用户可以点击或者按回车键显示全屏的获奖名单。
三、config.json(配置文件)说明
1、可以使用记事本打开本文件,打开后的示例内容如下:
[
{
"级别": 0,
"奖项": "特等奖",
"名额": 1,
"必含": ["张三"],
"排除": ["李四"],
"可重复": false
},
{
"级别": 1,
"奖项": "一等奖",
"名额": 2,
"必含": [],
"排除": ["赵六","王五"],
"可重复": true
},
{
"级别": 2,
"奖项": "二等奖",
"名额": 3,
"必含": ["赵六","王五"],
"排除": [],
"可重复": false
},
{
"级别": 3,
"奖项": "三等奖",
"名额": 4,
"必含": [],
"排除": ["钱七"],
"可重复": false
}
]
2、每组数据包含了6个字段:级别、奖项、名额、必含、排除、可重复。
级别:必须项,纯数字,不加引号。本字段用于在抽奖时对奖项出现的顺序进行设定,级别数字最小的奖项到最后才抽取,级别数字最大的奖项第一个抽取。
奖项:必须项,即奖项的名称,加引号。
名额:必须项,纯数字,不加引号。用于设置该奖项可以有几人获得。
必含:可选项。用于安排本奖项的必中人选,如不设定,则清空[];如设定,则在[]中加入namelist.txt中的名字,多个名字间用,分隔。
排除:可选项。用于排除本奖项的可能获奖人选,如不设定,则清空[];如设定,则在[]中加入namelist.txt中的名字,多个名字间用,分隔。
可重复:必须项,值为true或false。这是一个全局参数,true代表同一人可以在不同奖项里多次中奖;false代表某人一旦在一个奖项里中奖,后面的奖项就不能再中奖了。
四、namelist.txt(抽奖参与者总名单)说明
纯文本文件,utf-8编码。
每行一个名字,不要有空行。
五、分发须知
1、本系统仅发布于52pojie网站。
2、任何人均可使用和分发本系统。
3、本作者水平有限,欢迎对本系统进行改进和美化。相关改进和美化,欢迎发布在52pojie网站的本帖之下。
4、他人对本系统进行任何修改,如造成对第三方的损害,本作者概不负责。
以下是js部分核心代码:
[HTML] 纯文本查看 复制代码 <script>
// 全局变量
let configData = [];
let nameList = [];
let currentAwardIndex = 0;
let currentWinners = [];
let allWinners = {};
let excludedNames = new Set(); // 全局排除名单(已中奖者)
let isDrawing = false;
let drawInterval = null;
let candidateList = [];
let allAwardsCompleted = false; // 标记所有奖项是否已完成
let sparks = []; // 存储火花元素
let sparkInterval = null; // 火花生成间隔
// DOM元素
const loadingFile = document.getElementById('loading-section');
const uploadOverlay = document.getElementById('upload-overlay');
const configFileInput = document.getElementById('config-file');
const nameFileInput = document.getElementById('name-file');
const uploadBtn = document.getElementById('upload-btn');
const cancelUploadBtn = document.getElementById('cancel-upload-btn');
const startBtn = document.getElementById('start-btn');
const nextBtn = document.getElementById('next-btn');
const fullscreenBtn = document.getElementById('fullscreen-btn');
const currentAwardName = document.getElementById('current-award-name');
const awardQuota = document.getElementById('award-quota');
const awardDrawn = document.getElementById('award-drawn');
const awardRemaining = document.getElementById('award-remaining');
const winnerDisplay = document.getElementById('winner-display');
const winnersContainer = document.getElementById('winners-container');
const statusBar = document.getElementById('status-bar');
const drawingArea = document.getElementById('drawing-area');
const loadingSection = document.getElementById('loading-section');
const configError = document.getElementById('config-error');
const errorDetails = document.getElementById('error-details');
const controlContent = document.getElementById('control-content');
const currentWinnerSection = document.getElementById('current-winner-section');
const currentWinner = document.getElementById('current-winner');
const fullscreenOverlay = document.getElementById('fullscreen-overlay');
const closeFullscreenBtn = document.getElementById('close-fullscreen');
const fullscreenContent = document.getElementById('fullscreen-content');
// 事件监听
startBtn.addEventListener('click', startDrawing);
nextBtn.addEventListener('click', nextAward);
fullscreenBtn.addEventListener('click', showFullscreenWinners);
closeFullscreenBtn.addEventListener('click', hideFullscreenWinners);
uploadBtn.addEventListener('click', handleFileUpload);
cancelUploadBtn.addEventListener('click', hideUploadOverlay);
loadingFile.addEventListener('click', showUploadOverlay); // 添加标题点击事件
// 键盘事件监听
document.addEventListener('keydown', handleKeyDown);
// 页面加载时初始化
window.onload = function() {
// 不再自动加载配置文件
loadingSection.style.display = 'flex';
controlContent.style.display = 'none';
configError.style.display = 'none';
statusBar.textContent = '请点击上传配置文件';
};
// 显示上传弹层
function showUploadOverlay() {
uploadOverlay.classList.add('active');
document.body.style.overflow = 'hidden';
}
// 隐藏上传弹层
function hideUploadOverlay() {
uploadOverlay.classList.remove('active');
document.body.style.overflow = 'auto';
// 重置文件输入
configFileInput.value = '';
nameFileInput.value = '';
}
// 处理文件上传
function handleFileUpload() {
const configFile = configFileInput.files[0];
const nameFile = nameFileInput.files[0];
if (!configFile || !nameFile) {
alert('请选择两个文件:config.json 和 namelist.txt');
return;
}
// 检查文件类型
if (configFile.type !== 'application/json' && !configFile.name.endsWith('.json')) {
alert('配置文件必须是JSON格式 (.json)');
return;
}
if (nameFile.type !== 'text/plain' && !nameFile.name.endsWith('.txt')) {
alert('名单文件必须是纯文本格式 (.txt)');
return;
}
// 读取配置文件
const reader1 = new FileReader();
reader1.onload = function(e) {
try {
configData = JSON.parse(e.target.result);
// 按照级别数字从大到小排序(后台处理使用)
configData.sort((a, b) => (b.级别 || 0) - (a.级别 || 0));
// 读取名单文件
const reader2 = new FileReader();
reader2.onload = function(e) {
try {
// 解析纯文本名单(每行一个名字)
nameList = e.target.result.split('\n')
.map(name => name.trim())
.filter(name => name.length > 0);
// 验证数据
if (!Array.isArray(configData) || configData.length === 0) {
throw new Error('配置文件格式错误或为空');
}
if (!Array.isArray(nameList) || nameList.length === 0) {
throw new Error('名单文件格式错误或为空');
}
// 隐藏上传弹层
hideUploadOverlay();
// 隐藏加载界面,显示控制面板
loadingSection.style.display = 'none';
controlContent.style.display = 'block';
configError.style.display = 'none';
// 初始化抽奖
initLottery();
} catch (error) {
showConfigError(error.message);
}
};
reader2.readAsText(nameFile);
} catch (error) {
showConfigError(`配置文件解析错误: ${error.message}`);
}
};
reader1.readAsText(configFile);
}
// 显示配置错误
function showConfigError(message) {
loadingSection.style.display = 'none';
configError.style.display = 'block';
errorDetails.textContent = message;
statusBar.textContent = '配置加载失败,使用示例数据';
// 使用示例数据
useSampleData();
}
// 处理键盘事件 - 添加对小键盘回车键的支持
function handleKeyDown(event) {
// 如果焦点在按钮上,则不处理键盘事件
if (event.target.tagName === 'BUTTON' || event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
return;
}
switch(event.code) {
case 'Space':
event.preventDefault(); // 防止空格键滚动页面
if (!startBtn.disabled) {
startDrawing();
}
break;
case 'Enter':
case 'NumpadEnter': // 添加小键盘回车键支持
event.preventDefault();
if (allAwardsCompleted) {
// 所有奖项已完成,按回车键显示获奖名单
showFullscreenWinners();
} else if (!nextBtn.disabled) {
nextAward();
}
break;
}
}
// 示例数据
function useSampleData() {
configData = [
{
"奖项": "特等奖",
"级别": 0,
"名额": 1,
"必含": ["张三"],
"排除": ["李四"],
"可重复": false
},
{
"奖项": "一等奖",
"级别": 1,
"名额": 3,
"必含": [],
"排除": ["王五"],
"可重复": true
},
{
"奖项": "二等奖",
"级别": 2,
"名额": 5,
"必含": ["赵六"],
"排除": [],
"可重复": false
},
{
"奖项": "三等奖",
"级别": 3,
"名额": 10,
"必含": [],
"排除": ["钱七"],
"可重复": true
}
];
// 按照级别数字从大到小排序(后台处理使用)
configData.sort((a, b) => (b.级别 || 0) - (a.级别 || 0));
// 模拟纯文本名单格式(每行一个名字)
nameList = [
"张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十",
"郑十一", "王十二", "刘十三", "陈十四", "杨十五", "黄十六", "赵十七",
"周十八", "吴十九", "郑二十", "孙二十一", "李二十二", "张二十三", "王二十四",
"刘二十五", "陈二十六", "杨二十七", "黄二十八", "赵二十九", "周五", "吴六"
];
statusBar.textContent = "使用示例数据进行演示";
controlContent.style.display = 'block';
initLottery();
}
// 初始化抽奖
function initLottery() {
currentAwardIndex = 0;
currentWinners = [];
allWinners = {};
excludedNames.clear();
allAwardsCompleted = false;
sparks = []; // 清空火花数组
// 初始化所有获奖者
configData.forEach(award => {
allWinners[award["奖项"]] = [];
});
loadAward(currentAwardIndex);
startBtn.disabled = false;
nextBtn.disabled = true; // 初始时下一个奖项按钮禁用
fullscreenBtn.style.display = 'none';
nextBtn.style.display = 'block';
}
// 加载奖项
function loadAward(index) {
if (index >= configData.length) {
allAwardsCompleted = true;
statusBar.textContent = '所有奖项已抽取完毕!';
currentAwardName.textContent = '抽奖结束';
awardQuota.textContent = '0';
awardDrawn.textContent = '0';
awardRemaining.textContent = '0';
startBtn.disabled = true;
nextBtn.disabled = true;
nextBtn.style.display = 'none';
fullscreenBtn.style.display = 'block';
return;
}
allAwardsCompleted = false;
const award = configData[index];
currentAwardName.textContent = award["奖项"];
awardQuota.textContent = award["名额"];
// 获取当前奖项已抽取的获奖者
currentWinners = [...allWinners[award["奖项"]]];
awardDrawn.textContent = currentWinners.length;
const remaining = award["名额"] - currentWinners.length;
awardRemaining.textContent = remaining;
// 不再显示"准备抽取",直接显示当前奖项状态
winnerDisplay.textContent = '正在抽奖中';
winnerDisplay.style.color = '#ffcc00';
winnerDisplay.style.fontSize = '2.2rem';
statusBar.textContent = `当前奖项:${award["奖项"]}\u2003名额:${award["名额"]}\u2003已中奖: ${currentWinners.length}\u2003剩余:${remaining}`;
// 设置按钮状态:当开始按钮有效时(有剩余名额),下一个按钮禁用
startBtn.disabled = remaining <= 0;
nextBtn.disabled = remaining > 0; // 当有剩余名额时,禁用下一个按钮
currentWinnerSection.style.display = 'none';
nextBtn.style.display = 'block';
fullscreenBtn.style.display = 'none';
}
// 创建火花效果
function createSpark() {
const spark = document.createElement('div');
spark.className = 'spark';
// 随机火花属性
const size = Math.random() * 4 + 2; // 2-6px
const hue = Math.floor(Math.random() * 60) + 10; // 10-70度色调(暖色)
const saturation = Math.floor(Math.random() * 30) + 70; // 70-100%
const lightness = Math.floor(Math.random() * 30) + 50; // 50-80%
const duration = Math.random() * 0.5 + 0.3; // 0.3-0.8秒
spark.style.width = `${size}px`;
spark.style.height = `${size}px`;
spark.style.backgroundColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
spark.style.boxShadow = `0 0 ${size * 2}px hsl(${hue}, ${saturation}%, ${lightness}%)`;
// 随机起始位置
const startX = Math.random() * drawingArea.offsetWidth;
const startY = Math.random() * drawingArea.offsetHeight;
spark.style.left = `${startX}px`;
spark.style.top = `${startY}px`;
// 使用CSS动画
spark.style.transition = `all ${duration}s ease-out`;
drawingArea.appendChild(spark);
sparks.push(spark);
// 触发动画
requestAnimationFrame(() => {
const endX = startX + (Math.random() - 0.5) * 100;
const endY = startY + (Math.random() - 0.5) * 100;
const endSize = size + Math.random() * 4;
spark.style.left = `${endX}px`;
spark.style.top = `${endY}px`;
spark.style.width = `${endSize}px`;
spark.style.height = `${endSize}px`;
spark.style.opacity = '1';
// 动画结束后移除元素
setTimeout(() => {
spark.style.opacity = '0';
setTimeout(() => {
if (spark.parentNode) {
spark.parentNode.removeChild(spark);
}
const index = sparks.indexOf(spark);
if (index > -1) {
sparks.splice(index, 1);
}
}, duration * 1000);
}, duration * 1000);
});
}
// 开始火花动画
function startSparkAnimation() {
// 先清除已有的火花
clearSparks();
// 设置火花生成间隔
sparkInterval = setInterval(() => {
// 根据抽奖状态控制火花密度
if (isDrawing) {
// 抽奖中时增加火花密度
for (let i = 0; i < 5; i++) {
createSpark();
}
} else {
// 等待状态下减少火花密度
createSpark();
}
}, 50); // 每50毫秒生成一次火花
}
// 停止火花动画
function stopSparkAnimation() {
if (sparkInterval) {
clearInterval(sparkInterval);
sparkInterval = null;
}
// 逐渐清除火花
setTimeout(() => {
clearSparks();
}, 1000);
}
// 清除火花效果
function clearSparks() {
sparks.forEach(spark => {
if (spark.parentNode) {
spark.parentNode.removeChild(spark);
}
});
sparks = [];
}
// 开始抽奖
async function startDrawing() {
if (isDrawing) return;
const award = configData[currentAwardIndex];
const remaining = award["名额"] - currentWinners.length;
if (remaining <= 0) {
statusBar.textContent = '当前奖项名额已满';
return;
}
isDrawing = true;
startBtn.disabled = true;
nextBtn.disabled = true; // 开始抽奖时禁用下一个按钮
// 开始火花动画
startSparkAnimation();
statusBar.textContent = `正在抽取 ${award["奖项"]} 的所有名额...`;
// 获取候选名单
candidateList = [...nameList];
// 排除必排除名单
if (award["排除"] && award["排除"].length > 0) {
candidateList = candidateList.filter(name =>
!award["排除"].includes(name)
);
}
// 排除已中奖者(全局去重)
if (!award["可重复"]) {
candidateList = candidateList.filter(name =>
!excludedNames.has(name)
);
}
// 排除当前奖项已中奖者
candidateList = candidateList.filter(name =>
!currentWinners.includes(name)
);
if (candidateList.length === 0) {
winnerDisplay.textContent = "无可用候选人";
winnerDisplay.style.color = "#ffcc00";
isDrawing = false;
startBtn.disabled = award["名额"] - currentWinners.length <= 0;
nextBtn.disabled = award["名额"] - currentWinners.length > 0;
stopSparkAnimation();
return;
}
// 创建候选名单副本,用于抽取动画
let animationCandidates = [...candidateList];
// 抽奖动画
let animationCounter = 0;
drawInterval = setInterval(() => {
if (animationCandidates.length === 0) {
animationCandidates = [...candidateList];
}
const randomIndex = Math.floor(Math.random() * animationCandidates.length);
if (currentWinnerSection.style.display !== 'block') {
currentWinnerSection.style.display = 'block';
}
currentWinner.textContent = `当前抽取: ${animationCandidates[randomIndex]}`;
currentWinner.style.transform = `scale(${1 + Math.sin(animationCounter * 0.2) * 0.1})`;
animationCounter++;
}, 50);
// 延迟开始正式抽取,让动画先显示一会儿
setTimeout(() => {
// 开始正式抽取所有名额
drawAllWinners(award, remaining);
}, 1000);
}
// 抽取所有获奖者
async function drawAllWinners(award, remainingCount) {
// 清理候选名单
let availableCandidates = [...candidateList];
// 先抽取必含名单
if (award["必含"] && award["必含"].length > 0) {
for (const mustInclude of award["必含"]) {
if (remainingCount <= 0) break;
if (!currentWinners.includes(mustInclude) &&
nameList.includes(mustInclude) &&
!excludedNames.has(mustInclude) &&
availableCandidates.includes(mustInclude)) {
// 确保必含名字在候选名单中
selectWinner(mustInclude, award, true);
remainingCount--;
// 从候选名单中移除
const index = availableCandidates.indexOf(mustInclude);
if (index !== -1) {
availableCandidates.splice(index, 1);
}
// 短暂延迟,让用户看到结果
await delay(500);
}
}
}
// 随机抽取剩余名额
for (let i = 0; i < remainingCount; i++) {
if (availableCandidates.length === 0) {
winnerDisplay.textContent = "无可用候选人";
winnerDisplay.style.color = "#ffcc00";
break;
}
const winnerIndex = Math.floor(Math.random() * availableCandidates.length);
const winner = availableCandidates[winnerIndex];
// 抽取当前获奖者
selectWinner(winner, award, true);
// 从候选名单中移除
availableCandidates.splice(winnerIndex, 1);
// 短暂延迟,让用户看到结果
await delay(500);
}
// 抽取完成
finishDrawing();
}
// 延迟函数
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 完成抽取
function finishDrawing() {
if (drawInterval) {
clearInterval(drawInterval);
drawInterval = null;
}
isDrawing = false;
currentWinnerSection.style.display = 'none';
const award = configData[currentAwardIndex];
const remaining = award["名额"] - currentWinners.length;
if (remaining <= 0) {
statusBar.textContent = `${award["奖项"]} 抽奖完成,共抽取 ${currentWinners.length} 人`;
winnerDisplay.textContent = `${award["奖项"]}抽奖完成`;
//winnerDisplay.style.color = '#ffcc00';
} else {
statusBar.textContent = `${award["奖项"]} 抽奖中断,已抽取 ${currentWinners.length} 人,剩余 ${remaining} 人`;
winnerDisplay.textContent = '抽奖中断';
winnerDisplay.style.color = '#ffcc00';
}
// 设置按钮状态:当开始按钮有效时(有剩余名额),下一个按钮禁用
startBtn.disabled = remaining <= 0;
nextBtn.disabled = remaining > 0; // 当有剩余名额时,禁用下一个按钮
// 停止火花动画
stopSparkAnimation();
}
// 选择获奖者
function selectWinner(winner, award, showAnimation = false) {
// 添加到获奖者列表
currentWinners.push(winner);
allWinners[award["奖项"]].push(winner);
// 如果是全局不可重复,则添加到排除名单
if (!award["可重复"]) {
excludedNames.add(winner);
}
// 更新状态
awardDrawn.textContent = currentWinners.length;
const remaining = award["名额"] - currentWinners.length;
awardRemaining.textContent = remaining;
// 更新获奖名单显示
updateWinnersList();
// 显示当前获奖者
if (showAnimation) {
currentWinner.textContent = `恭喜 ${winner} 获得 ${award["奖项"]}`;
currentWinner.style.color = '#ffcc00';
currentWinner.style.fontSize = '1.5rem';
}
}
// 下一个奖项
function nextAward() {
// 保存当前奖项的结果
const currentAward = configData[currentAwardIndex];
currentWinners = [...allWinners[currentAward["奖项"]]];
// 移动到下一个奖项
currentAwardIndex++;
loadAward(currentAwardIndex);
}
// 更新获奖名单显示
function updateWinnersList() {
winnersContainer.innerHTML = '';
for (const awardName in allWinners) {
const winners = allWinners[awardName];
if (winners.length === 0) continue;
const awardGroup = document.createElement('div');
awardGroup.className = 'award-group';
const awardHeader = document.createElement('div');
awardHeader.className = 'award-header';
awardHeader.textContent = `${awardName} (${winners.length}人)`;
const winnersDiv = document.createElement('div');
winnersDiv.className = 'winners';
winners.forEach(winner => {
const winnerTag = document.createElement('div');
winnerTag.className = 'winner-tag';
winnerTag.textContent = winner;
winnersDiv.appendChild(winnerTag);
});
awardGroup.appendChild(awardHeader);
awardGroup.appendChild(winnersDiv);
winnersContainer.appendChild(awardGroup);
}
if (winnersContainer.children.length === 0) {
winnersContainer.innerHTML = '<p>暂无获奖者</p>';
}
}
// 显示全屏获奖名单
function showFullscreenWinners() {
// 更新全屏内容
updateFullscreenContent();
// 显示全屏覆盖层
fullscreenOverlay.classList.add('active');
// 阻止页面滚动
document.body.style.overflow = 'hidden';
}
// 隐藏全屏获奖名单
function hideFullscreenWinners() {
// 隐藏全屏覆盖层
fullscreenOverlay.classList.remove('active');
// 恢复页面滚动
document.body.style.overflow = 'auto';
}
// 更新全屏获奖名单内容 - 按级别由小到大显示
function updateFullscreenContent() {
fullscreenContent.innerHTML = '';
// 收集所有有获奖者的奖项及其级别
const awardsWithWinners = [];
for (const awardName in allWinners) {
const winners = allWinners[awardName];
if (winners.length === 0) continue;
// 查找奖项配置以获取级别
const awardConfig = configData.find(a => a["奖项"] === awardName);
const level = awardConfig ? (awardConfig["级别"] || 0) : 0;
awardsWithWinners.push({
name: awardName,
winners: winners,
level: level
});
}
// 按级别由小到大排序(升序)
awardsWithWinners.sort((a, b) => a.level - b.level);
if (awardsWithWinners.length === 0) {
fullscreenContent.innerHTML = '<div class="no-winners"><p>暂无获奖者</p></div>';
return;
}
// 为每个奖项创建显示区块
awardsWithWinners.forEach(awardInfo => {
const awardGroup = document.createElement('div');
awardGroup.className = 'fullscreen-award-group';
const awardHeader = document.createElement('div');
awardHeader.className = 'fullscreen-award-header';
awardHeader.textContent = `${awardInfo.name}(${awardInfo.winners.length}人)`;
const winnersDiv = document.createElement('div');
winnersDiv.className = 'fullscreen-winners';
awardInfo.winners.forEach(winner => {
const winnerTag = document.createElement('div');
winnerTag.className = 'fullscreen-winner-tag';
winnerTag.textContent = winner;
winnersDiv.appendChild(winnerTag);
});
awardGroup.appendChild(awardHeader);
awardGroup.appendChild(winnersDiv);
fullscreenContent.appendChild(awardGroup);
});
}
</script> |
|