吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 130|回复: 1
上一主题 下一主题
收起左侧

[学习记录] 自用名单抽奖代码

[复制链接]
跳转到指定楼层
楼主
zbmxqddn 发表于 2026-6-2 08:10 回帖奖励
本帖最后由 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>&#127881;</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="重置可用人数">&#128260; 重置</button>
                <button class="opt-btn" id="syncBtn" title="同步最新名单">&#128260; 刷新</button>
                <button class="opt-btn" id="toggleLogBtn" title="隐藏/显示记录">&#128220; 记录</button>
                <a href="admin.html" target="_blank">
                    <button class="opt-btn" title="后台管理">&#9881;&#65039; 设置</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>&#9201;&#65039; 今日开奖记录</span>
            <div class="log-actions">
                <span>&#128229; 导出</span>
                <span>&#128465;&#65039; 清空</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 = ['&#127941;','&#129352;','&#129353;','&#127942;','&#127942;','&#127942;','&#127942;','&#127942;','&#127942;'];

// 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]||'&#127942;'}${n}</span>`).join("")}</div>
                </div>`;
    });
    logDom.innerHTML = html || '<div class="log-item">暂无今日开奖记录</div>';
}

function clearEmoji(str){
    return str.replace(/[\u{1F300}-\u{1F999}]|&#127941;|&#129352;|&#129353;|&#127942;/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 = "&#127881; 恭喜中奖";
        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("&#127881; 开奖成功!", "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>&#127873;</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)

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
toms5566 发表于 2026-6-3 17:18
很实用,但请问为何名单无法更新。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-6-4 05:11

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表