好友
阅读权限10
听众
最后登录1970-1-1
|
本帖最后由 zbmxqddn 于 2026-6-2 10:49 编辑
声明:此代码为AI辅助生成,公司自用
前台功能:
1、选择选项和人数:选择要抽的奖项和要中奖的人数
2、人数显示:参加抽奖的人数及剩余人数
3、重置:默认中了奖的不再重复中,可以重置让他们还能再参与中奖
4、刷新:后台更新奖项和人名后,重新读取
5、记录:显示中奖记录
6、设置:后台设置入口
7、开始抽奖:点一下开始,再点开奖
8、抽奖区:同步状态,中奖后前十名有奖杯和奖牌,有彩带
9、开奖记录:默认展示今日开奖记录,可清空和导出
后台功能:
1、多账号:全功能管理员密码hcj123,名单及奖项录入密码:md123,可通过不同密码控制权限
2、奖项管理:可添加、修改、删除奖项
3、名单管理:可批量导入,修改、删除
4、抽奖记录:查看所有记录
部署方法:
支持本地和http环境,解压三个文件放到同一目录,本地直接打开index.html即可
以下为index.html源码
[HTML] 纯文本查看 复制代码 <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>幸运抽奖</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎉</text></svg>" type="image/svg+xml">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
}
body {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
color: #fff;
padding: 20px;
min-height: 100vh;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 80%, rgba(43, 125, 239, 0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(114, 46, 209, 0.15) 0%, transparent 50%),
radial-gradient(circle at 50% 50%, rgba(19, 194, 194, 0.08) 0%, transparent 70%);
pointer-events: none;
}
.wrap {
max-width: 100%;
width: 100%;
margin: 0 auto;
display:flex;
flex-direction: column;
height: calc(100vh - 40px);
position: relative;
z-index: 1;
}
.top-box {
background: linear-gradient(135deg, rgba(35, 40, 54, 0.95) 0%, rgba(45, 50, 65, 0.9) 100%);
backdrop-filter: blur(24px);
padding: 24px;
border-radius: 20px;
margin-bottom: 20px;
flex-shrink: 0;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.top-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
flex-wrap: wrap;
}
.left-section {
display: flex;
align-items: flex-end;
gap: 24px;
flex-wrap: wrap;
}
.form-item {
display: flex;
flex-direction: column;
min-width: 180px;
}
.form-item.status-badge {
flex-direction: row;
min-width: 100px;
}
.item-label {
display: block;
color: #8b949e;
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.item-select {
width: 100%;
height: 48px;
background: linear-gradient(145deg, rgba(55, 60, 75, 0.9) 0%, rgba(40, 45, 55, 0.95) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
color: #f0f6fc;
padding: 0 16px;
font-size: 15px;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
cursor: pointer;
}
.item-select:focus {
border-color: #58a6ff;
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15), 0 0 20px rgba(88, 166, 255, 0.1);
}
.item-select:hover {
border-color: rgba(255, 255, 255, 0.15);
}
select.item-select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238b949e' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
}
select.item-select option {
background: #2d333b;
color: #f0f6fc;
padding: 8px 12px;
font-size: 14px;
}
.right-section {
display: flex;
gap: 10px;
align-items: center;
}
.status-badge {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, rgba(16, 185, 129, 0.15) 0%, rgba(5, 150, 105, 0.1) 100%);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 10px;
padding: 0 16px;
height: 48px;
gap: 6px;
}
.badge-value {
color: #10b981;
font-size: 20px;
font-weight: 700;
}
.badge-total {
color: #6b7280;
font-size: 14px;
font-weight: 500;
}
.badge-total-num {
color: #9ca3af;
font-size: 14px;
font-weight: 500;
}
.opt-btn {
height: 48px;
padding: 0 16px;
background: linear-gradient(145deg, rgba(55, 60, 75, 0.9) 0%, rgba(40, 45, 55, 0.95) 100%);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
color: #8b949e;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
white-space: nowrap;
}
.opt-btn:hover {
background: linear-gradient(145deg, rgba(88, 166, 255, 0.2) 0%, rgba(43, 125, 239, 0.15) 100%);
border-color: rgba(88, 166, 255, 0.4);
color: #f0f6fc;
box-shadow: 0 4px 15px rgba(88, 166, 255, 0.15);
}
.opt-btn.reset-btn {
background: linear-gradient(145deg, rgba(239, 68, 68, 0.15) 0%, rgba(220, 38, 38, 0.1) 100%);
border-color: rgba(239, 68, 68, 0.3);
color: #f87171;
}
.opt-btn.reset-btn:hover {
background: linear-gradient(145deg, rgba(239, 68, 68, 0.25) 0%, rgba(220, 38, 38, 0.2) 100%);
border-color: rgba(239, 68, 68, 0.5);
color: #fca5a5;
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.2);
}
.btn-group {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 20px;
}
.btn {
padding: 14px 45px;
border: 0;
border-radius: 12px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
outline: none;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.btn:hover::before {
left: 100%;
}
.btn-blue {
background: linear-gradient(135deg, #2b7def 0%, #1d4ed8 100%);
color: #fff;
box-shadow: 0 4px 15px rgba(43, 125, 239, 0.4);
}
.btn-blue:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(43, 125, 239, 0.5);
}
.btn-gray {
background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
color: #fff;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.btn-gray:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
}
.tip-text {
text-align: center;
color: #999;
margin-top: 12px;
font-size: 16px;
}
.win-box {
background: rgba(35, 40, 54, 0.85);
backdrop-filter: blur(20px);
padding: 24px;
border-radius: 16px;
margin-bottom: 20px;
text-align: center;
flex: 1;
overflow-y: auto;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.win-title {
font-size: 36px;
color: #fff;
margin-bottom: 12px;
text-align: center;
background: linear-gradient(135deg, #ffd700 0%, #ff9500 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 30px rgba(255, 215, 0, 0.3);
}
.win-subtitle {
color: #9ca3af;
font-size: 18px;
margin-bottom: 30px;
text-align: center;
}
.win-list {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
align-content: flex-start;
width: 100%;
padding: 20px;
}
.win-item {
background: rgba(44, 49, 63, 0.9);
padding: 24px 32px;
border-radius: 16px;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
min-width: 180px;
min-height: 80px;
flex-shrink: 0;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.win-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #2b7def, #722ed1, #13c2c2, #faad14);
opacity: 0;
transition: opacity 0.3s ease;
}
.win-item:hover::before {
opacity: 1;
}
.win-item.placeholder {
background: rgba(30, 36, 48, 0.6);
border: 2px dashed rgba(255, 255, 255, 0.1);
color: #6b7280;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { border-color: rgba(255, 255, 255, 0.1); }
50% { border-color: rgba(255, 255, 255, 0.3); }
}
@keyframes borderRun {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
@keyframes glow {
0%, 100% { box-shadow: 0 0 20px rgba(43, 125, 239, 0.3); }
50% { box-shadow: 0 0 40px rgba(43, 125, 239, 0.6), 0 0 60px rgba(114, 46, 209, 0.3); }
}
.win-item:not(.placeholder) {
border: 2px solid transparent;
background-image: linear-gradient(rgba(44, 49, 63, 0.9),rgba(44, 49, 63, 0.9)),linear-gradient(90deg,#2b7def,#722ed1,#13c2c2,#faad14,#2b7def);
background-origin: border-box;
background-clip: padding-box, border-box;
background-size: 200% 100%;
animation: borderRun 3s linear infinite, glow 2s ease-in-out infinite;
transform: scale(1);
animation-delay: 0s, 0.5s;
}
.rank-1 { color: #ffd700 !important; }
.rank-2 { color: #c0c0c0 !important; }
.rank-3 { color: #cd7f32 !important; }
.rank-4 { color: #ff7f50 !important; }
.rank-5 { color: #87ceeb !important; }
.rank-6 { color: #9370db !important; }
.rank-7 { color: #32cd32 !important; }
.rank-8 { color: #ff69b4 !important; }
.rank-9 { color: #13c2c2 !important; }
.log-box {
background: rgba(35, 40, 54, 0.85);
backdrop-filter: blur(20px);
padding: 16px;
border-radius: 16px;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 120px;
max-height: 400px;
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.log-box.hide-log{
min-height: 0;
max-height: 0;
height: 0 !important;
padding:0;
margin:0;
opacity:0;
}
.drag-bar.hide-log{
display: none;
}
.log-title {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
margin-bottom: 12px;
background: rgba(35, 40, 54, 0.85) !important;
padding: 8px 0;
z-index: 10;
flex-shrink: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.log-content {
flex: 1;
overflow-y: auto;
padding-right: 8px;
}
.log-content::-webkit-scrollbar {
width: 6px;
}
.log-content::-webkit-scrollbar-track {
background: rgba(44, 49, 63, 0.5);
border-radius: 3px;
}
.log-content::-webkit-scrollbar-thumb {
background: rgba(43, 125, 239, 0.5);
border-radius: 3px;
}
.drag-bar {
width: 60px;
height: 8px;
margin: 0 auto 15px auto;
background: rgba(58, 66, 82, 0.8);
cursor: ns-resize;
border-radius: 4px;
transition: all 0.3s ease;
position: relative;
}
.drag-bar::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 2px;
background: rgba(255, 255, 255, 0.3);
border-radius: 1px;
}
.drag-bar:hover {
background: rgba(43, 125, 239, 0.4);
transform: scaleY(1.2);
}
.log-actions {
display: flex;
gap: 16px;
}
.log-actions span {
color: #9ca3af;
font-size: 13px;
cursor: pointer;
padding: 6px 12px;
border-radius: 8px;
transition: all 0.3s ease;
}
.log-actions span:hover {
background: rgba(43, 125, 239, 0.2);
color: #2b7def;
}
.log-item {
padding: 12px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-size: 13px;
transition: background 0.2s ease;
}
.log-item:hover {
background: rgba(43, 125, 239, 0.05);
padding-left: 8px;
border-radius: 8px;
}
.log-time {
font-size: 14px;
color: #9ca3af;
}
.log-tag {
display: inline-block;
background: linear-gradient(135deg, #2b7def 0%, #1d4ed8 100%);
color: #fff;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
box-shadow: 0 2px 8px rgba(43, 125, 239, 0.3);
}
.log-names {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.log-name-tag {
background: rgba(58, 66, 82, 0.8);
padding: 5px 12px;
border-radius: 20px;
font-size: 13px;
color: #fff;
display: flex;
align-items: center;
gap: 6px;
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.3s ease;
}
.log-name-tag:hover {
background: rgba(43, 125, 239, 0.2);
border-color: rgba(43, 125, 239, 0.3);
}
.log-header-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 6px;
}
/* Toast 提示样式 */
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 14px 28px;
border-radius: 12px;
color: #fff;
font-size: 16px;
font-weight: 500;
z-index: 9999;
opacity: 0;
transform: translateX(-50%) translateY(-20px) scale(0.9);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.toast.success {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.4);
}
.toast.error {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
box-shadow: 0 8px 25px rgba(239, 68, 68, 0.4);
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
/* 彩带效果 */
.confetti-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 99999;
overflow: hidden;
}
.confetti {
position: absolute;
width: 10px;
height: 10px;
top: -10px;
animation: confetti-fall linear infinite;
}
.confetti.round {
border-radius: 50%;
}
.confetti.square {
transform: rotate(45deg);
}
.confetti.triangle {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 12px solid;
}
@keyframes confetti-fall {
0% {
transform: translateY(-10px) rotate(0deg) scale(1);
opacity: 1;
}
25% {
transform: translateY(25vh) rotate(90deg) scale(1);
}
50% {
transform: translateY(50vh) rotate(180deg) scale(0.9);
}
75% {
transform: translateY(75vh) rotate(270deg) scale(0.8);
}
100% {
transform: translateY(100vh) rotate(360deg) scale(0.5);
opacity: 0;
}
}
@keyframes confetti-sway {
0%, 100% {
margin-left: 0;
}
50% {
margin-left: 20px;
}
}
</style>
</head>
<body>
<div class="wrap">
<div class="top-box">
<div class="top-row">
<div class="left-section">
<div class="form-item">
<label class="item-label">选择奖项</label>
<select class="item-select" id="prizeSel"></select>
</div>
<div class="form-item">
<label class="item-label">中奖人数</label>
<input type="number" class="item-select" id="winNum" min="1" value="1">
</div>
<div class="form-item status-badge">
<span class="badge-value" id="surplus">0</span>
<span class="badge-total">/</span>
<span class="badge-total-num" id="totalAll">0</span>
</div>
</div>
<div class="right-section">
<button class="opt-btn reset-btn" title="重置可用人数">🔄 重置</button>
<button class="opt-btn" id="syncBtn" title="同步最新名单">🔄 刷新</button>
<button class="opt-btn" id="toggleLogBtn" title="隐藏/显示记录">📜 记录</button>
<a href="admin.html" target="_blank">
<button class="opt-btn" title="后台管理">⚙️ 设置</button>
</a>
</div>
</div>
<div class="btn-group">
<button class="btn btn-blue" id="actionBtn">开始抽奖</button>
</div>
</div>
<div class="win-box">
<div class="win-title" id="winTitle">准备抽奖</div>
<div class="win-subtitle" id="winSubtitle">请选择奖项后开始</div>
<div class="win-list" id="winList"></div>
</div>
<div class="drag-bar" id="dragBar"></div>
<div class="log-box" id="logBox">
<div class="log-title">
<span>⏱️ 今日开奖记录</span>
<div class="log-actions">
<span>📥 导出</span>
<span>🗑️ 清空</span>
</div>
</div>
<div class="log-content" id="logList"></div>
</div>
</div>
<!-- Toast 容器 -->
<div class="toast" id="toast"></div>
<!-- 彩带容器 -->
<div class="confetti-container" id="confettiContainer"></div>
<script>
let isRoll = false;
let rollRaf = null;
let allData = {};
let lastPrizeId = "";
let isRequest = false;
let logShow = true;
// 彩带颜色
const confettiColors = ['#ffd700', '#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dfe6e9', '#fd79a8', '#a29bfe', '#00b894'];
const ROLL_SPEED = 50; // 优化:加快滚动速度
let lastUpdateTime = 0;
const medalIcons = ['🏅','🥈','🥉','🏆','🏆','🏆','🏆','🏆','🏆'];
// Mock数据 - 用于离线测试
const mockData = {
names: [
{ name: '张三' }, { name: '李四' }, { name: '王五' }, { name: '赵六' },
{ name: '钱七' }, { name: '孙八' }, { name: '周九' }, { name: '吴十' },
{ name: '郑十一' }, { name: '王十二' }, { name: '陈十三' }, { name: '刘十四' },
{ name: '杨十五' }, { name: '黄十六' }, { name: '周十七' }, { name: '吴十八' },
{ name: '郑十九' }, { name: '王二十' }, { name: '陈二十一' }, { name: '刘二十二' }
],
prizes: [
{ id: '1', name: '一等奖', count: 1 },
{ id: '2', name: '二等奖', count: 2 },
{ id: '3', name: '三等奖', count: 3 },
{ id: '4', name: '四等奖', count: 5 }
],
used: [],
records: []
};
// 彩带效果函数
function showConfetti() {
const container = document.getElementById('confettiContainer');
container.innerHTML = '';
const confettiCount = 80;
for (let i = 0; i < confettiCount; i++) {
const confetti = document.createElement('div');
const type = ['round', 'square', 'triangle'][Math.floor(Math.random() * 3)];
const color = confettiColors[Math.floor(Math.random() * confettiColors.length)];
confetti.className = `confetti ${type}`;
confetti.style.left = `${Math.random() * 100}%`;
confetti.style.backgroundColor = type !== 'triangle' ? color : 'transparent';
if (type === 'triangle') {
confetti.style.borderBottomColor = color;
}
confetti.style.animationDuration = `${1.5 + Math.random() * 1.5}s`; // 加快下落速度
confetti.style.animationDelay = `${Math.random() * 0.1}s`; // 减少延迟
confetti.style.width = `${8 + Math.random() * 10}px`;
confetti.style.height = confetti.style.width;
container.appendChild(confetti);
}
setTimeout(() => {
container.innerHTML = '';
}, 6000);
}
// Toast 提示函数
function showToast(msg, type = 'success') {
const toast = document.getElementById('toast');
toast.className = `toast ${type}`;
toast.innerText = msg;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 2000);
}
window.onload = async function(){
await loadAllData();
renderPlaceholders();
initDragHeight();
document.getElementById('toggleLogBtn').addEventListener('click',toggleLogShow);
// 仅保留手动同步按钮
document.getElementById('syncBtn').addEventListener('click', async function(){
await loadAllData();
showToast('名单同步成功!', 'success');
});
};
function toggleLogShow(){
logShow = !logShow;
const logBox = document.getElementById('logBox');
const dragBar = document.getElementById('dragBar');
if(logShow){
logBox.classList.remove('hide-log');
dragBar.classList.remove('hide-log');
document.getElementById('toggleLogBtn').title = "隐藏记录";
}else{
logBox.classList.add('hide-log');
dragBar.classList.add('hide-log');
document.getElementById('toggleLogBtn').title = "显示记录";
}
}
function renderPlaceholders(){
const count = parseInt(document.getElementById("winNum").value) || 1;
const winList = document.getElementById("winList");
let html = "";
for(let i=0; i<count; i++){
html += `<div class="win-item placeholder">待开奖</div>`;
}
winList.innerHTML = html;
document.getElementById("winTitle").innerText = "准备抽奖";
document.getElementById("winSubtitle").innerText = "本次幸运儿会是你吗";
document.getElementById("actionBtn").innerText = "开始抽奖";
isRoll = false;
if(rollRaf) cancelAnimationFrame(rollRaf);
}
async function loadAllData(){
try {
const res = await fetch("/lottery/api.php?act=all");
allData = await res.json();
} catch (error) {
// 离线模式:使用mock数据
console.log('使用离线mock数据');
allData = JSON.parse(JSON.stringify(mockData));
}
document.getElementById("totalAll").innerText = allData.names.length;
document.getElementById("surplus").innerText = allData.names.length - allData.used.length;
const sel = document.getElementById("prizeSel");
sel.innerHTML = '<option value="">请选择奖项</option>';
allData.prizes.forEach(p=>{
sel.innerHTML += `<option value="${p.id}">${p.name}</option>`;
});
sel.value = lastPrizeId;
sel.onchange = function(){
lastPrizeId = this.value;
let item = allData.prizes.find(x=>x.id == this.value);
if(item){
document.getElementById("winNum").value = item.count;
renderPlaceholders();
}
};
loadLog();
}
function loadLog(){
const logDom = document.getElementById("logList");
const today = new Date();
const todayStr = `${today.getFullYear()}/${(today.getMonth()+1).toString().padStart(2,0)}/${today.getDate().toString().padStart(2,0)}`;
let html = "";
allData.records.forEach(item=>{
let d = new Date(item.time);
let dStr = `${d.getFullYear()}/${(d.getMonth()+1).toString().padStart(2,0)}/${d.getDate().toString().padStart(2,0)}`;
if(dStr !== todayStr) return;
let timeStr = `${d.getFullYear()}/${(d.getMonth()+1).toString().padStart(2,0)} ${d.getHours().toString().padStart(2,0)}:${d.getMinutes().toString().padStart(2,0)}`;
html += `<div class="log-item">
<div class="log-header-row">
<span class="log-time">${timeStr}</span>
<span class="log-tag">${item.prize}</span>
</div>
<div class="log-names">${item.winners.split("、").map((n,i)=>`<span class="log-name-tag">${medalIcons[i]||'🏆'}${n}</span>`).join("")}</div>
</div>`;
});
logDom.innerHTML = html || '<div class="log-item">暂无今日开奖记录</div>';
}
function clearEmoji(str){
return str.replace(/[\u{1F300}-\u{1F999}]|🏅|🥈|🥉|🏆/gu,'').trim();
}
async function toggleLottery(){
const pid = document.getElementById("prizeSel").value;
const wCnt = parseInt(document.getElementById("winNum").value) || 1;
const surplus = allData.names.length - allData.used.length;
if(!pid){
showToast("请先选择奖项", "error");
return;
}
if(wCnt > surplus){
showToast(`剩余可用人数:${surplus} 人,当前需要抽取:${wCnt} 人`, "error");
return;
}
if(isRequest) return;
if(isRoll){
isRoll = false;
cancelAnimationFrame(rollRaf);
document.getElementById("actionBtn").innerText = "开始抽奖";
document.getElementById("winTitle").innerText = "🎉 恭喜中奖";
document.getElementById("winSubtitle").innerText = "恭喜以下幸运用户";
const items = document.querySelectorAll("#winList .win-item");
let finalNames = [];
items.forEach(el=>{
let pureName = clearEmoji(el.innerText);
finalNames.push(pureName);
});
// 开奖完成后显示奖牌
const winList = document.getElementById("winList");
winList.innerHTML = finalNames.map((name, i) => {
const medal = i < medalIcons.length ? medalIcons[i] : '';
return `<div class="win-item">${medal} ${name}</div>`;
}).join('');
isRequest = true;
try{
let nameStr = finalNames.join(",");
try {
await fetch(`/lottery/api.php?act=draw&prizeId=${pid}&count=${wCnt}&names=${encodeURIComponent(nameStr)}`);
// HTTP模式:重新加载数据以更新界面
await loadAllData();
} catch (fetchError) {
// 离线模式:本地记录中奖名单
finalNames.forEach(name => {
if (!allData.used.includes(name)) {
allData.used.push(name);
}
});
const prize = allData.prizes.find(p => p.id == pid);
const now = new Date();
const timeStr = `${now.getFullYear()}/${(now.getMonth()+1).toString().padStart(2,0)}/${now.getDate().toString().padStart(2,0)} ${now.getHours().toString().padStart(2,0)}:${now.getMinutes().toString().padStart(2,0)}`;
allData.records.unshift({
time: timeStr,
prize: prize ? prize.name : '未知奖项',
winners: finalNames.join('、')
});
document.getElementById("surplus").innerText = allData.names.length - allData.used.length;
}
showToast("🎉 开奖成功!", "success");
showConfetti(); // 显示彩带效果
loadLog(); // 更新日志显示
}catch(err){
showToast("开奖异常,请重试", "error");
renderPlaceholders();
}
isRequest = false;
const winItems = document.querySelectorAll("#winList .win-item");
winItems.forEach((item,idx)=>{
item.classList.add(`rank-${idx+1}`);
});
return;
}
isRoll = true;
lastUpdateTime = performance.now();
document.getElementById("actionBtn").innerText = "开 奖";
document.getElementById("winTitle").innerText="抽奖进行中";
document.getElementById("winSubtitle").innerText="等待揭晓幸运名单";
function roll(nowTime){
if(!isRoll) return;
rollRaf = requestAnimationFrame(roll);
if(nowTime - lastUpdateTime < ROLL_SPEED) return;
lastUpdateTime = nowTime;
let available = allData.names.filter(item => {
return !allData.used.includes(item.name);
});
if(available.length === 0) return;
let html = "";
let tempPool = [];
for(let i=0;i<wCnt;i++){
let rnd;
do{
let idx = Math.floor(Math.random() * available.length);
rnd = available[idx];
}while(tempPool.includes(rnd.name));
tempPool.push(rnd.name);
// 滚动时不显示奖牌,只显示名字
html += `<div class="win-item">${rnd.name}</div>`;
}
document.getElementById("winList").innerHTML = html;
}
rollRaf = requestAnimationFrame(roll);
}
// 直接重置,无任何确认弹窗,只提示结果
async function resetUsed(){
try {
try {
await fetch("/lottery/api.php?act=clearUsed");
} catch (fetchError) {
// 离线模式:本地重置
}
allData.used = [];
// 保留中奖记录,只重置可用人数
document.getElementById("surplus").innerText = allData.names.length;
renderPlaceholders();
showToast("重置成功!", "success");
} catch (e) {
showToast("重置失败!", "error");
}
}
function exportRecord(){
window.open("/lottery/api.php?act=export");
}
function clearRecord(){
document.getElementById("logList").innerHTML = '<div class="log-item">暂无今日开奖记录</div>';
showToast("记录已清空", "success");
}
function initDragHeight(){
const logBox = document.getElementById('logBox');
const dragBar = document.getElementById('dragBar');
let isDragging = false;
let targetHeight = 200;
dragBar.addEventListener('mousedown', (e) => {
isDragging = true;
document.body.style.cursor = 'ns-resize';
e.preventDefault();
targetHeight = parseInt(logBox.style.height) || 200;
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
targetHeight = window.innerHeight - e.clientY - 20;
if (targetHeight < 120) targetHeight = 120;
if (targetHeight > 400) targetHeight = 400;
});
function updateHeight() {
if (isDragging) {
logBox.style.height = targetHeight + 'px';
requestAnimationFrame(updateHeight);
}
}
window.addEventListener('mousedown', () => {
if (isDragging) {
updateHeight();
}
});
window.addEventListener('mouseup', () => {
isDragging = false;
document.body.style.cursor = '';
});
}
</script>
</body>
</html>
以下为api.phpl源码
[PHP] 纯文本查看 复制代码 <?php
session_start();
header('Content-Type: application/json; charset=utf-8');
$pdo = new PDO('mysql:host=localhost;dbname=lottery;charset=utf8mb4', 'root', 'Hcj@20260428');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$adminPwd = 'hcj123';
$editorPwd = 'md123';
$act = $_GET['act'] ?? '';
// 登录
if ($act === 'login') {
$pwd = $_GET['pwd'] ?? '';
session_unset();
if ($pwd === $adminPwd) {
$_SESSION['role'] = 'admin';
echo json_encode(['ok' => 1]);
} else if ($pwd === $editorPwd) {
$_SESSION['role'] = 'editor';
echo json_encode(['ok' => 1]);
} else {
echo json_encode(['error' => '密码错误']);
}
exit;
}
// 检查登录
if ($act === 'checkLogin') {
$role = $_SESSION['role'] ?? '';
if (in_array($role, ['admin', 'editor'])) {
echo json_encode(['ok' => 1, 'role' => $role]);
} else {
echo json_encode(['error' => '未登录']);
}
exit;
}
// 退出
if ($act === 'logout') {
session_destroy();
echo json_encode(['ok' => 1]);
exit;
}
// 公开接口
$public_actions = ['all', 'draw', 'clearUsed'];
$role = $_SESSION['role'] ?? '';
// 需要登录的接口
$needLogin = [
'namePage','addNames','delName','updateName','clearNames',
'addPrize','editPrize','delPrize','clearRecord','export'
];
if(in_array($act, $needLogin)){
if(!in_array($role,['admin','editor'])){
echo json_encode(['error'=>'请先登录']);exit;
}
}
// ===== 关键权限隔离 =====
// 仅管理员可用
$adminOnly = ['addPrize','editPrize','delPrize','clearRecord','export'];
if(in_array($act,$adminOnly) && $role !== 'admin'){
echo json_encode(['error'=>'无权限']);exit;
}
// editor 全部名单操作放行(增删改清空+去重)
// ========================
// 获取全部数据
if ($act === 'all') {
$prizes = $pdo->query("SELECT * FROM prizes")->fetchAll(PDO::FETCH_ASSOC);
$names = $pdo->query("SELECT id,name FROM names ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC);
$used = $pdo->query("SELECT name FROM used")->fetchAll(PDO::FETCH_COLUMN);
$records = $pdo->query("SELECT * FROM records ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'prizes' => $prizes,
'names' => $names,
'used' => $used,
'records' => $records
], JSON_UNESCAPED_UNICODE);
exit;
}
// 添加奖项
if ($act === 'addPrize') {
$name = $_GET['name'];
$count = (int)$_GET['count'];
$pdo->prepare("INSERT INTO prizes(name,count) VALUES(?,?)")->execute([$name, $count]);
echo json_encode(['ok' => 1]);
exit;
}
// 修改奖项
if ($act === 'editPrize') {
$id = (int)$_GET['id'];
$name = $_GET['name'];
$count = (int)$_GET['count'];
$pdo->prepare("UPDATE prizes SET name=?,count=? WHERE id=?")->execute([$name, $count, $id]);
echo json_encode(['ok' => 1]);
}
// 删除奖项
if ($act === 'delPrize') {
$id = (int)$_GET['id'];
$pdo->prepare("DELETE FROM prizes WHERE id=?")->execute([$id]);
echo json_encode(['ok' => 1]);
exit;
}
// 抽奖
if ($act === 'draw') {
$prizeId = (int)$_GET['prizeId'];
$count = (int)$_GET['count'];
$namesInput = $_GET['names'] ?? '';
$st = $pdo->prepare("SELECT name FROM prizes WHERE id=?");
$st->execute([$prizeId]);
$prize = $st->fetch(PDO::FETCH_ASSOC);
if (!$prize) {
echo json_encode(['error' => '奖项不存在']);
exit;
}
$candidates = $pdo->query("SELECT name FROM names WHERE name NOT IN (SELECT name FROM used)")->fetchAll(PDO::FETCH_COLUMN);
if (count($candidates) < $count) {
echo json_encode(['error' => '剩余参与人数不足']);
exit;
}
$winners = [];
if (!empty($namesInput)) {
$winners = array_map('trim', explode(',', $namesInput));
}
$validWinners = [];
foreach ($winners as $w) {
$w = trim($w);
if(empty($w)) continue;
$chkName = $pdo->prepare("SELECT 1 FROM names WHERE name = ?");
$chkName->execute([$w]);
if(!$chkName->fetchColumn()) continue;
$chkUsed = $pdo->prepare("SELECT 1 FROM used WHERE name = ?");
$chkUsed->execute([$w]);
if (!$chkUsed->fetchColumn()) {
$validWinners[] = $w;
}
}
if(count($validWinners) < $count){
shuffle($candidates);
$needNum = $count - count($validWinners);
$fillArr = array_slice($candidates,0,$needNum);
$validWinners = array_merge($validWinners,$fillArr);
}
$validWinners = array_slice($validWinners,0,$count);
foreach ($validWinners as $w) {
$pdo->prepare("INSERT IGNORE INTO used(name) VALUES(?)")->execute([$w]);
}
$winStr = implode('、', $validWinners);
$pdo->prepare("INSERT INTO records(prize,winners,time) VALUES(?,?,now())")->execute([$prize['name'], $winStr]);
echo json_encode(['winners' => $validWinners], JSON_UNESCAPED_UNICODE);
exit;
}
// 清空已中奖
if ($act === 'clearUsed') {
$pdo->exec("TRUNCATE TABLE used");
echo json_encode(['ok' => 1]);
exit;
}
// 清空记录
if ($act === 'clearRecord') {
$pdo->exec("TRUNCATE TABLE records");
echo json_encode(['ok' => 1]);
exit;
}
// 导出
if ($act === 'export') {
header("Content-Type:application/vnd.ms-excel");
header("Content-Disposition:attachment;filename=record.xls");
$res = $pdo->query("SELECT * FROM records");
echo "奖项\t中奖人\t时间\n";
while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
echo "{$row['prize']}\t{$row['winners']}\t{$row['time']}\n";
}
exit;
}
// 名单分页
if ($act === 'namePage') {
$page = (int)($_GET['page'] ?? 1);
$kw = $_GET['kw'] ?? '';
$size = 20;
$offset = ($page - 1) * $size;
$where = $kw ? "WHERE name LIKE '%" . $kw . "%'" : "";
$total = $pdo->query("SELECT COUNT(*) FROM names $where")->fetchColumn();
$list = $pdo->query("SELECT id,name FROM names $where ORDER BY id DESC LIMIT $offset,$size")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'list' => $list,
'page' => $page,
'total' => $total,
'totalPage' => ceil($total / $size)
], JSON_UNESCAPED_UNICODE);
exit;
}
// 添加名单【editor允许+重复校验】
if ($act === 'addNames') {
$names = array_filter(array_map('trim', explode("\n", $_GET['names'])));
$duplicate = [];
$success = [];
foreach ($names as $name) {
if (empty($name)) continue;
$stmt = $pdo->prepare("SELECT id FROM names WHERE name = ?");
$stmt->execute([$name]);
if ($stmt->fetchColumn()) {
$duplicate[] = $name;
} else {
$success[] = $name;
}
}
if (!empty($duplicate)) {
echo json_encode(["error" => "存在重复名单:".implode("、",$duplicate)]);
exit;
}
$stmt = $pdo->prepare("INSERT INTO names (name) VALUES (?)");
foreach ($success as $s) $stmt->execute([$s]);
echo json_encode(["ok" => 1]);
exit;
}
// 修改名单【editor允许+去重】
if ($act === 'updateName') {
$id = (int)$_GET['id'];
$newName = trim($_GET['name']);
if (!$newName) {
echo json_encode(['error' => '姓名不能为空']);
exit;
}
$stmt = $pdo->prepare("SELECT id FROM names WHERE name = ? AND id != ?");
$stmt->execute([$newName, $id]);
if ($stmt->fetchColumn()) {
echo json_encode(['error' => '该姓名已存在,不能重复']);
}else{
$pdo->prepare("UPDATE names SET name = ? WHERE id = ?")->execute([$newName, $id]);
echo json_encode(['ok' => 1]);
}
exit;
}
// 删除单个名单【editor允许】
if ($act === 'delName') {
$id = (int)$_GET['id'];
$pdo->prepare("DELETE FROM names WHERE id=?")->execute([$id]);
echo json_encode(['ok' => 1]);
exit;
}
// 清空全部名单【editor允许】
if ($act === 'clearNames') {
$pdo->exec("TRUNCATE TABLE names");
echo json_encode(['ok' => 1]);
exit;
}
echo json_encode(['error' => '请求错误']);
?>
以下为admin.html源码
[HTML] 纯文本查看 复制代码 <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>抽奖后台管理</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎁</text></svg>" type="image/svg+xml">
<style>
*{margin:0;padding:0;box-sizing:border-box;font-family:"Microsoft YaHei", "PingFang SC", sans-serif;}
body{
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
color:#fff;
padding:25px;
min-height:100vh;
font-size:16px;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 80%, rgba(43, 125, 239, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(114, 46, 209, 0.1) 0%, transparent 50%);
pointer-events: none;
}
.wrap{
max-width:1920px;
width:100%;
margin:0 auto;
padding:0 20px;
position: relative;
z-index: 1;
}
.box{
background: rgba(35, 40, 54, 0.85);
backdrop-filter: blur(20px);
padding:28px;
border-radius:16px;
margin-bottom:20px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
h2{
color: transparent;
background: linear-gradient(135deg, #ffd700 0%, #ff9500 100%);
-webkit-background-clip: text;
background-clip: text;
margin-bottom:24px;
font-size: 24px;
font-weight: 600;
}
h3{
margin-bottom:20px;
font-size: 18px;
color: #e5e7eb;
font-weight: 500;
}
.tab-bar{
display:flex;
gap:12px;
margin-bottom:24px;
flex-wrap:wrap;
padding: 12px;
background: rgba(35, 40, 54, 0.6);
backdrop-filter: blur(10px);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.tab-bar button{
background: rgba(44, 49, 63, 0.8);
border:1px solid rgba(255, 255, 255, 0.1);
color:#9ca3af;
padding:12px 28px;
border-radius:10px;
cursor:pointer;
font-size:16px;
font-weight: 500;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.tab-bar button::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #2b7def, #722ed1);
transform: scaleX(0);
transition: transform 0.3s ease;
}
.tab-bar button:hover{
background: rgba(43, 125, 239, 0.2);
border-color: rgba(43, 125, 239, 0.4);
color: #fff;
}
.tab-bar button.active{
background: linear-gradient(135deg, #2b7def 0%, #1d4ed8 100%);
border-color: #2b7def;
color: #fff;
box-shadow: 0 4px 15px rgba(43, 125, 239, 0.4);
}
.tab-bar button.active::before {
transform: scaleX(1);
}
.tab{display:none;}
.tab.show{display:block !important;}
input,textarea{
width:100%;
background: rgba(44, 49, 63, 0.8);
border:1px solid rgba(255, 255, 255, 0.1);
color:#fff;
padding:14px 16px;
border-radius:10px;
margin-bottom:16px;
font-size:16px;
transition: all 0.3s ease;
outline: none;
}
input:focus,textarea:focus{
border-color: #2b7def;
box-shadow: 0 0 0 3px rgba(43, 125, 239, 0.2);
}
input::placeholder,textarea::placeholder{
color: #6b7280;
}
button{
padding:12px 24px;
border:0;
border-radius:10px;
cursor:pointer;
font-size:16px;
font-weight: 500;
transition: all 0.3s ease;
outline: none;
position: relative;
overflow: hidden;
}
button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
button:hover::before {
left: 100%;
}
.btn-blue{
background: linear-gradient(135deg, #2b7def 0%, #1d4ed8 100%);
color:#fff;
box-shadow: 0 4px 15px rgba(43, 125, 239, 0.4);
}
.btn-blue:hover{
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(43, 125, 239, 0.5);
}
.btn-red{
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color:#fff;
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
}
.btn-red:hover{
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(239, 68, 68, 0.5);
}
.btn-green{
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color:#fff;
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
}
.btn-green:hover{
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.5);
}
table{
width:100%;
border-collapse:collapse;
margin:20px 0;
background: rgba(44, 49, 63, 0.4);
border-radius: 12px;
overflow: hidden;
}
th{
background: linear-gradient(135deg, rgba(43, 125, 239, 0.3) 0%, rgba(29, 78, 216, 0.2) 100%);
color: #e5e7eb;
font-weight: 600;
text-align: left;
padding: 16px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
td{
padding: 16px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
color: #d1d5db;
transition: background 0.2s ease;
}
tr:hover td{
background: rgba(43, 125, 239, 0.1);
}
tr:last-child td{
border-bottom: none;
}
.search-row{
display:flex;
gap:12px;
margin-bottom:20px;
align-items:center;
flex-wrap:wrap;
}
.search-row input{
max-width: 300px;
}
.page-bar{
display:flex;
gap:12px;
margin-top:20px;
align-items:center;
}
.page-bar button{
background: rgba(44, 49, 63, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #9ca3af;
padding: 8px 16px;
font-size: 14px;
}
.page-bar button:hover{
background: rgba(43, 125, 239, 0.2);
color: #fff;
border-color: rgba(43, 125, 239, 0.4);
}
.page-bar span{
color: #9ca3af;
font-size: 14px;
}
.tip{color:#9ca3af;margin:12px 0;font-size:14px;}
.login-box{
max-width:420px;
margin: 100px auto;
text-align:center;
background: rgba(35, 40, 54, 0.9);
backdrop-filter: blur(20px);
padding: 48px;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.toast {
position: fixed;
top: 25px;
right: 25px;
background: linear-gradient(135deg, #2b7def 0%, #1d4ed8 100%);
color: #fff;
padding: 14px 24px;
border-radius: 12px;
box-shadow: 0 8px 25px rgba(43, 125, 239, 0.4);
z-index: 9999;
opacity: 0;
transform: translateY(-20px) scale(0.9);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-weight: 500;
}
.toast.show {
opacity: 1;
transform: translateY(0) scale(1);
}
.toast.success {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.4);
}
.toast.error {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
box-shadow: 0 8px 25px rgba(239, 68, 68, 0.4);
}
</style>
</head>
<body>
<div class="wrap">
<div id="toast" class="toast"></div>
<div id="loginBox" class="login-box">
<h2>管理员登录</h2>
<input type="password" id="pwd" placeholder="请输入密码">
<button class="btn-blue">登录</button>
</div>
<div id="main" style="display:none;">
<div class="tab-bar">
<button class="active" id="tab1">奖项管理</button>
<button id="tab2">名单管理</button>
<button id="tab3">抽奖记录</button>
<button class="btn-green">返回抽奖</button>
<button class="btn-red">退出</button>
</div>
<div class="tab" id="c1">
<div class="box">
<h3>添加奖项</h3>
<input id="prizeName" placeholder="奖项名称">
<input id="prizeCount" type="number" placeholder="中奖人数" value="1">
<button class="btn-blue">添加</button>
<table>
<tr><th>ID</th><th>奖项</th><th>人数</th><th>操作</th></tr>
<tbody id="prizeList"></tbody>
</table>
</div>
</div>
<div class="tab" id="c2">
<div class="box">
<h3>批量导入名单</h3>
<textarea id="nameText" rows="6" placeholder="一行一个姓名"></textarea>
<button class="btn-blue">导入</button>
<button class="btn-red">清空名单</button>
<div class="tip">总人数:<span id="nameTotal">0</span></div>
<div class="search-row">
<input id="kw" placeholder="搜索姓名" style="flex:1;">
<button class="btn-blue">搜索</button>
<button>重置</button>
</div>
<table>
<tr><th>ID</th><th>姓名</th><th>操作</th></tr>
<tbody id="nameList"></tbody>
</table>
<div class="page-bar" id="namePage"></div>
</div>
</div>
<div class="tab" id="c3">
<div class="box">
<button class="btn-red">清空记录</button>
<button class="btn-blue">导出记录</button>
<table>
<tr><th>时间</th><th>奖项</th><th>中奖人</th></tr>
<tbody id="recordList"></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
let currentPage = 1;
const API = "api.php";
const LOGIN_KEY = "admin_login_token";
const EXPIRE_KEY = "admin_login_expire";
let userRole = "";
// Mock数据 - 用于离线测试
const mockData = {
names: [
{ id: 1, name: '张三' }, { id: 2, name: '李四' }, { id: 3, name: '王五' },
{ id: 4, name: '赵六' }, { id: 5, name: '钱七' }, { id: 6, name: '孙八' },
{ id: 7, name: '周九' }, { id: 8, name: '吴十' }, { id: 9, name: '郑十一' },
{ id: 10, name: '王十二' }, { id: 11, name: '陈十三' }, { id: 12, name: '刘十四' }
],
prizes: [
{ id: 1, name: '一等奖', count: 1 },
{ id: 2, name: '二等奖', count: 2 },
{ id: 3, name: '三等奖', count: 3 },
{ id: 4, name: '参与奖', count: 5 }
],
records: []
};
// 本地数据存储
let localData = JSON.parse(JSON.stringify(mockData));
window.onload = function () {
checkLogin();
};
function toast(msg, type = 'success') {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = `toast ${type}`;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2000);
}
async function checkLogin() {
const token = localStorage.getItem(LOGIN_KEY);
const expire = localStorage.getItem(EXPIRE_KEY);
if (!token || !expire || Date.now() > expire) {
logout(true);
return;
}
const res = await fetch(`${API}?act=checkLogin`);
const data = await res.json();
if (data.error) {
logout(true);
return;
}
userRole = data.role;
renderTabsByRole();
loadAll();
showMain();
}
function renderTabsByRole() {
const tab1 = document.getElementById('tab1');
const tab3 = document.getElementById('tab3');
const c1 = document.getElementById('c1');
const c3 = document.getElementById('c3');
if (userRole === 'editor') {
// 名单账号:隐藏奖项+记录
tab1.style.display = 'none';
tab3.style.display = 'none';
openTab(2);
} else {
// 管理员:全部显示
tab1.style.display = 'inline-block';
tab3.style.display = 'inline-block';
openTab(1);
}
}
async function login() {
const pwd = document.getElementById('pwd').value;
try {
const res = await fetch(`${API}?act=login&pwd=${pwd}`);
const data = await res.json();
if (data.error) {
toast(data.error, 'error');
} else {
const token = "login_" + Date.now();
const expire = Date.now() + 24 * 60 * 1000;
localStorage.setItem(LOGIN_KEY, token);
localStorage.setItem(EXPIRE_KEY, expire);
toast("登录成功");
checkLogin();
}
} catch (error) {
// 离线模式:使用mock登录
// 任何密码都可以登录(用于测试)
if (pwd) {
const token = "login_" + Date.now();
const expire = Date.now() + 24 * 60 * 1000;
localStorage.setItem(LOGIN_KEY, token);
localStorage.setItem(EXPIRE_KEY, expire);
userRole = 'admin'; // 离线模式默认为管理员
toast("离线模式登录成功");
showMain();
loadAll();
} else {
toast("请输入密码", 'error');
}
}
}
function goToLottery() {
window.location.href = 'index.html';
}
function logout(isAuto = false) {
localStorage.removeItem(LOGIN_KEY);
localStorage.removeItem(EXPIRE_KEY);
fetch(`${API}?act=logout`);
if (!isAuto) toast("已退出登录");
showLogin();
}
function showLogin() {
document.getElementById('loginBox').style.display = "block";
document.getElementById('main').style.display = "none";
}
function showMain() {
document.getElementById('loginBox').style.display = "none";
document.getElementById('main').style.display = "block";
}
function openTab(n) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('show'));
document.querySelectorAll('.tab-bar button').forEach(b => b.classList.remove('active'));
document.getElementById('c' + n).classList.add('show');
document.getElementById('tab' + n).classList.add('active');
if (n === 1) loadPrizes();
if (n === 2) loadNamePage(1);
if (n === 3) loadRecords();
}
async function loadAll() {
loadPrizes();
loadNamePage(1);
loadRecords();
}
async function loadPrizes() {
let data;
try {
const res = await fetch(`${API}?act=all`);
data = await res.json();
} catch (error) {
// 离线模式
data = localData;
}
let html = "";
data.prizes.forEach(p => {
html += `
<tr>
<td>${p.id}</td>
<td><input value="${p.name}" id="pn${p.id}" style="width:140px"></td>
<td><input type="number" value="${p.count}" id="pc${p.id}" style="width:80px"></td>
<td>
<button class="btn-blue">保存</button>
<button class="btn-red">删除</button>
</td>
</tr>`;
});
document.getElementById('prizeList').innerHTML = html;
}
async function addPrize() {
const name = document.getElementById('prizeName').value;
const count = document.getElementById('prizeCount').value;
try {
const res = await fetch(`${API}?act=addPrize&name=${encodeURIComponent(name)}&count=${count}`);
const data = await res.json();
if (data.error) toast(data.error, 'error');
else {
toast("添加成功");
loadPrizes();
}
} catch (error) {
// 离线模式
const newId = Math.max(...localData.prizes.map(p => p.id)) + 1;
localData.prizes.push({ id: newId, name, count: parseInt(count) });
toast("添加成功(离线模式)");
loadPrizes();
}
}
async function savePrize(id) {
const name = document.getElementById(`pn${id}`).value;
const count = document.getElementById(`pc${id}`).value;
try {
await fetch(`${API}?act=editPrize&id=${id}&name=${encodeURIComponent(name)}&count=${count}`);
toast("修改成功");
} catch (error) {
// 离线模式
const prize = localData.prizes.find(p => p.id == id);
if (prize) {
prize.name = name;
prize.count = parseInt(count);
toast("修改成功(离线模式)");
}
}
}
async function delPrize(id) {
if (!confirm("确定删除?")) return;
try {
await fetch(`${API}?act=delPrize&id=${id}`);
toast("删除成功");
loadPrizes();
} catch (error) {
// 离线模式
localData.prizes = localData.prizes.filter(p => p.id != id);
toast("删除成功(离线模式)");
loadPrizes();
}
}
async function loadNamePage(page) {
currentPage = page;
const kw = document.getElementById('kw').value;
let data;
try {
const res = await fetch(`${API}?act=namePage&page=${page}&kw=${encodeURIComponent(kw)}`);
data = await res.json();
} catch (error) {
// 离线模式
let filtered = localData.names;
if (kw) {
filtered = filtered.filter(n => n.name.includes(kw));
}
const pageSize = 10;
const total = filtered.length;
const totalPage = Math.ceil(total / pageSize);
const start = (page - 1) * pageSize;
const list = filtered.slice(start, start + pageSize);
data = { list, total, totalPage };
}
let html = "";
data.list.forEach(item => {
html += `
<tr>
<td>${item.id}</td>
<td><input type="text" id="name_${item.id}" value="${item.name}" style="width:180px"></td>
<td>
<button class="btn-blue">保存</button>
<button class="btn-red">删除</button>
</td>
</tr>`;
});
document.getElementById('nameList').innerHTML = html;
document.getElementById('nameTotal').textContent = data.total;
let pageHtml = "";
if (page > 1) pageHtml += `<button>上一页</button>`;
pageHtml += `<span>第${page}页 / 共${data.totalPage}页</span>`;
if (page < data.totalPage) pageHtml += `<button>下一页</button>`;
document.getElementById('namePage').innerHTML = pageHtml;
}
async function saveName(id) {
const newName = document.getElementById(`name_${id}`).value.trim();
if (!newName) {
toast("姓名不能为空", "error");
return;
}
try {
const res = await fetch(`${API}?act=updateName&id=${id}&name=${encodeURIComponent(newName)}`);
const data = await res.json();
if (data.error) {
toast(data.error, "error");
} else {
toast("修改成功");
loadNamePage(currentPage);
}
} catch (error) {
// 离线模式
const name = localData.names.find(n => n.id == id);
if (name) {
name.name = newName;
toast("修改成功(离线模式)");
loadNamePage(currentPage);
}
}
}
async function delName(id) {
try {
await fetch(`${API}?act=delName&id=${id}`);
toast("删除成功");
loadNamePage(currentPage);
} catch (error) {
// 离线模式
localData.names = localData.names.filter(n => n.id != id);
toast("删除成功(离线模式)");
loadNamePage(currentPage);
}
}
async function clearNames() {
if (!confirm("确定清空所有名单?不可恢复!")) return;
try {
await fetch(`${API}?act=clearNames`);
toast("清空成功");
loadNamePage(1);
} catch (error) {
// 离线模式
localData.names = [];
toast("清空成功(离线模式)");
loadNamePage(1);
}
}
async function addNames() {
const names = document.getElementById('nameText').value;
try {
const res = await fetch(`${API}?act=addNames&names=${encodeURIComponent(names)}`);
const data = await res.json();
if (data.error) {
toast(data.error, "error");
} else {
toast("导入成功");
loadNamePage(1);
}
} catch (error) {
// 离线模式
const nameList = names.split(/[\n,,;;、]/).filter(n => n.trim());
let count = 0;
nameList.forEach(name => {
const trimmedName = name.trim();
if (trimmedName && !localData.names.some(n => n.name === trimmedName)) {
const newId = Math.max(...localData.names.map(n => n.id), 0) + 1;
localData.names.push({ id: newId, name: trimmedName });
count++;
}
});
toast(`导入成功(离线模式),新增 ${count} 条记录`);
loadNamePage(1);
}
}
function searchName() {
loadNamePage(1);
}
function resetSearch() {
document.getElementById('kw').value = "";
loadNamePage(1);
}
async function loadRecords() {
let data;
try {
const res = await fetch(`${API}?act=all`);
data = await res.json();
} catch (error) {
// 离线模式
data = localData;
}
let html = "";
data.records.forEach(r => {
html += `<tr><td>${r.time}</td><td>${r.prize}</td><td>${r.winners}</td></tr>`;
});
document.getElementById('recordList').innerHTML = html;
}
async function clearRecord() {
if (!confirm("确定清空抽奖记录?")) return;
try {
await fetch(`${API}?act=clearRecord`);
toast("清空成功");
loadRecords();
} catch (error) {
// 离线模式
localData.records = [];
toast("清空成功(离线模式)");
loadRecords();
}
}
function exportRecord() {
window.open(`${API}?act=export`);
}
</script>
</body>
</html>
lottery.zip
(16.3 KB, 下载次数: 14)
|
|