Skip to content

Instantly share code, notes, and snippets.

@liuran001
Last active December 18, 2025 12:56
Show Gist options
  • Select an option

  • Save liuran001/6ac642b14aec326feaf4f5a73c6f6692 to your computer and use it in GitHub Desktop.

Select an option

Save liuran001/6ac642b14aec326feaf4f5a73c6f6692 to your computer and use it in GitHub Desktop.
Lucky Draw | 幸运抽奖器
{
// 使用前请删除所有注释
// 使用参数 ?config=<Link> 可指定配置链接
// 界面主题:可选 "red" (红), "blue" (蓝), "green" (绿), "cyber" (赛博朋克)
"theme": "red",
// 页面顶部的大标题
"title": "2025年度公司年会盛典",
// 抽奖模式:可选 "name" (名单模式) 或 "number" (数字模式)
"mode": "name",
// 数字模式下的最小值和最大值 (仅当 mode 为 number 时生效)
"min": 1,
"max": 500,
// 全局背景图 (可选,填图片URL)
"bgImg": "https://images.unsplash.com/photo-1606903251421-4d1066742512?auto=format&fit=crop&q=80&w=1920",
// 全屏中奖展示时的背景图 (可选)
"fullScreenBg": "",
// 页面底部的装饰大图 (通常放奖品图,会被奖项预设覆盖)
"bottomImg": "",
// 连抽次数 (自动抽奖配置,可选)
"autoCount": "",
// 滚动时长 (自动抽奖配置,可选)
"drawWait": "",
// 间隔 (自动抽奖配置,可选)
"autoInterval": "",
// 结束后自动全屏展示名单 (自动抽奖配置,默认关)
"autoFullscreen": false,
// 不重复抽取 (默认开)
"noRepeat": true,
// 奖项预设列表 (在界面左侧下拉框选择)
"prizes": [
{
"label": "特等奖", // 下拉框显示的名称
"title": "特等奖:MacBook Pro", // 切换后,大屏幕显示的标题
"image": "https://img.icons8.com/color/480/macbook.png" // 底部显示的奖品图片
},
{
"label": "一等奖",
"title": "一等奖:iPhone 15",
"image": "https://img.icons8.com/color/480/iphone.png"
},
{
"label": "二等奖",
"title": "二等奖:Switch 游戏机",
"image": "https://img.icons8.com/color/480/nintendo-switch.png"
}
],
// 参与人员名单
// 支持简单的字符串,也支持对象嵌套(用于在管理面板中按部门分组)
"names": [
"总经理", // 这里的名字没有分组,直接在根目录
{
"label": "技术部", // 这是一个分组
"children": [
"张三",
"李四",
"王五",
"赵六"
]
},
{
"label": "市场部", // 这是另一个分组
"children": [
"Alice",
"Bob",
{
"label": "销售一组", // 支持无限级嵌套分组
"children": ["Sale1", "Sale2"]
}
]
},
"行政-小红",
"财务-小明"
]
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lucky Draw | 幸运抽奖器</title>
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
<style>
:root {
--bg-color: #fff;
--bg-gradient: none;
--panel-bg: #fff;
--text-main: #000;
--primary: #000;
--accent: #f00;
--border: #ccc;
--shadow: none;
--display-glow: none;
--winner-color: #000;
--winner-stroke: transparent;
--winner-shadow: none;
--btn-text: #fff;
}
[data-theme="red"] { --bg-color: #2b0000; --bg-gradient: radial-gradient(circle at center, #800000 0%, #1a0000 100%); --panel-bg: rgba(60, 0, 0, 0.6); --text-main: #fff; --primary: #ffd700; --accent: #ff3333; --border: rgba(255, 215, 0, 0.3); --shadow: 0 10px 40px rgba(0, 0, 0, 0.6); --display-glow: 0 0 30px rgba(255, 215, 0, 0.2); --winner-color: #fff; --winner-stroke: #ffd700; --winner-shadow: 0 0 20px rgba(255, 215, 0, 0.8), 0 0 50px rgba(255, 0, 0, 0.5); --btn-text: #3e0000; }
[data-theme="blue"] { --bg-color: #f0f2f5; --bg-gradient: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); --panel-bg: rgba(255, 255, 255, 0.75); --text-main: #2c3e50; --primary: #2980b9; --accent: #e74c3c; --border: rgba(0, 0, 0, 0.1); --shadow: 0 4px 15px rgba(0,0,0,0.1); --display-glow: none; --winner-color: #d35400; --winner-stroke: #fff; --winner-shadow: 0 5px 15px rgba(0,0,0,0.1); --btn-text: #fff; }
[data-theme="green"] { --bg-color: #e8f5e9; --bg-gradient: linear-gradient(to bottom, #e8f5e9, #c8e6c9); --panel-bg: rgba(255, 255, 255, 0.65); --text-main: #2e7d32; --primary: #43a047; --accent: #d84315; --border: rgba(67, 160, 71, 0.2); --shadow: 0 5px 15px rgba(46, 125, 50, 0.1); --display-glow: none; --winner-color: #1b5e20; --winner-stroke: #fff; --winner-shadow: 0 5px 15px rgba(0,0,0,0.2); --btn-text: #fff; }
[data-theme="cyber"] { --bg-color: #050508; --bg-gradient: radial-gradient(circle at center, #1a1a2e 0%, #000 100%); --panel-bg: rgba(20, 20, 30, 0.7); --text-main: #fff; --primary: #00f3ff; --accent: #ff0055; --border: rgba(0, 243, 255, 0.3); --shadow: 0 0 20px rgba(0, 243, 255, 0.1); --display-glow: 0 0 30px var(--primary); --winner-color: #fff; --winner-stroke: #00f3ff; --winner-shadow: 0 0 30px #00f3ff, 0 0 60px #ff00ff; --btn-text: #000; }
* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; transition: background 0.3s, color 0.3s, border 0.3s; }
body {
background: var(--bg-color);
background-image: var(--bg-gradient);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
color: var(--text-main);
height: 100vh;
overflow: hidden;
display: flex; flex-direction: column;
}
.main-layout {
flex: 1; display: grid; grid-template-columns: 3fr 1fr; gap: 20px; padding: 20px;
height: calc(100vh - 50px); overflow: hidden;
}
.stage-area {
background: var(--panel-bg); backdrop-filter: blur(12px); border: 1px solid var(--border);
border-radius: 20px; display: flex; flex-direction: column; align-items: center;
justify-content: space-between; position: relative; box-shadow: var(--shadow);
overflow: hidden; padding: 20px;
}
.stage-title {
text-align: center; font-size: 6.0rem; color: var(--primary); opacity: 0.9;
letter-spacing: 2px; font-weight: 900; text-transform: uppercase;
text-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-top: 10px; flex-shrink: 0;
}
.display-text {
flex: 1; display: flex; align-items: center; justify-content: center;
font-size: 11vw; font-weight: 800; color: var(--text-main); text-shadow: var(--display-glow);
text-align: center; line-height: 1.1; word-break: break-word; z-index: 2; margin: 0;
padding-bottom: 12vh; transition: padding 0.3s ease;
}
.stage-area.has-img .display-text { padding-bottom: 0; }
.display-text.animate { transform: scale(1.05); opacity: 0.9; }
.display-text.winner {
color: var(--winner-color); -webkit-text-stroke: 6px var(--winner-stroke); paint-order: stroke fill;
text-shadow: var(--winner-shadow); transform: scale(1.2); transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.title-img-display {
height: 480px; max-width: 80%; object-fit: contain;
filter: drop-shadow(0 4px 6px rgba(0,0,0,0.3)); display: none; margin-bottom: 20px; flex-shrink: 0;
}
.sidebar { display: flex; flex-direction: column; gap: 15px; height: 100%; overflow: hidden; min-width: 300px; }
.panel {
background: var(--panel-bg); backdrop-filter: blur(12px); border: 1px solid var(--border);
border-radius: 16px; padding: 15px; display: flex; flex-direction: column; box-shadow: var(--shadow);
}
.history-panel { flex: 1; min-height: 0; }
.panel-header {
display: flex; justify-content: space-between; align-items: center;
border-bottom: 1px solid var(--border); padding-bottom: 8px; margin-bottom: 10px; flex-shrink: 0;
}
.panel-title { font-size: 0.95rem; color: var(--text-main); opacity: 0.8; font-weight: 700; }
.btn-icon {
background: transparent; border: 1px solid var(--border); color: var(--text-main);
cursor: pointer; width: 28px; height: 28px; border-radius: 6px;
display: flex; align-items: center; justify-content: center; opacity: 0.7;
}
.btn-icon:hover { opacity: 1; background: var(--border); }
.btn-icon.danger:hover { color: #fff; background: #ff3333; border-color: #ff3333; }
.history-list {
flex: 1; overflow-y: auto; overflow-x: hidden; padding-right: 4px; display: flex; flex-direction: column; gap: 6px;
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.history-list::-webkit-scrollbar { width: 4px; }
.history-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.history-item {
background: rgba(255,255,255,0.05); border: 1px solid transparent; padding: 0; border-radius: 8px;
border-left: 4px solid var(--border); display: flex; justify-content: space-between; align-items: center;
cursor: pointer; position: relative; height: 44px;
}
.history-item:hover { background: var(--border); }
.history-item:first-child { border-left-color: var(--primary); background: linear-gradient(90deg, rgba(255,255,255,0.1) 0%, transparent 100%); }
.history-content { display: flex; justify-content: space-between; align-items: center; flex: 1; height: 100%; padding: 0 12px; }
.history-index { font-size: 0.9rem; opacity: 0.6; font-family: monospace; min-width: 40px; }
.history-val { font-weight: 700; font-size: 1.1rem; color: var(--text-main); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px; text-align: right; }
.btn-delete-item {
background: transparent; border: none; color: var(--accent); font-size: 1.2rem;
width: 30px; height: 100%; cursor: pointer; display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity 0.2s; margin-right: 5px;
}
.history-item:hover .btn-delete-item { opacity: 1; pointer-events: auto; }
.btn-delete-item:hover { transform: scale(1.2); font-weight: bold; }
.controls-panel { flex-shrink: 0; position: relative; padding-top: 15px; }
/* JSON Mode Visibility Rules */
.controls-panel.json-mode .manual-settings { display: none !important; }
.controls-panel.json-mode .json-indicator { display: flex !important; }
/* In JSON mode, hide the title/upload row and external config row, but keep advanced open */
.controls-panel.json-mode .title-controls-row,
.controls-panel.json-mode .config-import-row { display: none !important; }
.json-indicator {
display: none; background: rgba(0,0,0,0.05); border: 1px solid var(--border);
padding: 8px 12px; border-radius: 10px; margin-bottom: 12px;
align-items: center; gap: 10px; flex-wrap: wrap;
}
.json-tag { font-size: 0.75rem; font-weight: 800; color: var(--primary); white-space: nowrap; opacity: 0.8; }
.preset-select {
flex: 1; padding: 6px 10px; border-radius: 6px; border: 1px solid rgba(0,0,0,0.2);
background: #fff; font-weight: bold; font-size: 0.9rem; color: #000;
outline: none; min-width: 0;
}
.preset-select option { background: #fff; color: #000; }
.btn-exit-json, .btn-manage-json {
padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border);
background: rgba(255,255,255,0.8); color: var(--text-main); font-size: 0.8rem;
cursor: pointer; font-weight: bold; flex-shrink: 0; transition: all 0.2s;
}
.btn-exit-json:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn-manage-json:hover { background: var(--primary); color: #fff; border-color: var(--primary); }
.advanced-settings-toggle {
width: 100%; padding: 8px; margin-bottom: 10px; border: 1px dashed var(--border);
background: rgba(255,255,255,0.05); color: var(--text-main); font-size: 0.85rem;
cursor: pointer; border-radius: 8px; display: flex; align-items: center; justify-content: center; gap: 5px; opacity: 0.8;
}
.advanced-settings-toggle:hover { background: rgba(255,255,255,0.1); opacity: 1; }
#advanced-wrapper { display: none; margin-bottom: 15px; padding: 10px; background: rgba(0,0,0,0.03); border-radius: 8px; }
#advanced-wrapper.open { display: block; animation: slideDown 0.2s ease-out; }
@keyframes slideDown { from {opacity:0; transform:translateY(-10px);} to {opacity:1; transform:translateY(0);} }
.config-import-row { display: flex; gap: 5px; margin-bottom: 10px; }
.config-input {
flex: 1; padding: 6px; border-radius: 6px; border: 1px solid var(--border);
background: rgba(255,255,255,0.1); color: var(--text-main); outline: none; font-size: 0.8rem;
}
.theme-select {
width: 100%; margin-bottom: 8px; padding: 8px; border-radius: 8px;
border: 1px solid var(--border); background: rgba(255,255,255,0.2);
color: var(--text-main); outline: none; cursor: pointer; font-weight: bold;
}
.theme-select option { color: #000; background: #fff; }
.title-controls-row { display: flex; gap: 5px; margin-bottom: 12px; }
.title-input {
flex: 1; padding: 8px; border-radius: 8px; border: 1px solid var(--border);
background: rgba(255,255,255,0.1); color: var(--text-main); outline: none; font-weight: bold; font-size: 0.9rem;
}
.bg-controls { display: flex; gap: 5px; margin-bottom: 12px; }
.btn-small {
flex: 1; padding: 6px; font-size: 0.85rem; border: 1px solid var(--border);
background: rgba(255,255,255,0.1); color: var(--text-main); border-radius: 6px; cursor: pointer;
}
.btn-small:hover { background: var(--border); }
.tabs { display: flex; gap: 5px; margin-bottom: 10px; background: var(--border); padding: 4px; border-radius: 8px; }
.tab {
flex: 1; text-align: center; padding: 6px; cursor: pointer; border-radius: 6px;
font-size: 0.85rem; color: var(--text-main); opacity: 0.6; font-weight: 600;
}
.tab.active { background: var(--bg-color); opacity: 1; color: var(--text-main); }
.input-row { display: flex; gap: 8px; margin-bottom: 10px; align-items: center; }
input[type="number"], textarea {
background: rgba(255,255,255,0.1); border: 1px solid var(--border);
color: var(--text-main); padding: 8px; border-radius: 6px; width: 100%; outline: none;
text-align: center; font-size: 1rem; font-weight: bold;
}
input:focus, textarea:focus { border-color: var(--primary); background: var(--bg-color); }
.main-btn {
width: 100%; padding: 16px; font-size: 1.3rem; font-weight: 800; border: none;
border-radius: 12px; cursor: pointer; margin-top: 15px; letter-spacing: 1px;
color: var(--btn-text); transition: transform 0.1s, box-shadow 0.2s;
}
.btn-start { background: var(--primary); box-shadow: 0 4px 15px var(--border); }
.btn-start:hover { transform: translateY(-2px); filter: brightness(1.2); }
.btn-stop { background: var(--accent); display: none; color: #fff; }
footer {
min-height: 40px; padding: 10px; display: flex; justify-content: center; align-items: center;
flex-wrap: wrap; gap: 15px; font-size: 0.75rem; color: var(--text-main); opacity: 0.6; flex-shrink: 0;
background: rgba(0,0,0,0.1);
}
footer a { color: inherit; text-decoration: none; border-bottom: 1px dashed currentColor; }
footer a:hover { color: var(--primary); border-bottom-style: solid; }
.footer-line { display: flex; gap: 10px; align-items: center; }
.divider { opacity: 0.5; }
.modal {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.92); z-index: 999;
flex-direction: column; justify-content: center; align-items: center;
backdrop-filter: blur(15px);
}
.modal-content-single {
font-size: 12vw; font-weight: 900; color: var(--winner-color);
-webkit-text-stroke: 4px var(--winner-stroke); paint-order: stroke fill;
text-shadow: var(--winner-shadow); text-align: center; padding: 20px; animation: zoomIn 0.3s;
}
.modal-full-layout {
display: flex; flex-direction: column; align-items: center; justify-content: space-between;
width: 100%; height: 100%; padding: 20px 40px; position: relative; box-sizing: border-box;
background-size: cover; background-position: center; background-repeat: no-repeat;
}
.modal-full-title {
font-size: 6.0rem; color: var(--primary); font-weight: 900; text-transform: uppercase;
text-shadow: 0 2px 10px rgba(255,255,255,0.2); margin-bottom: 10px; flex-shrink: 0; text-align: center;
}
.modal-full-list {
flex: 1; width: 100%; overflow-y: auto; display: flex; flex-wrap: wrap;
justify-content: center; align-content: center; gap: 25px; padding: 20px;
scrollbar-width: thin;
}
.modal-full-list span {
padding: 10px 30px; background: var(--panel-bg); color: var(--winner-color);
border: 2px solid var(--border); border-radius: 60px; font-weight: 900; font-size: 5.5rem;
line-height: 1.1; box-shadow: 0 5px 20px rgba(0,0,0,0.3); text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}
.modal-full-img {
height: 530px; max-width: 80%; object-fit: contain; margin-top: 10px; flex-shrink: 1;
filter: drop-shadow(0 4px 6px rgba(0,0,0,0.5));
}
.no-close { pointer-events: auto; }
.delete-btn-modal {
position: absolute; top: 30px; left: 30px; padding: 8px 16px;
background: transparent; border: 1px solid #fff; color: #fff; border-radius: 6px;
cursor: pointer; font-weight: bold; display: none;
}
.delete-btn-modal:hover { background: var(--accent); border-color: var(--accent); }
.pool-manager-box, .prize-manager-box {
background: var(--panel-bg); width: 90%; max-width: 600px; max-height: 80vh;
border-radius: 12px; display: flex; flex-direction: column; border: 1px solid var(--border);
box-shadow: 0 10px 30px rgba(0,0,0,0.3); color: var(--text-main);
}
.pm-header { padding: 15px; border-bottom: 1px solid var(--border); display:flex; justify-content:space-between; align-items:center;}
.pm-body { flex: 1; overflow-y: auto; padding: 10px; }
.pm-item {
display: flex; align-items: center; justify-content: space-between;
padding: 8px; border-bottom: 1px solid rgba(0,0,0,0.05); margin-left: 20px;
}
.pm-name { font-weight: bold; font-size: 1.1rem; }
.pm-switch {
position: relative; display: inline-block; width: 40px; height: 22px;
}
.pm-switch input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
background-color: #ccc; transition: .4s; border-radius: 22px;
}
.slider:before {
position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px;
background-color: white; transition: .4s; border-radius: 50%;
}
input:checked + .slider { background-color: var(--primary); }
input:checked + .slider:before { transform: translateX(18px); }
details.pm-group { margin-bottom: 5px; border: 1px solid rgba(0,0,0,0.05); border-radius: 8px; overflow: hidden; }
details.pm-group > summary {
padding: 10px; background: rgba(0,0,0,0.03); cursor: pointer; font-weight: bold;
display: flex; justify-content: space-between; align-items: center;
}
details.pm-group > summary:hover { background: rgba(0,0,0,0.06); }
details.pm-group[open] > summary { border-bottom: 1px solid rgba(0,0,0,0.05); }
.pm-group-content { padding-left: 10px; }
.prize-item {
padding: 12px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px;
background: rgba(255,255,255,0.05); display: flex; justify-content: space-between; align-items: center;
}
.prize-info h4 { margin: 0 0 4px 0; font-size: 1rem; }
.prize-info p { margin: 0; font-size: 0.8rem; opacity: 0.7; }
.prize-edit-form {
padding: 15px; background: rgba(0,0,0,0.03); border-bottom: 1px solid var(--border);
display: flex; flex-direction: column; gap: 8px;
}
.form-row { display: flex; gap: 8px; }
.confirm-box {
background: var(--panel-bg); padding: 30px; border-radius: 16px; text-align: center;
border: 1px solid var(--border); color: var(--text-main); max-width: 400px;
}
.confirm-btns { display: flex; gap: 10px; margin-top: 20px; justify-content: center; }
.btn-confirm { padding: 10px 20px; border-radius: 8px; border: none; cursor: pointer; font-weight: bold; }
.btn-confirm.yes { background: var(--accent); color: #fff; }
.btn-confirm.soft { background: var(--primary); color: #fff; }
.btn-confirm.no { background: transparent; border: 1px solid var(--border); color: var(--text-main); }
.section-label { font-size:0.75rem; font-weight:bold; opacity:0.7; margin: 10px 0 5px 0; display:block; }
@keyframes zoomIn { from { transform: scale(0.5); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@media (max-width: 1024px) {
body { overflow-y: auto; height: auto; }
.main-layout { display: flex; flex-direction: column; height: auto; overflow: visible; padding-bottom: 20px; }
.stage-area { order: 1; min-height: 400px; margin-bottom: 20px; }
.sidebar { order: 2; min-width: auto; height: auto; display: flex; flex-direction: column; overflow: visible; }
.controls-panel { order: 1; margin-bottom: 20px; }
.history-panel { order: 2; height: 400px; min-height: 400px; }
.stage-title { font-size: 2.2rem; margin-top: 10px; }
.title-img-display { height: 100px; margin-bottom: 10px; }
.display-text { font-size: 15vw; padding-bottom: 4vh; }
.modal-full-layout { padding: 10px; }
.modal-full-title { font-size: 3rem; }
.modal-full-list { align-content: flex-start; }
.modal-full-list span { font-size: 2.5rem; padding: 5px 15px; }
.modal-full-img { height: 150px; }
footer { flex-direction: column; gap: 5px; text-align: center; }
.desktop-divider { display: none; }
}
</style>
</head>
<body data-theme="red">
<div class="main-layout">
<section class="stage-area" id="stage-area">
<div class="stage-title" id="main-title">LUCKY DRAW</div>
<div class="display-text" id="display">READY</div>
<img id="title-img-display" class="title-img-display" src="" alt="Logo">
</section>
<section class="sidebar">
<div class="panel history-panel">
<div class="panel-header">
<span class="panel-title">中奖名单</span>
<div style="display:flex; gap:5px;">
<button class="btn-icon" onclick="copyList(this)" title="复制名单">📋</button>
<button class="btn-icon danger" onclick="confirmClearHistory()" title="清空全部">🗑</button>
<button class="btn-icon" onclick="showAllModal()" title="全屏">⛶</button>
</div>
</div>
<div class="history-list" id="history-container"></div>
</div>
<div class="panel controls-panel" id="controls-panel">
<div class="json-indicator" id="json-indicator">
<div class="json-tag">⚡ 已加载外部配置</div>
<select id="preset-selector" class="preset-select" style="display:none;" onchange="switchPreset(this.value)">
</select>
<button class="btn-manage-json" onclick="openPrizeManager()" title="管理奖项预设">⚙️</button>
<button class="btn-exit-json" onclick="exitJsonMode()" title="退出JSON配置模式">✕</button>
</div>
<div class="tabs manual-settings">
<div class="tab active" id="tab-num" onclick="switchTab('number')">数字模式</div>
<div class="tab" id="tab-name" onclick="switchTab('name')">名单模式</div>
</div>
<div id="settings-num" class="manual-settings">
<div class="input-row">
<input type="number" id="min-val" placeholder="Min" value="1">
<span style="opacity:0.5">-</span>
<input type="number" id="max-val" placeholder="Max" value="100">
</div>
</div>
<div id="settings-name" class="manual-settings" style="display: none;">
<textarea id="name-list" placeholder="粘贴名单...&#10;张三&#10;李四"></textarea>
</div>
<div class="status-bar">
<label style="display:flex;align-items:center;gap:5px;cursor:pointer;">
<input type="checkbox" id="no-repeat" checked> 不重复抽取
</label>
<span id="pool-info" style="font-weight:bold;">池内: 0</span>
</div>
<div class="advanced-settings-toggle" onclick="toggleAdvancedSettings()">
⚙️ 高级设置 <span id="adv-arrow">▼</span>
</div>
<div id="advanced-wrapper">
<div style="display: flex; justify-content: space-between; align-items: center; margin: 10px 0 5px 0;">
<span class="section-label" style="margin: 0; flex: 1;">自动抽奖配置 (留空则为手动)</span>
<button class="btn-small" onclick="resetAutoSettings()" style="padding: 2px 12px; width: auto; flex: 0 0 auto;">手动模式</button>
</div>
<div class="input-row">
<input type="number" id="auto-count" placeholder="连抽次数" title="留空默认1次">
<input type="number" id="draw-wait" placeholder="滚动时长" title="滚动多久后自动停, 留空不自动停">
<input type="number" id="auto-interval" placeholder="间隔" title="连抽时的间隔等待时间">
</div>
<div style="margin-bottom: 12px;">
<label style="cursor:pointer; display:flex; align-items:center; gap:5px; font-size: 0.85rem; opacity:0.9;">
<input type="checkbox" id="auto-fullscreen"> 结束后自动全屏展示名单
</label>
</div>
<span class="section-label">主题与背景</span>
<select class="theme-select" id="theme-selector" onchange="changeTheme(this.value)">
<option value="red">🧧 盛世红金 (Deep Red)</option>
<option value="blue">🌊 清新蓝调 (Simple Blue)</option>
<option value="green">🍃 护眼森系 (Zen Green)</option>
<option value="cyber">🌃 赛博霓虹 (Cyberpunk)</option>
</select>
<div class="title-controls-row">
<input type="text" class="title-input" id="title-input" placeholder="设置奖项标题" oninput="updateTitle(this.value)">
<input type="file" id="title-img-upload" accept="image/*" style="display:none" onchange="handleTitleImg(event)">
<button class="btn-icon" onclick="document.getElementById('title-img-upload').click()" title="上传底部大图">📷</button>
<button class="btn-icon danger" onclick="clearTitleImg()" title="清除图片">×</button>
</div>
<div class="bg-controls">
<input type="file" id="bg-file-input" accept="image/*" style="display:none" onchange="handleBgUpload(event)">
<button class="btn-small" onclick="document.getElementById('bg-file-input').click()">🖼 主背景</button>
<input type="file" id="fs-bg-file-input" accept="image/*" style="display:none" onchange="handleFsBgUpload(event)">
<button class="btn-small" onclick="document.getElementById('fs-bg-file-input').click()">🖼 全屏背景</button>
<button class="btn-small" onclick="clearCustomBg()" style="flex:0 0 30px;">✕</button>
</div>
<span class="section-label config-import-row">外部配置</span>
<div class="config-import-row">
<input type="text" class="config-input" id="json-url-input" placeholder="输入JSON配置网址...">
<button class="btn-small" onclick="loadJsonFromUrl()" style="flex:0 0 40px;">加载</button>
<input type="file" id="json-file-upload" accept=".json" style="display:none" onchange="handleJsonUpload(event)">
<button class="btn-small" onclick="document.getElementById('json-file-upload').click()" style="flex:0 0 40px;">📂</button>
</div>
</div>
<div class="json-indicator" id="json-pool-btn-area" style="display:none; justify-content: center; background:transparent; border:none; padding:0;">
<button class="btn-small" onclick="openPoolManager()" style="width:100%; padding:10px; font-weight:bold;">👥 管理本次抽奖名单</button>
</div>
<button class="main-btn btn-start" id="btn-start" onclick="startDraw()">开始抽奖</button>
<button class="main-btn btn-stop" id="btn-stop" onclick="stopDraw()">停 止</button>
</div>
</section>
</div>
<footer>
<div class="footer-line">
<span>Lucky Draw V2.3 - 251218</span>
</div>
<span class="divider desktop-divider">|</span>
<div class="footer-line">
<span>❤ From baka & Gemini</span>
<span class="divider">|</span>
<a href="https://gist.github.com/liuran001/6ac642b14aec326feaf4f5a73c6f6692" target="_blank">开源地址</a>
</div>
</footer>
<div class="modal" id="modal" onclick="closeModal(event)">
<button class="delete-btn-modal" id="modal-del-btn">🗑 删除此条</button>
<div id="modal-content"></div>
</div>
<div class="modal" id="confirm-modal">
<div class="confirm-box">
<h3>清空历史记录</h3>
<p style="margin:15px 0; opacity:0.8;">您希望如何处理当前已中奖的人员?</p>
<div class="confirm-btns">
<button class="btn-confirm no" onclick="closeConfirmModal()">取消</button>
<button class="btn-confirm soft" onclick="performClear(false)">仅清空记录</button>
<button class="btn-confirm yes" onclick="performClear(true)">清空并移除中奖者</button>
</div>
</div>
</div>
<div class="modal" id="pool-modal">
<div class="pool-manager-box no-close" onclick="event.stopPropagation()">
<div class="pm-header">
<h3>名单管理</h3>
<button class="btn-icon" onclick="document.getElementById('pool-modal').style.display='none'">×</button>
</div>
<div style="padding:10px; border-bottom:1px solid #eee; display:flex; gap:5px;">
<input type="text" id="temp-add-name" placeholder="临时添加人员..." class="config-input">
<button class="btn-small" onclick="addTempPerson()" style="flex:0 0 60px;">添加</button>
</div>
<div class="pm-body" id="pm-list"></div>
</div>
</div>
<div class="modal" id="prize-modal">
<div class="prize-manager-box no-close" onclick="event.stopPropagation()">
<div class="pm-header">
<h3>奖项预设管理</h3>
<button class="btn-icon" onclick="document.getElementById('prize-modal').style.display='none'">×</button>
</div>
<div class="prize-edit-form">
<div class="form-row">
<input type="text" id="prize-edit-label" placeholder="下拉显示名称 (如: 一等奖)" class="config-input">
<input type="text" id="prize-edit-title" placeholder="大屏显示标题" class="config-input">
</div>
<div class="form-row">
<input type="text" id="prize-edit-img" placeholder="底部图片链接..." class="config-input">
<input type="file" id="prize-img-upload" accept="image/*" style="display:none" onchange="handlePrizeImgUpload(event)">
<button class="btn-small" onclick="document.getElementById('prize-img-upload').click()" title="上传本地图片" style="flex:0 0 40px;">📷</button>
<button class="btn-small" onclick="savePrizeItem()" style="flex:0 0 60px; background:var(--primary); color:#fff; border:none;">保存</button>
</div>
<input type="hidden" id="prize-edit-index" value="-1"> </div>
<div class="pm-body" id="prize-list"></div>
</div>
</div>
<script>
let state = {
configMode: 'local',
mode: 'number',
pool: [],
history: [],
participants: [], // { name, active, isTemp, id, groupPath: [] }
presets: [], // Array of { label, title, image }
currentPresetIndex: -1,
isRunning: false,
timer: null,
theme: 'red',
titleText: 'LUCKY DRAW',
fullScreenBg: '',
// Auto draw state
autoRemaining: 0,
autoStopTimer: null,
nextDrawTimer: null,
isAutoSequence: false
};
const els = {
stageArea: document.getElementById('stage-area'),
display: document.getElementById('display'),
mainTitle: document.getElementById('main-title'),
titleInput: document.getElementById('title-input'),
historyList: document.getElementById('history-container'),
poolInfo: document.getElementById('pool-info'),
btnStart: document.getElementById('btn-start'),
btnStop: document.getElementById('btn-stop'),
tabNum: document.getElementById('tab-num'),
tabName: document.getElementById('tab-name'),
setNum: document.getElementById('settings-num'),
setName: document.getElementById('settings-name'),
modal: document.getElementById('modal'),
modalContent: document.getElementById('modal-content'),
modalDelBtn: document.getElementById('modal-del-btn'),
minVal: document.getElementById('min-val'),
maxVal: document.getElementById('max-val'),
nameList: document.getElementById('name-list'),
noRepeat: document.getElementById('no-repeat'),
themeSelector: document.getElementById('theme-selector'),
titleImgDisplay: document.getElementById('title-img-display'),
controlsPanel: document.getElementById('controls-panel'),
jsonIndicator: document.getElementById('json-indicator'),
jsonPoolBtn: document.getElementById('json-pool-btn-area'),
presetSelector: document.getElementById('preset-selector'),
pmList: document.getElementById('pm-list'),
confirmModal: document.getElementById('confirm-modal'),
advWrapper: document.getElementById('advanced-wrapper'),
advArrow: document.getElementById('adv-arrow'),
prizeList: document.getElementById('prize-list'),
// Auto inputs
autoCount: document.getElementById('auto-count'),
drawWait: document.getElementById('draw-wait'),
autoInterval: document.getElementById('auto-interval'),
autoFullscreen: document.getElementById('auto-fullscreen') // New Element
};
window.onload = () => {
const params = new URLSearchParams(window.location.search);
const configUrl = params.get('config');
if (configUrl) {
loadJsonFromUrl(configUrl);
} else {
loadLocalState();
}
};
function toggleAdvancedSettings() {
if (els.advWrapper.classList.contains('open')) {
els.advWrapper.classList.remove('open');
els.advArrow.innerHTML = '▼';
} else {
els.advWrapper.classList.add('open');
els.advArrow.innerHTML = '▲';
}
}
function loadLocalState() {
const saved = localStorage.getItem('draw_app_state_v3');
if (saved) {
try {
const data = JSON.parse(saved);
state.history = data.history || [];
state.theme = data.theme || 'red';
state.mode = data.mode || 'number';
state.titleText = data.titleText || 'LUCKY DRAW';
state.fullScreenBg = data.fullScreenBg || '';
els.minVal.value = data.min || 1;
els.maxVal.value = data.max || 100;
els.nameList.value = data.names || '';
els.noRepeat.checked = data.noRepeat ?? true;
// Load Auto Fullscreen setting
els.autoFullscreen.checked = data.autoFullscreen ?? false;
if (data.bgImg) document.body.style.backgroundImage = data.bgImg;
// Auto settings
if (data.autoCount) els.autoCount.value = data.autoCount;
if (data.drawWait) els.drawWait.value = data.drawWait;
if (data.autoInterval) els.autoInterval.value = data.autoInterval;
state.configMode = data.configMode || 'local';
state.participants = data.participants || [];
state.presets = data.presets || [];
state.currentPresetIndex = data.currentPresetIndex !== undefined ? data.currentPresetIndex : -1;
if (state.configMode === 'json') {
renderPresetsUI();
const currentP = state.presets[state.currentPresetIndex];
if (currentP && currentP.image) {
els.titleImgDisplay.src = currentP.image;
els.titleImgDisplay.style.display = 'block';
els.stageArea.classList.add('has-img');
} else if (data.bottomImg) {
els.titleImgDisplay.src = data.bottomImg;
els.titleImgDisplay.style.display = 'block';
els.stageArea.classList.add('has-img');
}
}
} catch (e) {
console.error("Local state parse error", e);
}
}
applyConfigUI();
}
function saveLocalState() {
const data = {
history: state.history,
theme: state.theme,
mode: state.mode,
titleText: state.titleText,
min: els.minVal.value,
max: els.maxVal.value,
names: els.nameList.value,
noRepeat: els.noRepeat.checked,
autoFullscreen: els.autoFullscreen.checked, // Save new setting
bgImg: document.body.style.backgroundImage,
fullScreenBg: state.fullScreenBg,
configMode: state.configMode,
participants: state.participants,
presets: state.presets,
currentPresetIndex: state.currentPresetIndex,
bottomImg: els.titleImgDisplay.style.display !== 'none' ? els.titleImgDisplay.src : '',
// Auto settings
autoCount: els.autoCount.value,
drawWait: els.drawWait.value,
autoInterval: els.autoInterval.value
};
localStorage.setItem('draw_app_state_v3', JSON.stringify(data));
if (!state.isRunning) updatePool();
}
function applyConfigUI() {
changeTheme(state.theme);
updateTitle(state.titleText);
els.themeSelector.value = state.theme;
els.titleInput.value = state.titleText === 'LUCKY DRAW' ? '' : state.titleText;
switchTab(state.mode);
renderHistory();
if (state.configMode === 'json') {
els.controlsPanel.classList.add('json-mode');
els.jsonPoolBtn.style.display = state.mode === 'name' ? 'flex' : 'none';
} else {
els.controlsPanel.classList.remove('json-mode');
els.jsonPoolBtn.style.display = 'none';
els.presetSelector.style.display = 'none';
}
updatePool();
}
async function loadJsonFromUrl(url) {
const targetUrl = url || document.getElementById('json-url-input').value;
if (!targetUrl) return;
try {
const res = await fetch(targetUrl);
const data = await res.json();
applyJsonConfig(data);
} catch (e) {
alert('加载配置失败,请检查链接或格式');
}
}
function handleJsonUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
applyJsonConfig(data);
} catch (err) { alert('JSON格式错误'); }
};
reader.readAsText(file);
}
function parseParticipants(list, groupPath = []) {
let result = [];
list.forEach(item => {
if (typeof item === 'string') {
result.push({
name: item,
active: true,
isTemp: false,
id: 'p_' + Math.random().toString(36).substr(2, 9),
groupPath: [...groupPath]
});
} else if (typeof item === 'object' && item.children) {
const newPath = [...groupPath, item.label || 'Group'];
result = result.concat(parseParticipants(item.children, newPath));
}
});
return result;
}
function applyJsonConfig(data) {
state.configMode = 'json';
state.theme = data.theme || 'red';
state.titleText = data.title || 'LUCKY DRAW';
state.mode = data.mode || 'name';
state.fullScreenBg = data.fullScreenBg ? `url(${data.fullScreenBg})` : '';
// Apply new switches from JSON
if (data.noRepeat !== undefined) els.noRepeat.checked = data.noRepeat;
if (data.autoFullscreen !== undefined) els.autoFullscreen.checked = data.autoFullscreen;
if (data.bgImg) document.body.style.backgroundImage = `url(${data.bgImg})`;
if (data.bottomImg) {
els.titleImgDisplay.src = data.bottomImg;
els.titleImgDisplay.style.display = 'block';
els.stageArea.classList.add('has-img');
} else {
els.titleImgDisplay.style.display = 'none';
els.stageArea.classList.remove('has-img');
}
// Auto Draw Settings from JSON
if (data.autoCount !== undefined) els.autoCount.value = data.autoCount;
if (data.drawWait !== undefined) els.drawWait.value = data.drawWait;
if (data.autoInterval !== undefined) els.autoInterval.value = data.autoInterval;
if (data.names && Array.isArray(data.names)) {
state.participants = parseParticipants(data.names);
}
if (data.min) els.minVal.value = data.min;
if (data.max) els.maxVal.value = data.max;
state.presets = [];
state.currentPresetIndex = -1;
if (data.prizes && Array.isArray(data.prizes)) {
state.presets = data.prizes;
renderPresetsUI();
if (state.presets.length > 0) switchPreset(0);
} else {
els.presetSelector.style.display = 'none';
}
state.history = [];
applyConfigUI();
updatePool();
// Force save to local storage immediately so refresh keeps the new auto settings
saveLocalState();
const url = new URL(window.location);
url.searchParams.delete('config');
window.history.replaceState({}, '', url);
alert('配置加载成功!');
}
function renderPresetsUI() {
els.presetSelector.innerHTML = '';
if (state.presets.length === 0) {
els.presetSelector.style.display = 'none';
return;
}
state.presets.forEach((p, index) => {
const opt = document.createElement('option');
opt.value = index;
opt.innerText = p.label || `奖项 ${index+1}`;
els.presetSelector.appendChild(opt);
});
els.presetSelector.style.display = 'block';
els.presetSelector.value = state.currentPresetIndex;
}
function switchPreset(index) {
index = parseInt(index);
if (index < 0 || index >= state.presets.length) return;
state.currentPresetIndex = index;
const p = state.presets[index];
if (p.title) updateTitle(p.title);
if (p.image) {
els.titleImgDisplay.src = p.image;
els.titleImgDisplay.style.display = 'block';
els.stageArea.classList.add('has-img');
} else {
els.titleImgDisplay.style.display = 'none';
els.stageArea.classList.remove('has-img');
}
els.presetSelector.value = index;
saveLocalState();
}
function exitJsonMode() {
if(confirm('确定退出配置模式?将清除当前的 JSON 配置数据并恢复为普通模式。')) {
state.configMode = 'local';
state.participants = [];
state.presets = [];
els.presetSelector.style.display = 'none';
els.titleImgDisplay.src = '';
els.titleImgDisplay.style.display = 'none';
els.stageArea.classList.remove('has-img');
document.body.style.backgroundImage = '';
state.fullScreenBg = '';
state.currentPresetIndex = -1;
resetAutoSettings();
// Fixed: Explicitly reset title before saving state
state.titleText = 'LUCKY DRAW';
saveLocalState();
loadLocalState();
location.reload();
}
}
// === Prize Manager (V3.6) ===
function openPrizeManager() {
renderPrizeList();
document.getElementById('prize-modal').style.display = 'flex';
document.getElementById('prize-edit-index').value = "-1";
document.getElementById('prize-edit-label').value = "";
document.getElementById('prize-edit-title').value = "";
document.getElementById('prize-edit-img').value = "";
}
function renderPrizeList() {
els.prizeList.innerHTML = '';
state.presets.forEach((p, index) => {
const div = document.createElement('div');
div.className = 'prize-item';
div.innerHTML = `
<div class="prize-info">
<h4>${p.label}</h4>
<p>${p.title}</p>
</div>
<div style="display:flex; gap:5px;">
<button class="btn-small" onclick="editPrizeItem(${index})">编辑</button>
<button class="btn-small" onclick="deletePrizeItem(${index})" style="color:red;border-color:red">删</button>
</div>
`;
els.prizeList.appendChild(div);
});
}
function editPrizeItem(index) {
const p = state.presets[index];
document.getElementById('prize-edit-index').value = index;
document.getElementById('prize-edit-label').value = p.label || "";
document.getElementById('prize-edit-title').value = p.title || "";
document.getElementById('prize-edit-img').value = p.image || "";
}
function handlePrizeImgUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
document.getElementById('prize-edit-img').value = e.target.result;
};
reader.readAsDataURL(file);
}
}
function savePrizeItem() {
const index = parseInt(document.getElementById('prize-edit-index').value);
const label = document.getElementById('prize-edit-label').value.trim();
const title = document.getElementById('prize-edit-title').value.trim();
const image = document.getElementById('prize-edit-img').value.trim();
if (!label || !title) { alert('名称和标题不能为空'); return; }
const newItem = { label, title, image };
if (index === -1) {
state.presets.push(newItem);
} else {
state.presets[index] = newItem;
}
renderPrizeList();
renderPresetsUI();
if (index === state.currentPresetIndex) {
switchPreset(index);
}
if (index === -1 && state.presets.length === 1) {
switchPreset(0);
}
document.getElementById('prize-edit-index').value = "-1";
document.getElementById('prize-edit-label').value = "";
document.getElementById('prize-edit-title').value = "";
document.getElementById('prize-edit-img').value = "";
saveLocalState();
}
function deletePrizeItem(index) {
if (!confirm('确认删除此奖项预设?')) return;
state.presets.splice(index, 1);
renderPrizeList();
renderPresetsUI();
if (state.presets.length === 0) {
switchPreset(-1); // Clear
} else if (index === state.currentPresetIndex) {
switchPreset(0); // Reset to first
} else if (index < state.currentPresetIndex) {
state.currentPresetIndex--; // Shift
}
saveLocalState();
}
// === Pool Manager Renderer ===
function openPoolManager() {
renderPoolManagerTree();
document.getElementById('pool-modal').style.display = 'flex';
}
function renderPoolManagerTree() {
els.pmList.innerHTML = '';
const root = { items: [], groups: {} };
state.participants.forEach(p => {
let current = root;
p.groupPath.forEach(groupName => {
if (!current.groups[groupName]) {
current.groups[groupName] = { items: [], groups: {} };
}
current = current.groups[groupName];
});
current.items.push(p);
});
function renderNode(node, container, level = 0) {
node.items.forEach(p => {
const div = document.createElement('div');
div.className = 'pm-item';
div.style.paddingLeft = (level * 20 + 10) + 'px';
div.innerHTML = `
<span class="pm-name" style="opacity:${p.active?1:0.5}">${p.name} ${p.isTemp?'<small>(临时)</small>':''}</span>
<div style="display:flex; gap:10px; align-items:center;">
${p.isTemp ? `<button class="btn-small" onclick="removeTempPerson('${p.id}')" style="color:red;border-color:red">删</button>` : ''}
<label class="pm-switch">
<input type="checkbox" ${p.active ? 'checked' : ''} onchange="togglePerson('${p.id}')">
<span class="slider"></span>
</label>
</div>
`;
container.appendChild(div);
});
for (const [groupName, groupNode] of Object.entries(node.groups)) {
const details = document.createElement('details');
details.className = 'pm-group';
details.style.marginLeft = (level * 10) + 'px';
details.open = true;
const summary = document.createElement('summary');
summary.innerHTML = `<span>📁 ${groupName}</span>`;
const contentDiv = document.createElement('div');
contentDiv.className = 'pm-group-content';
renderNode(groupNode, contentDiv, level + 1);
details.appendChild(summary);
details.appendChild(contentDiv);
container.appendChild(details);
}
}
renderNode(root, els.pmList);
}
function togglePerson(id) {
const p = state.participants.find(x => x.id === id);
if (p) {
p.active = !p.active;
updatePool();
const nameSpan = document.querySelector(`input[onchange="togglePerson('${id}')"]`).closest('.pm-item').querySelector('.pm-name');
if(nameSpan) nameSpan.style.opacity = p.active ? 1 : 0.5;
saveLocalState();
}
}
function addTempPerson() {
const name = document.getElementById('temp-add-name').value.trim();
if(name) {
state.participants.unshift({
name, active: true, isTemp: true,
id: 'temp_' + Date.now(),
groupPath: []
});
document.getElementById('temp-add-name').value = '';
renderPoolManagerTree();
updatePool();
saveLocalState();
}
}
function removeTempPerson(id) {
const idx = state.participants.findIndex(p => p.id === id);
if (idx > -1) {
state.participants.splice(idx, 1);
renderPoolManagerTree();
updatePool();
saveLocalState();
}
}
// === Standard Functions ===
function updateTitle(val) {
const text = val.trim() || 'LUCKY DRAW';
state.titleText = text;
els.mainTitle.innerText = text;
saveLocalState();
}
function handleTitleImg(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
els.titleImgDisplay.src = e.target.result;
els.titleImgDisplay.style.display = 'block';
els.stageArea.classList.add('has-img');
saveLocalState();
};
reader.readAsDataURL(file);
}
}
function clearTitleImg() {
els.titleImgDisplay.src = '';
els.titleImgDisplay.style.display = 'none';
els.stageArea.classList.remove('has-img');
document.getElementById('title-img-upload').value = '';
saveLocalState();
}
function handleBgUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
document.body.style.backgroundImage = `url(${e.target.result})`;
saveLocalState();
};
reader.readAsDataURL(file);
}
}
function handleFsBgUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
state.fullScreenBg = `url(${e.target.result})`;
saveLocalState();
alert('全屏背景已设置');
};
reader.readAsDataURL(file);
}
}
function clearCustomBg() {
document.body.style.backgroundImage = '';
state.fullScreenBg = '';
document.getElementById('bg-file-input').value = '';
changeTheme(state.theme);
}
function changeTheme(themeName) {
state.theme = themeName;
document.body.setAttribute('data-theme', themeName);
saveLocalState();
}
function switchTab(mode) {
state.mode = mode;
if (mode === 'number') {
els.tabNum.classList.add('active');
els.tabName.classList.remove('active');
els.setNum.style.display = 'block';
els.setName.style.display = 'none';
} else {
els.tabNum.classList.remove('active');
els.tabName.classList.add('active');
els.setNum.style.display = 'none';
els.setName.style.display = 'block';
}
if(state.configMode === 'json') {
els.jsonPoolBtn.style.display = mode === 'name' ? 'flex' : 'none';
}
updatePool();
saveLocalState();
}
function updatePool() {
if (state.isRunning) return;
const noRepeat = els.noRepeat.checked;
if (state.mode === 'number') {
const min = parseInt(els.minVal.value) || 1;
const max = parseInt(els.maxVal.value) || 10;
let temp = [];
for(let i=min; i<=max; i++) temp.push(i);
state.pool = noRepeat ? temp.filter(x => !state.history.includes(x)) : temp;
} else {
if (state.configMode === 'json') {
state.pool = state.participants
.filter(p => p.active)
.map(p => p.name);
if (noRepeat) {
state.pool = state.pool.filter(name => !state.history.includes(name));
}
} else {
const text = els.nameList.value;
let temp = text.split(/[\n,,]/).map(s=>s.trim()).filter(s=>s);
state.pool = noRepeat ? temp.filter(x => !state.history.includes(x)) : temp;
}
}
els.poolInfo.innerText = `池内: ${state.pool.length}`;
els.poolInfo.style.color = state.pool.length === 0 ? 'var(--accent)' : 'var(--text-main)';
}
// Add listener for the new checkbox
[els.minVal, els.maxVal, els.noRepeat, els.nameList, els.autoCount, els.drawWait, els.autoInterval, els.autoFullscreen].forEach(el => {
el.addEventListener(el.tagName === 'INPUT' ? 'change' : 'input', () => { updatePool(); saveLocalState(); });
});
function resetAutoSettings() {
els.autoCount.value = '';
els.drawWait.value = '';
els.autoInterval.value = '';
updatePool();
saveLocalState();
}
function startDraw() {
updatePool();
if (state.pool.length === 0) { alert('池子空了!'); return; }
// Initialize Auto sequence if not already running recursively
if (!state.isRunning) {
const countVal = els.autoCount.value;
const waitVal = els.drawWait.value;
// If both are empty, it's fully manual (default behavior)
// If either is set, we treat it as auto mode
if (countVal || waitVal) {
if (!state.isAutoSequence) {
// First start of an auto sequence
state.autoRemaining = countVal ? parseInt(countVal) : 1;
state.isAutoSequence = true;
}
}
}
state.isRunning = true;
els.btnStart.style.display = 'none';
els.btnStop.style.display = 'block';
els.display.classList.remove('winner');
els.display.classList.add('animate');
let speed = 50;
function loop() {
const r = Math.floor(Math.random() * state.pool.length);
els.display.innerText = state.pool[r];
if (speed > 15) speed -= 0.5;
state.timer = setTimeout(loop, speed);
}
loop();
// Handle Auto Stop Timer
const waitTime = parseFloat(els.drawWait.value);
if (waitTime > 0 && state.isAutoSequence) {
state.autoStopTimer = setTimeout(() => {
stopDraw(true); // true means triggered by auto timer
}, waitTime * 1000);
}
}
function stopDraw(isAutoTriggered = false) {
clearTimeout(state.timer);
clearTimeout(state.autoStopTimer);
state.isRunning = false;
els.btnStart.style.display = 'block';
els.btnStop.style.display = 'none';
els.display.classList.remove('animate');
els.display.classList.add('winner');
const r = Math.floor(Math.random() * state.pool.length);
const winner = state.pool[r];
els.display.innerText = winner;
state.history.unshift(winner);
renderHistory();
saveLocalState();
fireConfetti();
updatePool();
if (!isAutoTriggered) {
// User clicked STOP manually
state.autoRemaining = 0;
state.isAutoSequence = false;
clearTimeout(state.nextDrawTimer);
} else {
// Was stopped automatically
state.autoRemaining--;
if (state.autoRemaining > 0 && state.pool.length > 0) {
els.btnStart.style.display = 'none';
els.btnStop.style.display = 'block';
const interval = parseFloat(els.autoInterval.value) || 1;
state.nextDrawTimer = setTimeout(() => {
startDraw();
}, interval * 1000);
} else {
// Sequence finished
state.isAutoSequence = false;
state.autoRemaining = 0;
// NEW LOGIC: Check if auto fullscreen is enabled
if (els.autoFullscreen.checked) {
const interval = parseFloat(els.autoInterval.value) || 1;
setTimeout(() => {
showAllModal();
}, interval * 1000);
}
}
}
}
function renderHistory() {
els.historyList.innerHTML = '';
state.history.forEach((item, index) => {
const div = document.createElement('div');
div.className = 'history-item';
div.innerHTML = `
<div class="history-content" onclick="showOneModal('${item}', ${index})">
<span class="history-index">#${state.history.length - index}</span>
<span class="history-val">${item}</span>
</div>
<button class="btn-delete-item" onclick="deleteItem(event, ${index})" title="移除此记录">×</button>
`;
els.historyList.appendChild(div);
});
}
function confirmClearHistory() {
if (state.history.length === 0) return;
els.confirmModal.style.display = 'flex';
}
function closeConfirmModal() { els.confirmModal.style.display = 'none'; }
function performClear(removeWinners) {
closeConfirmModal();
if (removeWinners) {
if (state.mode === 'name') {
if (state.configMode === 'local') {
let text = els.nameList.value;
state.history.forEach(winner => {
const regex = new RegExp(`(^|[\\n,,])${winner}(?=$|[\\n,,])`, 'g');
text = text.replace(regex, '');
});
els.nameList.value = text.split(/[\n,,]/).filter(s=>s.trim()).join('\n');
} else {
// JSON模式下从 active 状态移除
state.history.forEach(winner => {
const p = state.participants.find(p => p.name == winner);
if (p) p.active = false;
});
}
}
}
state.history = [];
renderHistory();
saveLocalState();
updatePool();
}
function deleteItem(e, index) {
e.stopPropagation();
if (confirm('确认移除这条记录吗?')) {
state.history.splice(index, 1);
renderHistory();
saveLocalState();
updatePool();
}
}
function deleteFromModal(e, index) {
e.stopPropagation();
if (confirm('确认移除这条记录吗?')) {
state.history.splice(index, 1);
renderHistory();
saveLocalState();
updatePool();
closeModal();
}
}
function showOneModal(text, index) {
els.modal.style.display = 'flex';
els.modalDelBtn.style.display = 'block';
els.modalDelBtn.onclick = (e) => deleteFromModal(e, index);
els.modalContent.className = 'modal-content-single no-close';
els.modalContent.innerHTML = text;
}
function showAllModal() {
if (state.history.length === 0) return;
els.modal.style.display = 'flex';
els.modalDelBtn.style.display = 'none';
els.modalContent.className = 'modal-full-layout';
if (state.fullScreenBg) els.modalContent.style.backgroundImage = state.fullScreenBg;
else els.modalContent.style.backgroundImage = '';
const imgEl = els.titleImgDisplay;
const hasImg = imgEl.style.display === 'block' && imgEl.src.length > 0;
let html = '';
html += `<div class="modal-full-title no-close">${state.titleText}</div>`;
html += '<div class="modal-full-list">';
html += state.history.map(h => `<span class="no-close">${h}</span>`).join('');
html += '</div>';
if (hasImg) {
html += `<img src="${imgEl.src}" class="modal-full-img no-close">`;
}
els.modalContent.innerHTML = html;
}
function closeModal(e) {
if (!e) { els.modal.style.display = 'none'; return; }
if (e.target.closest('.no-close')) return;
els.modal.style.display = 'none';
}
function fireConfetti() {
const rect = els.display.getBoundingClientRect();
const x = (rect.left + rect.width / 2) / window.innerWidth;
const y = (rect.top + rect.height / 2) / window.innerHeight;
let colors = [];
switch(state.theme) {
case 'red': colors = ['#ffd700', '#ff0000', '#ffffff']; break;
case 'green': colors = ['#43a047', '#81c784', '#ffffff']; break;
case 'cyber': colors = ['#00f3ff', '#ff00ff', '#ffff00']; break;
default: colors = ['#3498db', '#2980b9', '#ecf0f1']; break;
}
const defaults = { spread: 360, ticks: 100, gravity: 0.6, decay: 0.94, startVelocity: 30, colors: colors, zIndex: 10000 };
const offset = 0.15;
confetti({ ...defaults, particleCount: 30, origin: { x: x - offset, y: y - offset } });
confetti({ ...defaults, particleCount: 30, origin: { x: x + offset, y: y - offset } });
confetti({ ...defaults, particleCount: 30, origin: { x: x - offset, y: y + offset } });
confetti({ ...defaults, particleCount: 30, origin: { x: x + offset, y: y + offset } });
setTimeout(() => confetti({ ...defaults, particleCount: 80, startVelocity: 45, origin: { x, y } }), 150);
}
function copyList(btn) {
if (!state.history.length) return;
navigator.clipboard.writeText(state.history.join(' ')).then(() => {
const original = btn.innerHTML;
btn.innerHTML = '✔';
btn.style.opacity = '1';
setTimeout(() => {
btn.innerHTML = original;
btn.style.opacity = '';
}, 1500);
});
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment