2026-07-23 11:14:35 +08:00

506 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
<meta name="referrer" content="no-referrer" />
<link rel="icon" href="docs/netease.png" />
<title>云小编每日抽奖 - 网易云音乐 API Enhanced</title>
<style>
:root {
--gold: #fdcb6e;
--text-light: #636e72;
--fg: #333;
--muted: #666;
--border: #ddd;
--bg: #f5f5f5;
--panel: #ffffff;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
min-height: 100vh;
display: flex;
/* align-items: center; */
justify-content: center;
padding: 1.5rem;
color: var(--fg);
background: var(--bg);
line-height: 1.5;
}
.container {
width: 100%;
max-width: 520px;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* 标题 */
.header h1 {
font-size: 24px;
font-weight: 600;
}
.header p {
margin-top: 4px;
color: var(--muted);
font-size: 14px;
}
/* 卡片 */
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
/* 按钮 */
button {
padding: 8px 18px;
border-radius: 8px;
border: 1px solid var(--border);
font-size: 14px;
font-weight: 500;
cursor: pointer;
background: var(--panel);
color: var(--fg);
transition: all 0.15s;
}
button:hover {
background: #eee;
}
button.primary {
background: var(--fg);
color: var(--panel);
border-color: var(--fg);
}
button.primary:hover {
opacity: 0.85;
}
/* 页脚 */
.footer {
padding-top: 12px;
border-top: 1px solid var(--border);
color: var(--muted);
text-align: center;
font-size: 13px;
}
.footer a {
color: var(--fg);
text-decoration: none;
}
.footer a:hover {
border-bottom: 1px dotted var(--fg);
}
.rolling-display {
font-size: 2.4rem;
font-weight: 800;
color: #c4b5fd;
min-height: 4rem;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 1px;
margin: 0.5rem 0 1rem;
word-break: break-all;
transition: color 0.2s;
}
.rolling-display.idle {
font-size: 1.3rem;
font-weight: 500;
}
/* 结果展示 */
.winner-display {
background: linear-gradient(135deg, #fff9e6, #fff3cd);
border: 2px solid var(--gold);
border-radius: var(--radius);
padding: 1.5rem;
margin: 1rem 0 1.5rem;
animation: popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
text-align: center;
}
.winner-label {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #b8860b;
margin-bottom: 0.3rem;
}
.winner-name {
font-size: 2.2rem;
font-weight: 800;
color: #d63031;
letter-spacing: 1px;
word-break: break-all;
}
.winner-img {
width: 200px;
height: 200px;
margin: -20px;
}
.confetti-row {
margin-top: 0.5rem;
display: flex;
justify-content: center;
gap: 0.5rem;
}
.confetti-emoji {
font-size: 1.8rem;
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 popIn {
0% {
opacity: 0;
transform: scale(0.7);
}
70% {
transform: scale(1.05);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes bounce {
from {
transform: translateY(0);
}
to {
transform: translateY(-8px);
}
}
/* 状态提示 */
.status-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 0.85rem;
color: var(--text-light);
margin-top: 1rem;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #00b894;
animation: pulse 1.5s ease infinite;
}
.status-dot.error {
background: #e17055;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
/* 响应式 */
@media (max-width: 500px) {
.rolling-display {
font-size: 1.8rem;
min-height: 3rem;
}
.winner-name {
font-size: 1.7rem;
}
}
</style>
</head>
<body>
<div class="container">
<!-- 标题 -->
<div class="header">
<h1>🎉 云小编每日抽奖</h1>
<p>每次抽奖消耗 <b>200 个曲库编辑部积分</b>,每日最多可抽奖 3 次</p>
</div>
<!-- 抽奖 -->
<div class="card">
<div class="rolling-display idle" id="rollingDisplay">等待抽奖</div>
<div id="resultContainer"></div>
<button id="lotteryBtn" class="primary">
<span>🎲</span> 开始抽奖
</button>
<button 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="footer">
<a href="/">← 返回首页</a>
</div>
</div>
<script>
(function () {
// ============ DOM 元素 ============
const rollingDisplay = document.getElementById('rollingDisplay');
const resultContainer = document.getElementById('resultContainer');
const lotteryBtn = document.getElementById('lotteryBtn');
const resetBtn = document.getElementById('resetBtn');
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');
}
}
// ============ 重置结果 ============
function resetResult() {
resultContainer.innerHTML = '';
rollingDisplay.textContent = '等待抽奖';
rollingDisplay.className = 'rolling-display idle';
setStatus('info', '就绪,点击按钮开始抽奖');
}
// ============ 本地令牌管理 ============
const cookieKey = 'ncm_cookie';
let cookie = localStorage.getItem(cookieKey) || '';
function setCookie() {
const ck = prompt('请输入令牌:', cookie);
if (ck !== null) {
cookie = ck;
localStorage.setItem(cookieKey, cookie);
}
}
// ============ 调用后端接口 ============
async function request(url, params) {
try {
url += '?timestamp=' + Date.now() + params;
if (cookie) url += '&cookie=' + encodeURIComponent(cookie);
const response = await fetch(url);
try {
var 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 prizeMap = res.data?.prizeDetailInfoMap || {};
for (var k in prizeMap) break; // 取第一个奖品 ID
const prize = k ? prizeMap[k] : null;
if (!prize) {
throw new Error('无法解析响应数据');
}
// [抽奖结果, 剩余机会, 奖品图片]
return [prize.prizeName, res.data.restChance, prize.prizeImgList[0] || ''];
}
// ============ 滚动动画 ============
function startRollingAnimation(finalWinner, img) {
const placeholderNames = ['🎰', '🎲', '🎯', '✨', '💫', '🌟', '🎪', '🎭'];
const totalDuration = 2000;
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;
rollingDisplay.style.display = 'none';
showWinnerResult(finalWinner, img);
finishDrawing();
return;
}
rollingTimer = setTimeout(roll, currentInterval);
}
roll();
}
function showWinnerResult(winner, img) {
resultContainer.innerHTML = `
<div class="winner-display">
<div class="winner-label">🏆 恭喜中奖</div>
<div class="winner-name">${escapeHTML(winner)}</div>
<img src="${img}" alt="奖品图片" class="winner-img" />
<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;
resetBtn.disabled = false;
// setStatus('info', '抽奖完成!点击重置可再次抽奖');
}
// ============ 主抽奖流程 ============
async function startLottery() {
if (isDrawing) return;
// 开始前状态
isDrawing = true;
lotteryBtn.disabled = true;
resetBtn.disabled = true;
resultContainer.innerHTML = '';
rollingDisplay.className = 'rolling-display';
rollingDisplay.textContent = '🎰';
rollingDisplay.style.display = 'flex';
setStatus('info', '正在获取抽奖结果...');
try {
// 调用后端接口获取中奖者
const res = await fetchLotteryResult();
setStatus('info', '抽奖成功,剩余次数 ' + res[1]);
// 启动滚动动画,最终定格在中奖者
startRollingAnimation(res[0], res[2]);
} catch (error) {
// 请求失败处理
rollingDisplay.textContent = '抽奖失败';
rollingDisplay.className = 'rolling-display idle';
setStatus('error', '抽奖失败: ' + error.message);
isDrawing = false;
lotteryBtn.disabled = false;
resetBtn.disabled = false;
}
}
// ============ 事件绑定 ============
lotteryBtn.addEventListener('click', startLottery);
resetBtn.addEventListener('click', setCookie);
// 空格键快捷抽奖
document.addEventListener('keydown', (e) => {
if (e.key === ' ') {
e.preventDefault();
if (!isDrawing && !lotteryBtn.disabled) {
startLottery();
}
}
});
// ============ 初始化 ============
resetResult();
window.demo = function (name, chance, img) {
rollingDisplay.style.display = 'flex';
setStatus('info', '抽奖成功,剩余次数 ' + (chance || '0'));
startRollingAnimation(name || '演示', img || 'https://p5.music.126.net/obj/worDmcOCw6HCnzzCmsKx/61891591760/8d1c/528a/e977/10cbaa3fb39e8057be5d2560182cc5c7.png');
};
})();
</script>
</body>
</html>