2026-08-02 21:48:01 +08:00

422 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="no-referrer" />
<link rel="icon" href="docs/netease.png" />
<link rel="stylesheet" href="/static/shell.css" />
<title>云小编每日抽奖 - 网易云音乐 API Enhanced</title>
<style>
body { padding: 1.25rem; }
.shell-header { text-align: center; }
.subtitle { text-align: center; }
.subtitle b { color: var(--destructive); }
/* ---- Lottery Display ---- */
.lottery-display {
text-align: center;
padding: 1.5rem 1rem;
background: var(--gray-2);
border: 1px solid var(--border);
margin-bottom: 1rem;
}
.rolling-display {
font-size: 2.4rem;
font-weight: 800;
color: var(--foreground);
min-height: 4rem;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 1px;
word-break: break-all;
}
.rolling-display.idle {
color: var(--muted-foreground);
font-size: 1.2rem;
font-weight: 500;
}
/* ---- Winner Result ---- */
.winner-display {
background: color-mix(in srgb, var(--success) 8%, var(--gray-2));
border: 1px solid var(--success);
padding: 1.25rem 1rem;
margin-top: 1rem;
text-align: center;
}
.winner-label {
font-family: var(--font-mono);
font-size: 0.875rem;
font-weight: 600;
color: var(--success);
margin-bottom: 0.5rem;
}
.winner-name {
font-size: 2rem;
font-weight: 800;
color: var(--foreground);
letter-spacing: 1px;
word-break: break-all;
}
.confetti-row {
margin-top: 0.5rem;
display: flex;
justify-content: center;
gap: 0.5rem;
}
.confetti-emoji {
font-size: 1.6rem;
animation: bounce 0.6s ease infinite alternate;
}
.confetti-emoji:nth-child(2) { animation-delay: 0.15s; }
.confetti-emoji:nth-child(3) { animation-delay: 0.3s; }
@keyframes bounce {
from { transform: translateY(0); }
to { transform: translateY(-8px); }
}
/* ---- Status ---- */
.status-bar {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 0.875rem;
background: var(--gray-2);
border: 1px solid var(--border);
margin-top: 1rem;
font-size: 0.8125rem;
color: var(--muted-foreground);
}
.status-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 9999px;
background: var(--success);
flex-shrink: 0;
animation: pulse 1.5s ease infinite;
}
.status-dot.error { background: var(--destructive); }
.status-dot.idle { background: var(--gray-5); animation: none; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.status-text { flex: 1; }
/* ---- Footer ---- */
.footer-link {
text-align: center;
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
font-size: 0.8125rem;
}
.footer-link a { color: var(--muted-foreground); }
.footer-link a:hover { color: var(--foreground); text-decoration: underline; }
@media (max-width: 500px) {
.rolling-display { font-size: 1.8rem; min-height: 3rem; }
.winner-name { font-size: 1.5rem; }
}
</style>
</head>
<body>
<header class="shell-topbar">
<a class="brand" href="/"><img src="docs/netease.png" alt="logo">云小编抽奖</a>
<nav>
<a class="shell-btn shell-btn--ghost shell-btn--sm" href="/ugc.html">任务中心</a>
<a class="shell-btn shell-btn--ghost shell-btn--sm" href="/">返回首页</a>
</nav>
</header>
<main class="shell-main">
<div class="shell-container shell-narrow" style="max-width:32rem">
<header class="shell-header">
<h1>云小编每日抽奖</h1>
<p class="subtitle">每次抽奖消耗 <b>200 个曲库编辑部积分</b>,每日最多可抽奖 3 次</p>
</header>
<section class="shell-card">
<!-- 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 class="section" style="border-bottom:none;padding-bottom:0;margin-bottom:0">
<div class="section-header">
<span class="section-title" style="font-family:var(--font-mono);font-size:1rem;font-weight:600">每日抽奖</span>
</div>
<div class="lottery-display">
<div class="rolling-display idle" id="rollingDisplay">等待抽奖</div>
<div id="resultContainer"></div>
</div>
<button class="shell-btn shell-btn--default shell-btn--block" 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 0.5rem; color: var(--border);">·</span>
<a href="/ugc.html">云小编任务中心</a>
</div>
</section>
</div>
</main>
<script>
(function () {
// ============ DOM 元素 ============
const cookieInput = document.getElementById('cookieInput');
const rollingDisplay = document.getElementById('rollingDisplay');
const resultContainer = document.getElementById('resultContainer');
const lotteryBtn = document.getElementById('lotteryBtn');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
// ============ 状态 ============
let isDrawing = false;
let rollingTimer = null;
// ============ 状态栏更新 ============
function setStatus(type, message) {
statusText.textContent = message;
statusDot.className = 'status-dot';
if (type === 'error') {
statusDot.classList.add('error');
} else if (type === 'info') {
statusDot.style.background = '#059669';
} else {
statusDot.classList.add('idle');
}
}
// ============ 重置结果 ============
function resetResult() {
resultContainer.innerHTML = '';
rollingDisplay.textContent = '等待抽奖';
rollingDisplay.className = 'rolling-display idle';
setStatus('idle', '就绪,点击按钮开始抽奖');
}
// ============ 本地令牌管理 ============
const cookieKey = 'ncm_cookie';
let cookie = localStorage.getItem(cookieKey) || '';
if (cookie) {
cookieInput.value = 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) {
try {
url += '?timestamp=' + Date.now() + params;
const ck = getCookie();
if (ck) url += '&cookie=' + encodeURIComponent(ck);
const response = await fetch(url);
let res;
try {
res = await response.json();
} catch (error) {
console.error('数据解析错误:', error);
throw new Error('接口请求错误: ' + response.status + ': ' + response.statusText);
}
if (res.code !== 200) {
throw new Error('接口返回错误: ' + res.code + ': ' + (res.message || '未知错误'));
}
return res;
} catch (error) {
console.error('接口请求失败:', error);
throw error;
}
}
async function fetchLotteryResult() {
const user = await request('rep/ugc/user/get', '');
if ((user.data?.availablePoints ?? 0) < 200) {
throw new Error('用户剩余积分不足,请先完成任务获取积分');
}
const chance = await request('middle/play/lottery/remain/chance', '&activityId=6501202');
if (chance.data === 0) {
throw new Error('今日机会已用完,明天再来吧');
}
const res = await request('middle/play/do/lottery', '&activityId=6501202&drawCount=1');
const winner = res.data?.prizeDetailInfoMap?.['8186002']?.prizeName;
if (!winner) {
throw new Error('无法解析响应数据');
}
return [winner, res.data.restChance];
}
// ============ 滚动动画 ============
function startRollingAnimation(finalWinner) {
const placeholderNames = ['🎰', '🎲', '🎯', '✨', '💫', '🌟', '🎪', '🎭'];
const totalDuration = 2800;
const startInterval = 50;
const endInterval = 280;
const startTime = Date.now();
rollingDisplay.className = 'rolling-display';
resultContainer.innerHTML = '';
function roll() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / totalDuration, 1.0);
const easeOutProgress = 1 - Math.pow(1 - progress, 3);
const currentInterval = startInterval + (endInterval - startInterval) * easeOutProgress;
const randomPlaceholder = placeholderNames[Math.floor(Math.random() * placeholderNames.length)];
rollingDisplay.textContent = randomPlaceholder;
if (progress >= 1.0) {
clearTimeout(rollingTimer);
rollingDisplay.textContent = finalWinner;
showWinnerResult(finalWinner);
finishDrawing();
return;
}
rollingTimer = setTimeout(roll, currentInterval);
}
roll();
}
function showWinnerResult(winner) {
resultContainer.innerHTML =
'<div class="winner-display">' +
'<div class="winner-label">🏆 恭喜中奖</div>' +
'<div class="winner-name">' + escapeHTML(winner) + '</div>' +
'<div class="confetti-row">' +
'<span class="confetti-emoji">🎉</span>' +
'<span class="confetti-emoji">🎊</span>' +
'<span class="confetti-emoji">✨</span>' +
'</div>' +
'</div>';
}
function escapeHTML(str) {
const div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
function finishDrawing() {
isDrawing = false;
lotteryBtn.disabled = false;
}
// ============ 主抽奖流程 ============
async function startLottery() {
if (isDrawing) return;
isDrawing = true;
lotteryBtn.disabled = true;
resultContainer.innerHTML = '';
rollingDisplay.className = 'rolling-display';
rollingDisplay.textContent = '🎰';
setStatus('info', '正在获取抽奖结果...');
try {
const res = await fetchLotteryResult();
setStatus('info', '抽奖成功,剩余次数 ' + res[1]);
startRollingAnimation(res[0]);
} catch (error) {
rollingDisplay.textContent = '抽奖失败';
rollingDisplay.className = 'rolling-display idle';
setStatus('error', '抽奖失败: ' + error.message);
isDrawing = false;
lotteryBtn.disabled = false;
}
}
// ============ 事件绑定 ============
lotteryBtn.addEventListener('click', startLottery);
document.addEventListener('keydown', function (e) {
if (e.key === ' ' && document.activeElement !== cookieInput) {
e.preventDefault();
if (!isDrawing && !lotteryBtn.disabled) {
startLottery();
}
}
});
// 回车键在输入框中触发保存
cookieInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
saveCookie();
}
});
// ============ 初始化 ============
resetResult();
})();
</script>
</body>
</html>