chore: v1.1.0 - 5项功能优化
This commit is contained in:
780
index.html
780
index.html
@@ -194,12 +194,46 @@
|
||||
}
|
||||
|
||||
.name-list .name-tag {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
margin: 2px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255,255,255,0.06);
|
||||
color: var(--text-secondary);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.name-list .name-tag .delete-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
color: #888;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.name-list .name-tag .delete-btn:hover {
|
||||
color: #ff4444;
|
||||
background: rgba(255, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
.name-list .name-tag .delete-btn:active {
|
||||
transform: scale(0.85);
|
||||
}
|
||||
|
||||
.name-list.disabled .name-tag .delete-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ===== Mode Switch ===== */
|
||||
@@ -499,6 +533,274 @@
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
/* ===== Probability Settings ===== */
|
||||
.probability-hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.probability-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.probability-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.probability-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.probability-row .person-name {
|
||||
flex: 0 0 80px;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.probability-row .weight-input {
|
||||
width: 56px;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.probability-row .weight-input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.weight-quick-btns {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.weight-quick-btns button {
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-card);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.weight-quick-btns button:hover {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.weight-quick-btns button.active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.probability-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.probability-actions button {
|
||||
flex: 1;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.probability-actions button:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.probability-actions button.secondary {
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
}
|
||||
|
||||
.probability-actions button.secondary:hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
/* ===== Winner Popup ===== */
|
||||
.winner-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
z-index: 10001;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.winner-overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.winner-modal {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 20px;
|
||||
padding: 40px 50px;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
|
||||
animation: winnerPopIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
@keyframes winnerPopIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5) translateY(30px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.winner-modal .winner-close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
padding: 4px 8px;
|
||||
border-radius: 50%;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.winner-modal .winner-close:hover {
|
||||
color: var(--accent);
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
.winner-modal .winner-congrats {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #FFD700;
|
||||
margin-bottom: 20px;
|
||||
text-shadow: 0 0 20px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
|
||||
.winner-modal .winner-avatar-large {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 20px;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
text-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
||||
border: 4px solid rgba(255,255,255,0.2);
|
||||
box-shadow: 0 0 30px rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
.winner-modal .winner-name-large {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.winner-modal .winner-label {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ===== Horizontal Scroll (Slot Machine) ===== */
|
||||
.scroll-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 40px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.scroll-container::before,
|
||||
.scroll-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scroll-container::before {
|
||||
left: 0;
|
||||
background: linear-gradient(to right, var(--bg-secondary), transparent);
|
||||
}
|
||||
|
||||
.scroll-container::after {
|
||||
right: 0;
|
||||
background: linear-gradient(to left, var(--bg-secondary), transparent);
|
||||
}
|
||||
|
||||
.scroll-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.scroll-item {
|
||||
flex-shrink: 0;
|
||||
padding: 0 16px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
.scroll-item.active {
|
||||
opacity: 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ===== Scrollbar ===== */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@@ -568,6 +870,9 @@
|
||||
<div id="avatarDisplay" class="avatar-display">
|
||||
<div id="avatarText" class="avatar-text">?</div>
|
||||
</div>
|
||||
<div id="scrollContainer" class="scroll-container hidden">
|
||||
<div id="scrollTrack" class="scroll-track"></div>
|
||||
</div>
|
||||
<div id="rollName" class="roll-name">等待开始</div>
|
||||
</div>
|
||||
|
||||
@@ -603,6 +908,15 @@
|
||||
<div id="mustWinChecklist" class="checklist"></div>
|
||||
<button id="btnConfirmMustWin">确认</button>
|
||||
</div>
|
||||
<div class="backdoor-section">
|
||||
<h3>🎲 概率设置</h3>
|
||||
<p class="probability-hint">权重值 0-100,数值越大被抽中的概率越高。默认权重为 1,设为 0 则不参与抽奖。</p>
|
||||
<div id="probabilityList" class="probability-list"></div>
|
||||
<div class="probability-actions">
|
||||
<button id="btnResetAllWeights" class="secondary">重置全部为 1</button>
|
||||
<button id="btnConfirmProbabilities">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="backdoor-section">
|
||||
<h3>排除名单</h3>
|
||||
<textarea id="excludeInput" placeholder="输入排除名字(逗号分隔)..."></textarea>
|
||||
@@ -613,6 +927,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中奖弹窗(默认隐藏) -->
|
||||
<div id="winnerOverlay" class="winner-overlay hidden">
|
||||
<div class="winner-modal">
|
||||
<button id="winnerClose" class="winner-close">✕</button>
|
||||
<div class="winner-congrats">🎉 恭喜中奖!🎉</div>
|
||||
<div id="winnerAvatarLarge" class="winner-avatar-large">?</div>
|
||||
<div id="winnerNameLarge" class="winner-name-large"></div>
|
||||
<div class="winner-label">本次幸运儿</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -621,7 +946,7 @@
|
||||
|
||||
// ===== 共享状态 =====
|
||||
const state = {
|
||||
pool: [], // 当前抽奖池:[{ id, name, color, avatar }]
|
||||
pool: [], // 当前抽奖池:[{ id, name, color, avatar, weight }]
|
||||
history: [], // 历史记录(内存,页面关闭清空)
|
||||
totalImported: 0, // 累计导入总人数
|
||||
|
||||
@@ -635,6 +960,7 @@
|
||||
enabled: false,
|
||||
mustWinList: [], // 必中名单(名字字符串数组)
|
||||
excludeList: [], // 排除名单(名字字符串数组)
|
||||
probabilities: {}, // { name: weight },权重值 0-100
|
||||
},
|
||||
|
||||
// 动画
|
||||
@@ -759,7 +1085,8 @@
|
||||
id: Utils.generateId(),
|
||||
name: name,
|
||||
color: Utils.hashColor(name),
|
||||
avatar: Utils.getAvatarText(name)
|
||||
avatar: Utils.getAvatarText(name),
|
||||
weight: 1 // v1.1.0: 默认权重 1
|
||||
};
|
||||
},
|
||||
|
||||
@@ -779,28 +1106,84 @@
|
||||
var container = document.getElementById('nameList');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
// 抽奖中禁用删除
|
||||
if (state.isDrawing) {
|
||||
container.classList.add('disabled');
|
||||
} else {
|
||||
container.classList.remove('disabled');
|
||||
}
|
||||
var fragment = document.createDocumentFragment();
|
||||
for (var i = 0; i < state.pool.length; i++) {
|
||||
var person = state.pool[i];
|
||||
var span = document.createElement('span');
|
||||
span.className = 'name-tag';
|
||||
span.textContent = state.pool[i].name;
|
||||
|
||||
var nameText = document.createTextNode(person.name);
|
||||
span.appendChild(nameText);
|
||||
|
||||
// 删除按钮
|
||||
var delBtn = document.createElement('button');
|
||||
delBtn.className = 'delete-btn';
|
||||
delBtn.textContent = '×';
|
||||
delBtn.title = '删除 ' + person.name;
|
||||
delBtn.setAttribute('data-name', person.name);
|
||||
if (state.isDrawing) {
|
||||
delBtn.disabled = true;
|
||||
}
|
||||
span.appendChild(delBtn);
|
||||
|
||||
fragment.appendChild(span);
|
||||
}
|
||||
container.appendChild(fragment);
|
||||
},
|
||||
|
||||
/** 按名字删除人员(v1.1.0 新增) */
|
||||
removeByName: function(name) {
|
||||
if (state.isDrawing) return; // 抽奖中禁止删除
|
||||
|
||||
// 从池中移除
|
||||
var idx = -1;
|
||||
for (var i = 0; i < state.pool.length; i++) {
|
||||
if (state.pool[i].name === name) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx === -1) return;
|
||||
state.pool.splice(idx, 1);
|
||||
|
||||
// 同步清理必中名单
|
||||
var mwIdx = state.backdoor.mustWinList.indexOf(name);
|
||||
if (mwIdx !== -1) state.backdoor.mustWinList.splice(mwIdx, 1);
|
||||
|
||||
// 同步清理排除名单
|
||||
var exIdx = state.backdoor.excludeList.indexOf(name);
|
||||
if (exIdx !== -1) state.backdoor.excludeList.splice(exIdx, 1);
|
||||
|
||||
// 同步清理概率配置
|
||||
delete state.backdoor.probabilities[name];
|
||||
|
||||
this.updatePoolCount();
|
||||
this.renderNameList();
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 抽奖引擎模块 =====
|
||||
const LotteryEngine = {
|
||||
/** 根据模式和后门设置,从池中选出中奖人 */
|
||||
/** 根据模式和后门设置,从池中选出中奖人(v1.1.0 加权随机) */
|
||||
pickOne: function() {
|
||||
// 1. 候选池 = state.pool 副本
|
||||
var candidates = state.pool.slice();
|
||||
|
||||
// 2. 过滤排除名单
|
||||
// 2. 过滤排除名单 + 权重为 0 的人
|
||||
var self = this;
|
||||
candidates = this.applyExclude(candidates);
|
||||
candidates = candidates.filter(function(person) {
|
||||
var w = self.getWeight(person.name);
|
||||
return w > 0;
|
||||
});
|
||||
|
||||
// 3. 若必中名单非空,取交集
|
||||
// 3. 若必中名单非空,取交集(最高优先级,等概率)
|
||||
if (state.backdoor.mustWinList.length > 0) {
|
||||
var intersection = [];
|
||||
for (var i = 0; i < candidates.length; i++) {
|
||||
@@ -812,13 +1195,49 @@
|
||||
}
|
||||
}
|
||||
if (intersection.length > 0) {
|
||||
candidates = intersection;
|
||||
return intersection[Math.floor(Math.random() * intersection.length)];
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 从候选池随机选一人
|
||||
// 4. 加权随机(无必中名单或必中名单不匹配时)
|
||||
if (candidates.length === 0) return null;
|
||||
return candidates[Math.floor(Math.random() * candidates.length)];
|
||||
|
||||
var totalWeight = 0;
|
||||
for (var k = 0; k < candidates.length; k++) {
|
||||
totalWeight += self.getWeight(candidates[k].name);
|
||||
}
|
||||
|
||||
if (totalWeight === 0) {
|
||||
// 兜底:所有人权重都为 0 时等概率
|
||||
return candidates[Math.floor(Math.random() * candidates.length)];
|
||||
}
|
||||
|
||||
// 加权随机选择
|
||||
var r = Math.random() * totalWeight;
|
||||
var cumulative = 0;
|
||||
for (var m = 0; m < candidates.length; m++) {
|
||||
cumulative += self.getWeight(candidates[m].name);
|
||||
if (r < cumulative) {
|
||||
return candidates[m];
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底
|
||||
return candidates[candidates.length - 1];
|
||||
},
|
||||
|
||||
/** 获取某人权重(v1.1.0) */
|
||||
getWeight: function(name) {
|
||||
// 优先从 probabilities 读取,其次 person.weight,默认 1
|
||||
if (state.backdoor.probabilities.hasOwnProperty(name)) {
|
||||
return state.backdoor.probabilities[name];
|
||||
}
|
||||
for (var i = 0; i < state.pool.length; i++) {
|
||||
if (state.pool[i].name === name) {
|
||||
return state.pool[i].weight || 1;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
},
|
||||
|
||||
/** 过滤掉排除名单中的人 */
|
||||
@@ -855,42 +1274,95 @@
|
||||
rAFId: null,
|
||||
lastTickTime: 0,
|
||||
|
||||
/** 名字滚动动画:快速切换 → 3秒后减速 → 停止在 winner */
|
||||
/** 名字滚动动画:水平老虎机效果 → 减速 → 停止在 winner */
|
||||
startRollAnimation: function(winnerPerson, callback) {
|
||||
var self = this;
|
||||
var startTime = performance.now();
|
||||
var duration = 3500; // 总时长 3.5 秒
|
||||
var lastTickTime = 0;
|
||||
var rollNameEl = document.getElementById('rollName');
|
||||
var avatarTextEl = document.getElementById('avatarText');
|
||||
var avatarDisplayEl = document.getElementById('avatarDisplay');
|
||||
var scrollContainerEl = document.getElementById('scrollContainer');
|
||||
var scrollTrackEl = document.getElementById('scrollTrack');
|
||||
|
||||
// 移除 highlight 类
|
||||
// 移除 highlight
|
||||
rollNameEl.classList.remove('highlight');
|
||||
|
||||
// 构建滚动轨道:重复多份名单以产生连续滚动效果
|
||||
var allNames = [];
|
||||
for (var i = 0; i < 8; i++) {
|
||||
for (var j = 0; j < state.pool.length; j++) {
|
||||
allNames.push(state.pool[j]);
|
||||
}
|
||||
}
|
||||
|
||||
// 清空并填充轨道
|
||||
scrollTrackEl.innerHTML = '';
|
||||
scrollTrackEl.style.transform = 'translateX(0px)';
|
||||
for (var k = 0; k < allNames.length; k++) {
|
||||
var item = document.createElement('span');
|
||||
item.className = 'scroll-item';
|
||||
item.textContent = allNames[k].name;
|
||||
scrollTrackEl.appendChild(item);
|
||||
}
|
||||
|
||||
// 显示滚动容器,隐藏静态名字
|
||||
scrollContainerEl.classList.remove('hidden');
|
||||
rollNameEl.textContent = '';
|
||||
|
||||
// 测量轨道总宽度
|
||||
var totalWidth = scrollTrackEl.scrollWidth;
|
||||
var containerWidth = scrollContainerEl.clientWidth;
|
||||
|
||||
// 缓动函数
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
var lastItemIndex = -1;
|
||||
|
||||
function tick(now) {
|
||||
var elapsed = now - startTime;
|
||||
var progress = Math.min(elapsed / duration, 1);
|
||||
var easedProgress = easeOutCubic(progress);
|
||||
|
||||
// 缓动:前 85% 快速切换,后 15% 减速
|
||||
var easeProgress = self.easeOutCubic(progress);
|
||||
var interval = 50 + easeProgress * 400; // 50ms → 450ms
|
||||
// 总位移 = 轨道宽度 - 容器宽度(滚动到末尾附近)
|
||||
var maxScroll = totalWidth - containerWidth;
|
||||
// 最终停在 winner 附近
|
||||
var targetScroll = maxScroll * 0.85;
|
||||
var currentScroll = targetScroll * easedProgress;
|
||||
|
||||
if (elapsed - lastTickTime >= interval) {
|
||||
// 获取当前滚动候选
|
||||
var candidate = self.getRollCandidate(winnerPerson, progress);
|
||||
if (candidate) {
|
||||
rollNameEl.textContent = candidate.name;
|
||||
avatarTextEl.textContent = candidate.avatar;
|
||||
avatarDisplayEl.style.background = candidate.color;
|
||||
scrollTrackEl.style.transform = 'translateX(' + (-currentScroll) + 'px)';
|
||||
|
||||
// 高亮当前可见的中间项
|
||||
var items = scrollTrackEl.querySelectorAll('.scroll-item');
|
||||
var centerOffset = containerWidth / 2 + currentScroll;
|
||||
var itemWidth = 100; // 估算每个名字宽度
|
||||
var currentIndex = Math.floor(centerOffset / itemWidth) % items.length;
|
||||
|
||||
if (currentIndex !== lastItemIndex && currentIndex >= 0 && currentIndex < items.length) {
|
||||
// 清除旧高亮
|
||||
for (var m = 0; m < items.length; m++) {
|
||||
items[m].classList.remove('active');
|
||||
}
|
||||
lastTickTime = elapsed;
|
||||
items[currentIndex].classList.add('active');
|
||||
|
||||
// 更新头像和名字
|
||||
var personIdx = currentIndex % state.pool.length;
|
||||
var currentPerson = state.pool[personIdx];
|
||||
if (currentPerson) {
|
||||
rollNameEl.textContent = currentPerson.name;
|
||||
avatarTextEl.textContent = currentPerson.avatar;
|
||||
avatarDisplayEl.style.background = currentPerson.color;
|
||||
}
|
||||
lastItemIndex = currentIndex;
|
||||
}
|
||||
|
||||
if (progress < 1) {
|
||||
self.rAFId = requestAnimationFrame(tick);
|
||||
} else {
|
||||
// 最终停在 winner,添加 highlight
|
||||
// 最终停在 winner
|
||||
scrollContainerEl.classList.add('hidden');
|
||||
rollNameEl.textContent = winnerPerson.name;
|
||||
avatarTextEl.textContent = winnerPerson.avatar;
|
||||
avatarDisplayEl.style.background = winnerPerson.color;
|
||||
@@ -924,14 +1396,40 @@
|
||||
return state.pool[Math.floor(Math.random() * state.pool.length)];
|
||||
},
|
||||
|
||||
/** Canvas 烟花粒子效果 */
|
||||
/** 显示中奖弹窗(v1.1.0 新增) */
|
||||
showWinnerPopup: function(winnerPerson) {
|
||||
var overlay = document.getElementById('winnerOverlay');
|
||||
var avatarLarge = document.getElementById('winnerAvatarLarge');
|
||||
var nameLarge = document.getElementById('winnerNameLarge');
|
||||
|
||||
avatarLarge.textContent = winnerPerson.avatar;
|
||||
avatarLarge.style.background = winnerPerson.color;
|
||||
nameLarge.textContent = winnerPerson.name;
|
||||
|
||||
overlay.classList.remove('hidden');
|
||||
},
|
||||
|
||||
/** 关闭中奖弹窗 */
|
||||
hideWinnerPopup: function() {
|
||||
var overlay = document.getElementById('winnerOverlay');
|
||||
overlay.classList.add('hidden');
|
||||
},
|
||||
|
||||
/** Canvas 烟花粒子效果(v1.1.0 改进:弹窗周围爆炸 + 5秒 + 更密集) */
|
||||
fireFireworks: function() {
|
||||
var canvas = document.getElementById('fireworksCanvas');
|
||||
var ctx = canvas.getContext('2d');
|
||||
var running = true;
|
||||
var fireworks = [];
|
||||
var spawnTimer = 0;
|
||||
var endTime = performance.now() + 4000; // 持续 4 秒
|
||||
var endTime = performance.now() + 5000; // 持续 5 秒
|
||||
|
||||
// 获取弹窗位置作为爆炸中心
|
||||
var popupEl = document.getElementById('winnerOverlay');
|
||||
var popupRect = popupEl.getBoundingClientRect();
|
||||
var centerX = popupRect.left + popupRect.width / 2;
|
||||
var centerY = popupRect.top + popupRect.height / 2;
|
||||
var spreadRadius = Math.max(popupRect.width, popupRect.height) * 0.8;
|
||||
|
||||
// 调整 canvas 尺寸
|
||||
function resize() {
|
||||
@@ -943,17 +1441,17 @@
|
||||
// 粒子类
|
||||
function Particle(x, y, color) {
|
||||
var angle = Math.random() * Math.PI * 2;
|
||||
var speed = 2 + Math.random() * 6;
|
||||
var speed = 2 + Math.random() * 8;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.vx = Math.cos(angle) * speed;
|
||||
this.vy = Math.sin(angle) * speed;
|
||||
this.color = color;
|
||||
this.alpha = 1;
|
||||
this.size = 2 + Math.random() * 3;
|
||||
this.life = 60 + Math.random() * 40;
|
||||
this.size = 2 + Math.random() * 4;
|
||||
this.life = 80 + Math.random() * 50;
|
||||
this.maxLife = this.life;
|
||||
this.gravity = 0.05;
|
||||
this.gravity = 0.04;
|
||||
this.friction = 0.98;
|
||||
}
|
||||
|
||||
@@ -977,34 +1475,25 @@
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
// 烟花类
|
||||
// 烟花类(v1.1.0:无上升阶段,直接爆炸)
|
||||
function Firework(targetX, targetY, colors) {
|
||||
this.x = targetX;
|
||||
this.y = canvas.height;
|
||||
this.targetX = targetX;
|
||||
this.targetY = targetY;
|
||||
this.colors = colors;
|
||||
this.particles = [];
|
||||
this.state = 'rising';
|
||||
this.speed = 4 + Math.random() * 3;
|
||||
this.state = 'exploding';
|
||||
// 初始爆炸生成更多粒子
|
||||
for (var i = 0; i < 100 + Math.floor(Math.random() * 40); i++) {
|
||||
var color = colors[Math.floor(Math.random() * colors.length)];
|
||||
this.particles.push(new Particle(targetX, targetY, color));
|
||||
}
|
||||
}
|
||||
|
||||
Firework.prototype.update = function() {
|
||||
if (this.state === 'rising') {
|
||||
this.y -= this.speed;
|
||||
if (this.y <= this.targetY) {
|
||||
this.state = 'exploding';
|
||||
// 生成爆炸粒子
|
||||
for (var i = 0; i < 60 + Math.floor(Math.random() * 20); i++) {
|
||||
var color = this.colors[Math.floor(Math.random() * this.colors.length)];
|
||||
this.particles.push(new Particle(this.targetX, this.targetY, color));
|
||||
}
|
||||
}
|
||||
} else if (this.state === 'exploding') {
|
||||
if (this.state === 'exploding') {
|
||||
for (var j = 0; j < this.particles.length; j++) {
|
||||
this.particles[j].update();
|
||||
}
|
||||
// 过滤死亡粒子
|
||||
this.particles = this.particles.filter(function(p) { return p.life > 0; });
|
||||
if (this.particles.length === 0) {
|
||||
this.state = 'dead';
|
||||
@@ -1013,14 +1502,7 @@
|
||||
};
|
||||
|
||||
Firework.prototype.draw = function(ctx) {
|
||||
if (this.state === 'rising') {
|
||||
ctx.save();
|
||||
ctx.fillStyle = this.colors[0];
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
} else if (this.state === 'exploding') {
|
||||
if (this.state === 'exploding') {
|
||||
for (var i = 0; i < this.particles.length; i++) {
|
||||
this.particles[i].draw(ctx);
|
||||
}
|
||||
@@ -1054,11 +1536,17 @@
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 每 200-500ms 生成一朵烟花
|
||||
// 每 150-400ms 生成一朵烟花
|
||||
spawnTimer++;
|
||||
if (spawnTimer > 4 + Math.random() * 6) {
|
||||
var x = canvas.width * (0.2 + Math.random() * 0.6);
|
||||
var y = canvas.height * (0.1 + Math.random() * 0.4);
|
||||
if (spawnTimer > 3 + Math.random() * 5) {
|
||||
// 在弹窗周围随机位置生成爆炸点
|
||||
var angle = Math.random() * Math.PI * 2;
|
||||
var radius = spreadRadius * (0.5 + Math.random() * 1.2);
|
||||
var x = centerX + Math.cos(angle) * radius;
|
||||
var y = centerY + Math.sin(angle) * radius;
|
||||
// 限制在屏幕范围内
|
||||
x = Math.max(50, Math.min(canvas.width - 50, x));
|
||||
y = Math.max(50, Math.min(canvas.height - 50, y));
|
||||
var colors = randomColorPalette();
|
||||
fireworks.push(new Firework(x, y, colors));
|
||||
spawnTimer = 0;
|
||||
@@ -1188,6 +1676,124 @@
|
||||
} else {
|
||||
excludeChecklist.innerHTML = '<span style="color:var(--text-secondary);font-size:12px;">抽奖池为空</span>';
|
||||
}
|
||||
|
||||
// v1.1.0: 渲染概率列表
|
||||
this.renderProbabilityList();
|
||||
},
|
||||
|
||||
/** 渲染概率设置列表(v1.1.0 新增) */
|
||||
renderProbabilityList: function() {
|
||||
var list = document.getElementById('probabilityList');
|
||||
if (!list) return;
|
||||
list.innerHTML = '';
|
||||
|
||||
if (state.pool.length === 0) {
|
||||
list.innerHTML = '<span style="color:var(--text-secondary);font-size:12px;">抽奖池为空</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < state.pool.length; i++) {
|
||||
var person = state.pool[i];
|
||||
var row = document.createElement('div');
|
||||
row.className = 'probability-row';
|
||||
row.setAttribute('data-name', person.name);
|
||||
|
||||
// 获取当前权重
|
||||
var currentWeight = 1;
|
||||
if (state.backdoor.probabilities.hasOwnProperty(person.name)) {
|
||||
currentWeight = state.backdoor.probabilities[person.name];
|
||||
} else if (person.weight) {
|
||||
currentWeight = person.weight;
|
||||
}
|
||||
|
||||
// 人名
|
||||
var nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'person-name';
|
||||
nameSpan.textContent = person.name;
|
||||
|
||||
// 权重输入框
|
||||
var weightInput = document.createElement('input');
|
||||
weightInput.type = 'number';
|
||||
weightInput.className = 'weight-input';
|
||||
weightInput.min = 0;
|
||||
weightInput.max = 100;
|
||||
weightInput.value = currentWeight;
|
||||
weightInput.setAttribute('data-name', person.name);
|
||||
|
||||
// 快捷按钮
|
||||
var quickBtns = document.createElement('div');
|
||||
quickBtns.className = 'weight-quick-btns';
|
||||
var quickValues = [0, 1, 5, 10, 50, 100];
|
||||
for (var q = 0; q < quickValues.length; q++) {
|
||||
var w = quickValues[q];
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.textContent = w;
|
||||
btn.setAttribute('data-weight', w);
|
||||
if (w === currentWeight) {
|
||||
btn.className = 'active';
|
||||
}
|
||||
(function(input, b, wVal) {
|
||||
btn.addEventListener('click', function() {
|
||||
input.value = wVal;
|
||||
var siblings = quickBtns.querySelectorAll('button');
|
||||
for (var s = 0; s < siblings.length; s++) {
|
||||
siblings[s].className = '';
|
||||
}
|
||||
b.className = 'active';
|
||||
});
|
||||
})(weightInput, btn, w);
|
||||
quickBtns.appendChild(btn);
|
||||
}
|
||||
|
||||
// 输入框变化时更新快捷按钮高亮
|
||||
weightInput.addEventListener('input', function() {
|
||||
var v = parseInt(this.value) || 0;
|
||||
var btns = quickBtns.querySelectorAll('button');
|
||||
for (var b = 0; b < btns.length; b++) {
|
||||
btns[b].className = (parseInt(btns[b].getAttribute('data-weight')) === v) ? 'active' : '';
|
||||
}
|
||||
});
|
||||
|
||||
row.appendChild(nameSpan);
|
||||
row.appendChild(weightInput);
|
||||
row.appendChild(quickBtns);
|
||||
list.appendChild(row);
|
||||
}
|
||||
},
|
||||
|
||||
/** 保存概率设置(v1.1.0 新增) */
|
||||
saveProbabilities: function() {
|
||||
var inputs = document.querySelectorAll('.weight-input');
|
||||
var newProbs = {};
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
var input = inputs[i];
|
||||
var name = input.getAttribute('data-name');
|
||||
var weight = Math.max(0, Math.min(100, parseInt(input.value) || 0));
|
||||
if (weight !== 1) {
|
||||
newProbs[name] = weight;
|
||||
}
|
||||
}
|
||||
state.backdoor.probabilities = newProbs;
|
||||
|
||||
// 同步更新 pool 中每个人的 weight
|
||||
for (var j = 0; j < state.pool.length; j++) {
|
||||
var person = state.pool[j];
|
||||
if (newProbs.hasOwnProperty(person.name)) {
|
||||
person.weight = newProbs[person.name];
|
||||
} else {
|
||||
person.weight = 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** 重置所有权重为 1(v1.1.0 新增) */
|
||||
resetAllWeights: function() {
|
||||
state.backdoor.probabilities = {};
|
||||
for (var i = 0; i < state.pool.length; i++) {
|
||||
state.pool[i].weight = 1;
|
||||
}
|
||||
this.renderProbabilityList();
|
||||
},
|
||||
|
||||
/** 确认必中名单 */
|
||||
@@ -1255,7 +1861,12 @@
|
||||
btnCloseBackdoor: document.getElementById('btnCloseBackdoor'),
|
||||
btnConfirmMustWin: document.getElementById('btnConfirmMustWin'),
|
||||
btnConfirmExclude: document.getElementById('btnConfirmExclude'),
|
||||
fireworksCanvas: document.getElementById('fireworksCanvas')
|
||||
fireworksCanvas: document.getElementById('fireworksCanvas'),
|
||||
// v1.1.0 新增
|
||||
btnConfirmProbabilities: document.getElementById('btnConfirmProbabilities'),
|
||||
btnResetAllWeights: document.getElementById('btnResetAllWeights'),
|
||||
winnerOverlay: document.getElementById('winnerOverlay'),
|
||||
winnerClose: document.getElementById('winnerClose')
|
||||
};
|
||||
|
||||
// ===== 事件绑定 =====
|
||||
@@ -1319,6 +1930,39 @@
|
||||
Backdoor.close();
|
||||
}
|
||||
});
|
||||
|
||||
// v1.1.0: 名单删除按钮(事件委托)
|
||||
DOM.nameList.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('.delete-btn');
|
||||
if (btn && !btn.disabled) {
|
||||
var name = btn.getAttribute('data-name');
|
||||
if (name) {
|
||||
NameManager.removeByName(name);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// v1.1.0: 概率设置确认
|
||||
DOM.btnConfirmProbabilities.addEventListener('click', function() {
|
||||
Backdoor.saveProbabilities();
|
||||
});
|
||||
|
||||
// v1.1.0: 重置所有权重
|
||||
DOM.btnResetAllWeights.addEventListener('click', function() {
|
||||
Backdoor.resetAllWeights();
|
||||
});
|
||||
|
||||
// v1.1.0: 关闭中奖弹窗
|
||||
DOM.winnerClose.addEventListener('click', function() {
|
||||
AnimationEngine.hideWinnerPopup();
|
||||
});
|
||||
|
||||
// v1.1.0: 点击遮罩关闭中奖弹窗
|
||||
DOM.winnerOverlay.addEventListener('click', function(e) {
|
||||
if (e.target === DOM.winnerOverlay) {
|
||||
AnimationEngine.hideWinnerPopup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置抽奖按钮的点击/长按事件 */
|
||||
@@ -1389,25 +2033,23 @@
|
||||
state.isDrawing = true;
|
||||
DOM.btnStart.disabled = true;
|
||||
DOM.resultArea.classList.add('hidden');
|
||||
NameManager.renderNameList(); // 禁用删除按钮
|
||||
|
||||
// 执行抽奖逻辑
|
||||
var winner = LotteryEngine.pickOne();
|
||||
if (!winner) {
|
||||
state.isDrawing = false;
|
||||
DOM.btnStart.disabled = false;
|
||||
NameManager.renderNameList();
|
||||
alert('无法选出中奖人,请检查名单或后门设置。');
|
||||
return;
|
||||
}
|
||||
|
||||
// 开始滚动动画
|
||||
AnimationEngine.startRollAnimation(winner, function(finalWinner) {
|
||||
// 动画结束,显示结果
|
||||
// 动画结束
|
||||
state.winner = finalWinner;
|
||||
|
||||
// 显示中奖结果
|
||||
DOM.winnerName.textContent = finalWinner.name;
|
||||
DOM.resultArea.classList.remove('hidden');
|
||||
|
||||
// 根据模式处理
|
||||
if (state.mode === 'single') {
|
||||
LotteryEngine.removeWinner(finalWinner.id);
|
||||
@@ -1418,12 +2060,16 @@
|
||||
// 记录历史
|
||||
HistoryModule.addRecord(finalWinner);
|
||||
|
||||
// 播放烟花
|
||||
// 显示中奖弹窗
|
||||
AnimationEngine.showWinnerPopup(finalWinner);
|
||||
|
||||
// 播放烟花(围绕弹窗)
|
||||
AnimationEngine.fireFireworks();
|
||||
|
||||
// 恢复按钮
|
||||
state.isDrawing = false;
|
||||
DOM.btnStart.disabled = false;
|
||||
NameManager.renderNameList(); // 恢复删除按钮
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user