Merge pull request #215 from ZxwyWebSite/feat-lottery

This commit is contained in:
ElyPrism 2026-07-24 17:08:42 +08:00
parent fda7c9e5b3
commit 9adfd50c58
No known key found for this signature in database

View File

@ -1,30 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="no-referrer" /> <meta name="referrer" content="no-referrer" />
<link rel="icon" href="docs/netease.png" /> <link rel="icon" href="docs/netease.png" />
<title>云小编每日抽奖 - 网易云音乐 API Enhanced</title> <title>云小编每日抽奖 - 网易云音乐 API Enhanced</title>
<style> <style>
:root {
--bg-start: #f3f0ff;
--bg-end: #e8f4fd;
--primary: #7c6cf0;
--primary-light: #ede9fe;
--accent: #fd79a8;
--gold: #fdcb6e;
--text: #2d3436;
--text-light: #636e72;
--card-bg: #ffffff;
--border: #e8e8f0;
--shadow: 0 8px 30px rgba(108, 92, 231, 0.08);
--radius: 20px;
--radius-sm: 12px;
--transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -32,135 +15,269 @@
} }
body { body {
font-family: 'Segoe UI', system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, var(--bg-start) 0%, var(--bg-end) 50%, #fce4ec 100%);
min-height: 100vh; min-height: 100vh;
display: flex; background: #f5f5f5;
align-items: center; padding: 20px;
justify-content: center;
padding: 1.5rem;
color: var(--text);
line-height: 1.5;
} }
.container { .container {
width: 100%;
max-width: 520px; max-width: 520px;
margin: 40px auto;
background: white;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
text-align: center;
}
.subtitle {
font-size: 14px;
color: #666;
margin-bottom: 24px;
text-align: center;
}
.subtitle b {
color: #d63031;
}
/* ---- Section ---- */
.section {
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.section:last-of-type {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.section-header {
display: flex; display: flex;
flex-direction: column; align-items: center;
gap: 1.5rem; justify-content: space-between;
margin-bottom: 12px;
} }
/* 标题编辑卡片 */ .section-title {
.card { font-size: 16px;
background: var(--card-bg); font-weight: 600;
border-radius: var(--radius); color: #333;
padding: 1.8rem 2rem;
box-shadow: var(--shadow);
border: 1px solid var(--border);
transition: var(--transition);
} }
.title-section { /* ---- Form Elements ---- */
text-align: center; .form-group {
margin-bottom: 16px;
text-align: left;
} }
.title-input { label {
display: block;
font-size: 13px;
font-weight: 500;
color: #555;
margin-bottom: 6px;
}
label .tag {
display: inline-block;
font-size: 11px;
font-weight: 400;
padding: 1px 6px;
border-radius: 3px;
margin-left: 6px;
}
label .tag.o {
background: #fef3c7;
color: #92400e;
}
input[type="text"],
input[type="number"],
textarea {
width: 100%; width: 100%;
border: 2px solid transparent; padding: 10px 14px;
border-radius: var(--radius-sm); border: 1px solid #ddd;
font-size: 1.6rem; border-radius: 6px;
font-weight: 700; font-size: 14px;
text-align: center;
padding: 0.6rem 1rem;
color: var(--text);
background: #fafafa;
transition: var(--transition);
outline: none; outline: none;
font-family: inherit; font-family: inherit;
transition: border-color 0.2s;
} }
.title-input:hover { input[type="text"]:focus,
background: #f5f3ff; input[type="number"]:focus,
textarea:focus {
border-color: #333;
} }
.title-input:focus { textarea {
border-color: var(--primary-light); resize: vertical;
background: #fff;
box-shadow: 0 0 0 4px rgba(124, 108, 240, 0.06);
} }
.title-hint { .btn {
font-size: 0.8rem; display: block;
color: #b2bec3; width: 100%;
margin-top: 0.4rem; padding: 12px;
} background: #333;
color: white;
/* 抽奖主卡片 */ font-size: 15px;
.lottery-card { font-weight: 500;
background: var(--card-bg); border-radius: 6px;
border-radius: var(--radius); cursor: pointer;
padding: 2rem 2rem 2.2rem; transition: background 0.2s ease;
box-shadow: var(--shadow); border: none;
border: 1px solid var(--border);
text-align: center; text-align: center;
margin-top: 16px;
}
.btn:hover {
background: #555;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-sm {
display: inline-block;
width: auto;
padding: 6px 16px;
background: #333;
color: white;
font-size: 13px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
border: none;
text-align: center;
margin-top: 0;
}
.btn-sm:hover {
background: #555;
}
.btn-sm:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-sm.gray {
background: #6b7280;
}
.btn-sm.gray:hover {
background: #4b5563;
}
.btn-sm.gray:disabled {
background: #d1d5db;
}
.btn-sm.green {
background: #059669;
}
.btn-sm.green:hover {
background: #047857;
}
.btn-sm.green:disabled {
background: #a7f3d0;
}
.quick-fill {
margin-top: 5px;
font-size: 12px;
color: #666;
}
.quick-fill span {
color: #0066cc;
cursor: pointer;
margin-right: 8px;
}
.quick-fill span:hover {
text-decoration: underline;
}
/* ---- Lottery Display ---- */
.lottery-display {
text-align: center;
padding: 24px 16px;
background: #f9fafb;
border-radius: 8px;
border: 1px solid #eee;
margin-bottom: 16px;
} }
.rolling-display { .rolling-display {
font-size: 2.4rem; font-size: 2.4rem;
font-weight: 800; font-weight: 800;
color: var(--primary); color: #333;
min-height: 4rem; min-height: 4rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
letter-spacing: 1px; letter-spacing: 1px;
margin: 0.5rem 0 1rem;
word-break: break-all; word-break: break-all;
transition: color 0.2s;
} }
.rolling-display.idle { .rolling-display.idle {
color: #c4b5fd; color: #999;
font-size: 1.3rem; font-size: 1.2rem;
font-weight: 500; font-weight: 500;
} }
/* 结果展示 */ /* ---- Winner Result ---- */
.winner-display { .winner-display {
background: linear-gradient(135deg, #fff9e6, #fff3cd); background: #f0fdf4;
border: 2px solid var(--gold); border: 1px solid #86efac;
border-radius: var(--radius); border-radius: 8px;
padding: 1.5rem; padding: 20px 16px;
margin: 1rem 0 1.5rem; margin-top: 16px;
animation: popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); text-align: center;
} }
.winner-label { .winner-label {
font-size: 0.85rem; font-size: 14px;
text-transform: uppercase; font-weight: 600;
letter-spacing: 1.5px; color: #065f46;
color: #b8860b; margin-bottom: 8px;
margin-bottom: 0.3rem;
} }
.winner-name { .winner-name {
font-size: 2.2rem; font-size: 2rem;
font-weight: 800; font-weight: 800;
color: #d63031; color: #dc2626;
letter-spacing: 1px; letter-spacing: 1px;
word-break: break-all; word-break: break-all;
} }
.confetti-row { .confetti-row {
margin-top: 0.5rem; margin-top: 8px;
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 0.5rem; gap: 0.5rem;
} }
.confetti-emoji { .confetti-emoji {
font-size: 1.8rem; font-size: 1.6rem;
animation: bounce 0.6s ease infinite alternate; animation: bounce 0.6s ease infinite alternate;
} }
@ -172,130 +289,78 @@
animation-delay: 0.3s; animation-delay: 0.3s;
} }
@keyframes popIn {
0% {
opacity: 0;
transform: scale(0.7);
}
70% {
transform: scale(1.05);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes bounce { @keyframes bounce {
from { from { transform: translateY(0); }
transform: translateY(0); to { transform: translateY(-8px); }
}
to {
transform: translateY(-8px);
}
} }
/* 按钮 */ /* ---- Status ---- */
.btn {
padding: 0.9rem 2.5rem;
border: none;
border-radius: 50px;
font-weight: 700;
font-size: 1.05rem;
cursor: pointer;
transition: var(--transition);
letter-spacing: 0.5px;
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-lottery {
background: linear-gradient(135deg, #fd79a8, #e84393);
color: #fff;
box-shadow: 0 6px 20px rgba(253, 121, 168, 0.35);
}
.btn-lottery:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 10px 28px rgba(253, 121, 168, 0.45);
}
.btn-lottery:active:not(:disabled) {
transform: translateY(0);
}
.btn-lottery:disabled {
background: #b2bec3;
box-shadow: none;
cursor: not-allowed;
transform: none;
}
.btn-reset {
background: transparent;
border: 2px solid var(--border);
color: var(--text-light);
margin-top: 0.8rem;
font-weight: 600;
}
.btn-reset:hover {
border-color: var(--primary-light);
color: var(--primary);
background: #f8f7ff;
}
/* 状态提示 */
.status-bar { .status-bar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; gap: 8px;
gap: 0.5rem; padding: 10px 14px;
font-size: 0.85rem; background: #f9fafb;
color: var(--text-light); border-radius: 6px;
margin-top: 1rem; margin-top: 16px;
font-size: 13px;
color: #666;
} }
.status-dot { .status-dot {
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: #00b894; background: #059669;
flex-shrink: 0;
animation: pulse 1.5s ease infinite; animation: pulse 1.5s ease infinite;
} }
.status-dot.error { .status-dot.error {
background: #e17055; background: #dc2626;
}
.status-dot.idle {
background: #9ca3af;
animation: none;
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; }
0%, 50% { opacity: 0.4; }
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
} }
/* 响应式 */ .status-text {
@media (max-width: 500px) { flex: 1;
}
.card, /* ---- Footer ---- */
.lottery-card { .footer-link {
padding: 1.4rem 1.2rem; text-align: center;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #eee;
font-size: 13px;
}
.footer-link a {
color: #666;
text-decoration: none;
}
.footer-link a:hover {
text-decoration: underline;
}
/* ---- Responsive ---- */
@media (max-width: 500px) {
.container {
padding: 20px 16px;
margin: 20px auto;
} }
.title-input { h1 {
font-size: 1.3rem; font-size: 20px;
} }
.rolling-display { .rolling-display {
@ -304,7 +369,7 @@
} }
.winner-name { .winner-name {
font-size: 1.7rem; font-size: 1.5rem;
} }
} }
</style> </style>
@ -312,37 +377,56 @@
<body> <body>
<div class="container"> <div class="container">
<!-- 标题编辑区 --> <h1>云小编每日抽奖</h1>
<div class="card title-section"> <p class="subtitle">每次抽奖消耗 <b>200 个曲库编辑部积分</b>,每日最多可抽奖 3 次</p>
<input type="text" class="title-input" id="titleInput" value="🎉 云小编每日抽奖 🎉" autocomplete="off" disabled>
<div class="title-hint">每次抽奖消耗 <b>200 个曲库编辑部积分</b>,每日最多可抽奖 3 次</div> <!-- Cookie 管理 -->
<div class="form-group">
<label for="cookieInput">令牌 <span class="tag o">可选</span></label>
<input type="text" id="cookieInput" placeholder="留空则默认读取本地存储的令牌" autocomplete="off" />
<div class="quick-fill">
<span onclick="readLocalCookie()">读取本地令牌</span>
<span onclick="clearCookie()">清除令牌</span>
<span onclick="saveCookie()">保存令牌</span>
</div>
</div> </div>
<!-- 抽奖主区域 --> <!-- 抽奖区域 -->
<div class="lottery-card"> <div class="section">
<div class="rolling-display idle" id="rollingDisplay">等待抽奖</div> <div class="section-header">
<div id="resultContainer"></div> <span class="section-title">每日抽奖</span>
<button class="btn btn-lottery" id="lotteryBtn">
<span>🎲</span> 开始抽奖
</button>
<button class="btn btn-reset" id="resetBtn">
<span>🔄</span> 编辑令牌
</button>
<div class="status-bar" id="statusBar">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">就绪,点击按钮开始抽奖</span>
</div> </div>
<div class="lottery-display">
<div class="rolling-display idle" id="rollingDisplay">等待抽奖</div>
<div id="resultContainer"></div>
</div>
<button class="btn" id="lotteryBtn">
🎲 开始抽奖
</button>
<div class="status-bar" id="statusBar">
<span class="status-dot idle" id="statusDot"></span>
<span class="status-text" id="statusText">就绪,点击按钮开始抽奖</span>
</div>
</div>
<!-- 页脚 -->
<div class="footer-link">
<a href="/">← 返回首页</a>
<span style="margin: 0 8px; color: #ddd;">·</span>
<a href="/ugc.html">云小编任务中心</a>
</div> </div>
</div> </div>
<script> <script>
(function () { (function () {
// ============ DOM 元素 ============ // ============ DOM 元素 ============
const titleInput = document.getElementById('titleInput'); const cookieInput = document.getElementById('cookieInput');
const rollingDisplay = document.getElementById('rollingDisplay'); const rollingDisplay = document.getElementById('rollingDisplay');
const resultContainer = document.getElementById('resultContainer'); const resultContainer = document.getElementById('resultContainer');
const lotteryBtn = document.getElementById('lotteryBtn'); const lotteryBtn = document.getElementById('lotteryBtn');
const resetBtn = document.getElementById('resetBtn');
const statusDot = document.getElementById('statusDot'); const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText'); const statusText = document.getElementById('statusText');
@ -356,6 +440,10 @@
statusDot.className = 'status-dot'; statusDot.className = 'status-dot';
if (type === 'error') { if (type === 'error') {
statusDot.classList.add('error'); statusDot.classList.add('error');
} else if (type === 'info') {
statusDot.style.background = '#059669';
} else {
statusDot.classList.add('idle');
} }
} }
@ -364,30 +452,63 @@
resultContainer.innerHTML = ''; resultContainer.innerHTML = '';
rollingDisplay.textContent = '等待抽奖'; rollingDisplay.textContent = '等待抽奖';
rollingDisplay.className = 'rolling-display idle'; rollingDisplay.className = 'rolling-display idle';
setStatus('info', '就绪,点击按钮开始抽奖'); setStatus('idle', '就绪,点击按钮开始抽奖');
} }
// ============ 本地令牌管理 ============ // ============ 本地令牌管理 ============
const cookieKey = 'ncm_cookie'; const cookieKey = 'ncm_cookie';
let cookie = localStorage.getItem(cookieKey) || ''; let cookie = localStorage.getItem(cookieKey) || '';
function setCookie() { if (cookie) {
const ck = prompt('请输入令牌:', cookie); cookieInput.value = cookie;
if (ck !== null) { }
cookie = ck;
localStorage.setItem(cookieKey, cookie); window.readLocalCookie = function () {
const local = localStorage.getItem(cookieKey);
if (local) {
cookie = local;
cookieInput.value = local;
setStatus('info', '已读取本地令牌');
} else {
setStatus('error', '未在本地发现令牌,请手动输入');
} }
};
window.clearCookie = function () {
cookie = '';
cookieInput.value = '';
localStorage.removeItem(cookieKey);
setStatus('info', '已清除本地令牌');
};
window.saveCookie = function () {
const ck = cookieInput.value.trim();
if (ck) {
cookie = ck;
localStorage.setItem(cookieKey, ck);
setStatus('info', '已保存本地令牌');
} else {
setStatus('error', '令牌内容为空,未保存');
}
};
function getCookie() {
const ck = cookieInput.value.trim();
if (ck) cookie = ck;
return ck || cookie || '';
} }
// ============ 调用后端接口 ============ // ============ 调用后端接口 ============
async function request(url, params) { async function request(url, params) {
try { try {
url += '?timestamp=' + Date.now() + params; url += '?timestamp=' + Date.now() + params;
if (cookie) url += '&cookie=' + encodeURIComponent(cookie); const ck = getCookie();
if (ck) url += '&cookie=' + encodeURIComponent(ck);
const response = await fetch(url); const response = await fetch(url);
let res;
try { try {
var res = await response.json(); res = await response.json();
} catch (error) { } catch (error) {
console.error('数据解析错误:', error); console.error('数据解析错误:', error);
throw new Error('接口请求错误: ' + response.status + ': ' + response.statusText); throw new Error('接口请求错误: ' + response.status + ': ' + response.statusText);
@ -419,13 +540,11 @@
const res = await request('middle/play/do/lottery', '&activityId=6501202&drawCount=1'); const res = await request('middle/play/do/lottery', '&activityId=6501202&drawCount=1');
// 根据你的后端返回格式提取中奖者名称
const winner = res.data?.prizeDetailInfoMap?.['8186002']?.prizeName; const winner = res.data?.prizeDetailInfoMap?.['8186002']?.prizeName;
if (!winner) { if (!winner) {
throw new Error('无法解析响应数据'); throw new Error('无法解析响应数据');
} }
// [抽奖结果, 剩余机会]
return [winner, res.data.restChance]; return [winner, res.data.restChance];
} }
@ -446,12 +565,10 @@
const easeOutProgress = 1 - Math.pow(1 - progress, 3); const easeOutProgress = 1 - Math.pow(1 - progress, 3);
const currentInterval = startInterval + (endInterval - startInterval) * easeOutProgress; const currentInterval = startInterval + (endInterval - startInterval) * easeOutProgress;
// 随机显示占位符
const randomPlaceholder = placeholderNames[Math.floor(Math.random() * placeholderNames.length)]; const randomPlaceholder = placeholderNames[Math.floor(Math.random() * placeholderNames.length)];
rollingDisplay.textContent = randomPlaceholder; rollingDisplay.textContent = randomPlaceholder;
if (progress >= 1.0) { if (progress >= 1.0) {
// 动画结束,显示真实结果
clearTimeout(rollingTimer); clearTimeout(rollingTimer);
rollingDisplay.textContent = finalWinner; rollingDisplay.textContent = finalWinner;
showWinnerResult(finalWinner); showWinnerResult(finalWinner);
@ -466,17 +583,16 @@
} }
function showWinnerResult(winner) { function showWinnerResult(winner) {
resultContainer.innerHTML = ` resultContainer.innerHTML =
<div class="winner-display"> '<div class="winner-display">' +
<div class="winner-label">🏆 恭喜中奖</div> '<div class="winner-label">🏆 恭喜中奖</div>' +
<div class="winner-name">${escapeHTML(winner)}</div> '<div class="winner-name">' + escapeHTML(winner) + '</div>' +
<div class="confetti-row"> '<div class="confetti-row">' +
<span class="confetti-emoji">🎉</span> '<span class="confetti-emoji">🎉</span>' +
<span class="confetti-emoji">🎊</span> '<span class="confetti-emoji">🎊</span>' +
<span class="confetti-emoji"></span> '<span class="confetti-emoji"></span>' +
</div> '</div>' +
</div> '</div>';
`;
} }
function escapeHTML(str) { function escapeHTML(str) {
@ -488,47 +604,37 @@
function finishDrawing() { function finishDrawing() {
isDrawing = false; isDrawing = false;
lotteryBtn.disabled = false; lotteryBtn.disabled = false;
resetBtn.disabled = false;
// setStatus('info', '抽奖完成!点击重置可再次抽奖');
} }
// ============ 主抽奖流程 ============ // ============ 主抽奖流程 ============
async function startLottery() { async function startLottery() {
if (isDrawing) return; if (isDrawing) return;
// 开始前状态
isDrawing = true; isDrawing = true;
lotteryBtn.disabled = true; lotteryBtn.disabled = true;
resetBtn.disabled = true;
resultContainer.innerHTML = ''; resultContainer.innerHTML = '';
rollingDisplay.className = 'rolling-display'; rollingDisplay.className = 'rolling-display';
rollingDisplay.textContent = '🎰'; rollingDisplay.textContent = '🎰';
setStatus('info', '正在获取抽奖结果...'); setStatus('info', '正在获取抽奖结果...');
try { try {
// 调用后端接口获取中奖者
const res = await fetchLotteryResult(); const res = await fetchLotteryResult();
setStatus('info', '抽奖成功,剩余次数 ' + res[1]); setStatus('info', '抽奖成功,剩余次数 ' + res[1]);
// 启动滚动动画,最终定格在中奖者
startRollingAnimation(res[0]); startRollingAnimation(res[0]);
} catch (error) { } catch (error) {
// 请求失败处理
rollingDisplay.textContent = '抽奖失败'; rollingDisplay.textContent = '抽奖失败';
rollingDisplay.className = 'rolling-display idle'; rollingDisplay.className = 'rolling-display idle';
setStatus('error', '抽奖失败: ' + error.message); setStatus('error', '抽奖失败: ' + error.message);
isDrawing = false; isDrawing = false;
lotteryBtn.disabled = false; lotteryBtn.disabled = false;
resetBtn.disabled = false;
} }
} }
// ============ 事件绑定 ============ // ============ 事件绑定 ============
lotteryBtn.addEventListener('click', startLottery); lotteryBtn.addEventListener('click', startLottery);
resetBtn.addEventListener('click', setCookie);
// 空格键快捷抽奖 document.addEventListener('keydown', function (e) {
document.addEventListener('keydown', (e) => { if (e.key === ' ' && document.activeElement !== cookieInput) {
if (e.key === ' ' && document.activeElement !== titleInput) {
e.preventDefault(); e.preventDefault();
if (!isDrawing && !lotteryBtn.disabled) { if (!isDrawing && !lotteryBtn.disabled) {
startLottery(); startLottery();
@ -536,6 +642,13 @@
} }
}); });
// 回车键在输入框中触发保存
cookieInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
saveCookie();
}
});
// ============ 初始化 ============ // ============ 初始化 ============
resetResult(); resetResult();
})(); })();