Files
2025-12-26 22:41:42 +08:00

799 lines
37 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>阴阳师抽卡系统 - 管理后台</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<style>
.stat-card {
border-radius: 15px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.rarity-r { color: #6c757d; }
.rarity-sr { color: #0d6efd; }
.rarity-ssr { color: #ffc107; }
.rarity-sp { color: #dc3545; }
.achievement-card {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 10px;
padding: 15px;
margin-bottom: 15px;
}
.progress {
height: 25px;
}
.navbar-brand {
font-weight: bold;
}
.table th {
border-top: none;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="#">
<i class="bi bi-dice-5"></i> 阴阳师抽卡系统 - 管理后台
</a>
<div class="ms-auto">
<span class="navbar-text">
<i class="bi bi-shield-lock"></i> 管理员访问
</span>
</div>
</div>
</nav>
<div class="container mt-4">
<!-- 统计概览 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card stat-card bg-light">
<div class="card-body text-center">
<h5 class="card-title"><i class="bi bi-people"></i> 参与人数</h5>
<h2 class="text-primary" id="totalUsers">-</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card bg-light">
<div class="card-body text-center">
<h5 class="card-title"><i class="bi bi-dice-6"></i> 总抽卡次数</h5>
<h2 class="text-success" id="totalDraws">-</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card bg-light">
<div class="card-body text-center">
<h5 class="card-title"><i class="bi bi-trophy"></i> SSR/SP总数</h5>
<h2 class="text-warning" id="totalSSRSP">-</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card bg-light">
<div class="card-body text-center">
<h5 class="card-title"><i class="bi bi-percent"></i> SSR/SP概率</h5>
<h2 class="text-danger" id="ssrSpRate">-</h2>
</div>
</div>
</div>
</div>
<!-- 稀有度分布 -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-pie-chart"></i> 稀有度分布</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="text-center">
<h3 class="rarity-r">R</h3>
<div class="progress mb-2">
<div class="progress-bar bg-secondary" id="rRate" style="width: 0%"></div>
</div>
<h4 id="rCount">-</h4>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h3 class="rarity-sr">SR</h3>
<div class="progress mb-2">
<div class="progress-bar bg-primary" id="srRate" style="width: 0%"></div>
</div>
<h4 id="srCount">-</h4>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h3 class="rarity-ssr">SSR</h3>
<div class="progress mb-2">
<div class="progress-bar bg-warning" id="ssrRate" style="width: 0%"></div>
</div>
<h4 id="ssrCount">-</h4>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h3 class="rarity-sp">SP</h3>
<div class="progress mb-2">
<div class="progress-bar bg-danger" id="spRate" style="width: 0%"></div>
</div>
<h4 id="spCount">-</h4>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 排行榜 -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="bi bi-trophy"></i> SSR/SP排行榜</h5>
<button class="btn btn-sm btn-outline-primary" onclick="refreshRankList()">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>排名</th>
<th>用户ID</th>
<th>总抽卡次数</th>
<th class="rarity-r">R</th>
<th class="rarity-sr">SR</th>
<th class="rarity-ssr">SSR</th>
<th class="rarity-sp">SP</th>
<th>SSR/SP总数</th>
<th>操作</th>
</tr>
</thead>
<tbody id="rankListBody">
<tr>
<td colspan="9" class="text-center">加载中...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- 用户查询 -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-person-search"></i> 用户查询</h5>
</div>
<div class="card-body">
<div class="input-group mb-3">
<input type="text" class="form-control" id="userIdInput" placeholder="输入用户ID">
<button class="btn btn-primary" onclick="queryUser()">
<i class="bi bi-search"></i> 查询
</button>
</div>
<div id="userStatsResult" style="display: none;">
<div class="row">
<div class="col-md-6">
<h6>抽卡统计</h6>
<table class="table table-sm">
<tr>
<td>总抽卡次数</td>
<td id="userTotalDraws">-</td>
</tr>
<tr>
<td class="rarity-r">R卡数量</td>
<td id="userRCount">-</td>
</tr>
<tr>
<td class="rarity-sr">SR卡数量</td>
<td id="userSRCount">-</td>
</tr>
<tr>
<td class="rarity-ssr">SSR卡数量</td>
<td id="userSSRCount">-</td>
</tr>
<tr>
<td class="rarity-sp">SP卡数量</td>
<td id="userSPCount">-</td>
</tr>
</table>
</div>
<div class="col-md-6">
<h6>最近抽卡记录</h6>
<div id="recentDraws">
加载中...
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 成就查询 -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5><i class="bi bi-award"></i> 成就查询</h5>
</div>
<div class="card-body">
<div class="input-group mb-3">
<input type="text" class="form-control" id="achievementUserIdInput" placeholder="输入用户ID">
<button class="btn btn-primary" onclick="queryAchievements()">
<i class="bi bi-search"></i> 查询
</button>
</div>
<div id="achievementResult" style="display: none;">
<div class="row">
<div class="col-md-6">
<h6>已解锁成就</h6>
<div id="unlockedAchievements">
加载中...
</div>
</div>
<div class="col-md-6">
<h6>成就进度</h6>
<div class="achievement-card">
<div class="d-flex justify-content-between">
<span>连续抽卡天数</span>
<span id="consecutiveDays">-</span>
</div>
<div class="progress mt-2">
<div class="progress-bar" id="consecutiveDaysProgress" style="width: 0%"></div>
</div>
</div>
<div class="achievement-card">
<div class="d-flex justify-content-between">
<span>无SSR/SP连击</span>
<span id="noSsrStreak">-</span>
</div>
<div class="progress mt-2">
<div class="progress-bar bg-danger" id="noSsrStreakProgress" style="width: 0%"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 每日详细抽卡记录 -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="bi bi-table"></i> 今日抽卡详细记录</h5>
<div>
<input type="date" class="form-control form-control-sm d-inline-block me-2" id="recordDateInput" style="width: auto;">
<button class="btn btn-sm btn-outline-primary" onclick="loadDailyRecords()">
<i class="bi bi-arrow-clockwise"></i> 刷新
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>抽卡时间</th>
<th>用户ID</th>
<th>式神名称</th>
<th>稀有度</th>
<th>成就解锁</th>
</tr>
</thead>
<tbody id="dailyRecordsBody">
<tr>
<td colspan="5" class="text-center">加载中...</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<div>
<span>总记录数: <strong id="totalRecordsCount">0</strong></span>
</div>
<div>
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm" id="recordsPagination">
<!-- 分页控件将通过JS动态生成 -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<footer class="bg-light text-center py-3 mt-5">
<div class="container">
<p class="mb-0">阴阳师抽卡系统 - 管理后台 &copy; 2024</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// API配置
const API_BASE = '/onmyoji_gacha/api';
let ADMIN_TOKEN = localStorage.getItem('adminToken');
// 令牌重置函数
function resetToken() {
localStorage.removeItem('adminToken');
const newToken = prompt('请输入管理员令牌:');
if (newToken) {
ADMIN_TOKEN = newToken;
localStorage.setItem('adminToken', ADMIN_TOKEN);
// 更新请求头
headers.Authorization = `Bearer ${ADMIN_TOKEN}`;
console.log('令牌已重置:', ADMIN_TOKEN);
// 重新加载数据
loadDailyStats();
loadRankList();
loadDailyRecords();
} else {
alert('需要管理员令牌才能访问');
}
}
// 如果没有保存的令牌,提示输入
if (!ADMIN_TOKEN) {
ADMIN_TOKEN = prompt('请输入管理员令牌:');
if (ADMIN_TOKEN) {
localStorage.setItem('adminToken', ADMIN_TOKEN);
} else {
alert('需要管理员令牌才能访问');
window.location.reload();
}
}
console.log('使用的管理员令牌:', ADMIN_TOKEN);
// API请求头
const headers = {
'Authorization': `Bearer ${ADMIN_TOKEN}`,
'Content-Type': 'application/json'
};
console.log('请求头:', headers);
// 添加令牌重置按钮到导航栏
window.addEventListener('DOMContentLoaded', function() {
const navbar = document.querySelector('.navbar .ms-auto');
const resetButton = document.createElement('button');
resetButton.className = 'btn btn-outline-light btn-sm ms-2';
resetButton.innerHTML = '<i class="bi bi-key"></i> 重置令牌';
resetButton.onclick = resetToken;
navbar.appendChild(resetButton);
});
// 页面加载时获取数据
document.addEventListener('DOMContentLoaded', function() {
loadDailyStats();
loadRankList();
// 设置今天的日期为默认值
const today = new Date().toISOString().split('T')[0];
document.getElementById('recordDateInput').value = today;
loadDailyRecords();
});
// 加载今日统计
async function loadDailyStats() {
try {
console.log('正在请求每日统计...');
const response = await fetch(`${API_BASE}/stats/daily`, { headers });
console.log('响应状态:', response.status);
console.log('响应头:', response.headers);
if (!response.ok) {
const errorText = await response.text();
console.error('API 错误响应:', errorText);
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json();
console.log('每日统计数据:', data);
if (data.success) {
const stats = data.stats;
document.getElementById('totalUsers').textContent = stats.total_users;
document.getElementById('totalDraws').textContent = stats.total_draws;
const ssrSpTotal = stats.rarity_stats.SSR + stats.rarity_stats.SP;
document.getElementById('totalSSRSP').textContent = ssrSpTotal;
const ssrSpRate = ((ssrSpTotal / stats.total_draws) * 100).toFixed(2);
document.getElementById('ssrSpRate').textContent = ssrSpRate + '%';
// 更新稀有度分布
updateRarityDistribution(stats.rarity_stats, stats.total_draws);
} else {
console.error('API 返回失败:', data);
}
} catch (error) {
console.error('加载统计数据失败:', error);
// 显示错误信息给用户
document.getElementById('totalUsers').textContent = '错误';
document.getElementById('totalDraws').textContent = '错误';
document.getElementById('totalSSRSP').textContent = '错误';
document.getElementById('ssrSpRate').textContent = '错误';
}
}
// 更新稀有度分布
function updateRarityDistribution(rarityStats, totalDraws) {
const rarities = ['R', 'SR', 'SSR', 'SP'];
rarities.forEach(rarity => {
const count = rarityStats[rarity];
const rate = totalDraws > 0 ? (count / totalDraws * 100).toFixed(1) : 0;
document.getElementById(`${rarity.toLowerCase()}Count`).textContent = count;
document.getElementById(`${rarity.toLowerCase()}Rate`).style.width = rate + '%';
document.getElementById(`${rarity.toLowerCase()}Rate`).textContent = rate + '%';
});
}
// 加载排行榜
async function loadRankList() {
try {
console.log('正在请求排行榜数据...');
const response = await fetch(`${API_BASE}/stats/rank`, { headers });
console.log('排行榜响应状态:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('排行榜 API 错误响应:', errorText);
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json();
console.log('排行榜数据:', data);
if (data.success) {
const tbody = document.getElementById('rankListBody');
tbody.innerHTML = '';
data.data.forEach((user, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${index + 1}</td>
<td>${user.user_id}</td>
<td>${user.total_draws}</td>
<td class="rarity-r">${user.R_count}</td>
<td class="rarity-sr">${user.SR_count}</td>
<td class="rarity-ssr">${user.SSR_count}</td>
<td class="rarity-sp">${user.SP_count}</td>
<td><strong>${user.ssr_sp_total}</strong></td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="viewUserDetails('${user.user_id}')">
<i class="bi bi-eye"></i> 详情
</button>
</td>
`;
tbody.appendChild(row);
});
if (data.data.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" class="text-center">暂无数据</td></tr>';
}
} else {
console.error('排行榜 API 返回失败:', data);
document.getElementById('rankListBody').innerHTML = '<tr><td colspan="9" class="text-center">数据加载失败</td></tr>';
}
} catch (error) {
console.error('加载排行榜失败:', error);
document.getElementById('rankListBody').innerHTML = '<tr><td colspan="9" class="text-center">加载失败,请检查令牌</td></tr>';
}
}
// 刷新排行榜
function refreshRankList() {
loadRankList();
}
// 查询用户
async function queryUser() {
const userId = document.getElementById('userIdInput').value.trim();
if (!userId) {
alert('请输入用户ID');
return;
}
try {
const response = await fetch(`${API_BASE}/stats/user/${userId}`, { headers });
const data = await response.json();
if (data.success) {
document.getElementById('userStatsResult').style.display = 'block';
document.getElementById('userTotalDraws').textContent = data.total_draws;
document.getElementById('userRCount').textContent = data.R_count;
document.getElementById('userSRCount').textContent = data.SR_count;
document.getElementById('userSSRCount').textContent = data.SSR_count;
document.getElementById('userSPCount').textContent = data.SP_count;
// 显示最近抽卡记录
const recentDrawsDiv = document.getElementById('recentDraws');
if (data.recent_draws && data.recent_draws.length > 0) {
recentDrawsDiv.innerHTML = data.recent_draws.map(draw =>
`<div class="mb-1">
<span class="badge bg-secondary">${draw.date}</span>
<span class="badge rarity-${draw.rarity.toLowerCase()}">${draw.rarity}</span>
${draw.name}
</div>`
).join('');
} else {
recentDrawsDiv.innerHTML = '<p class="text-muted">暂无抽卡记录</p>';
}
} else {
alert('未找到用户数据');
document.getElementById('userStatsResult').style.display = 'none';
}
} catch (error) {
console.error('查询用户失败:', error);
alert('查询失败');
}
}
// 查看用户详情
function viewUserDetails(userId) {
document.getElementById('userIdInput').value = userId;
queryUser();
document.getElementById('achievementUserIdInput').value = userId;
queryAchievements();
// 滚动到用户查询区域
document.querySelector('.card:has(#userIdInput)').scrollIntoView({ behavior: 'smooth' });
}
// 查询成就
async function queryAchievements() {
const userId = document.getElementById('achievementUserIdInput').value.trim();
if (!userId) {
alert('请输入用户ID');
return;
}
try {
const response = await fetch(`${API_BASE}/achievements/${userId}`, { headers });
const data = await response.json();
if (data.success) {
document.getElementById('achievementResult').style.display = 'block';
// 显示已解锁成就
const unlockedDiv = document.getElementById('unlockedAchievements');
const achievements = Object.entries(data.achievements);
if (achievements.length > 0) {
unlockedDiv.innerHTML = achievements.map(([id, info]) =>
`<div class="achievement-card">
<div class="d-flex justify-content-between align-items-center">
<span>${id}</span>
<span class="badge bg-success">已解锁</span>
</div>
<small class="text-muted">${info.unlocked_date}</small>
</div>`
).join('');
} else {
unlockedDiv.innerHTML = '<p class="text-muted">暂无已解锁成就</p>';
}
// 显示成就进度
const progress = data.progress;
document.getElementById('consecutiveDays').textContent = progress.consecutive_days + ' 天';
document.getElementById('noSsrStreak').textContent = progress.no_ssr_streak + ' 次';
// 更新进度条
const consecutiveProgress = Math.min((progress.consecutive_days / 150) * 100, 100);
document.getElementById('consecutiveDaysProgress').style.width = consecutiveProgress + '%';
const noSsrProgress = Math.min((progress.no_ssr_streak / 180) * 100, 100);
document.getElementById('noSsrStreakProgress').style.width = noSsrProgress + '%';
} else {
alert('未找到用户成就数据');
document.getElementById('achievementResult').style.display = 'none';
}
} catch (error) {
console.error('查询成就失败:', error);
alert('查询失败');
}
}
// 每页记录数
const RECORDS_PER_PAGE = 20;
let currentRecords = [];
let currentPage = 1;
// 加载每日详细抽卡记录
async function loadDailyRecords() {
const date = document.getElementById('recordDateInput').value;
if (!date) {
alert('请选择日期');
return;
}
try {
console.log('正在请求每日详细抽卡记录...', date);
const response = await fetch(`${API_BASE}/records/daily?date=${date}`, { headers });
console.log('每日详细记录响应状态:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('每日详细记录 API 错误响应:', errorText);
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json();
console.log('每日详细记录数据:', data);
if (data.success) {
currentRecords = data.records;
currentPage = 1;
document.getElementById('totalRecordsCount').textContent = data.total_count;
displayRecords();
} else {
console.error('每日详细记录 API 返回失败:', data);
document.getElementById('dailyRecordsBody').innerHTML = '<tr><td colspan="5" class="text-center">暂无数据</td></tr>';
document.getElementById('totalRecordsCount').textContent = '0';
document.getElementById('recordsPagination').innerHTML = '';
}
} catch (error) {
console.error('加载每日详细记录失败:', error);
document.getElementById('dailyRecordsBody').innerHTML = '<tr><td colspan="5" class="text-center">加载失败,请检查令牌</td></tr>';
document.getElementById('totalRecordsCount').textContent = '0';
document.getElementById('recordsPagination').innerHTML = '';
}
}
// 显示记录(支持分页)
function displayRecords() {
const tbody = document.getElementById('dailyRecordsBody');
tbody.innerHTML = '';
// 计算分页
const startIndex = (currentPage - 1) * RECORDS_PER_PAGE;
const endIndex = Math.min(startIndex + RECORDS_PER_PAGE, currentRecords.length);
const pageRecords = currentRecords.slice(startIndex, endIndex);
if (pageRecords.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center">暂无数据</td></tr>';
document.getElementById('recordsPagination').innerHTML = '';
return;
}
// 显示当前页的记录
pageRecords.forEach(record => {
const row = document.createElement('tr');
// 格式化成就解锁信息
let achievementsHtml = '';
if (record.unlocked_achievements && record.unlocked_achievements.length > 0) {
achievementsHtml = record.unlocked_achievements.map(achievement =>
`<span class="badge bg-success me-1">${achievement}</span>`
).join('');
} else {
achievementsHtml = '<span class="text-muted">无</span>';
}
row.innerHTML = `
<td>${record.draw_time}</td>
<td>${record.user_id}</td>
<td>${record.shikigami_name}</td>
<td><span class="badge rarity-${record.rarity.toLowerCase()}">${record.rarity}</span></td>
<td>${achievementsHtml}</td>
`;
tbody.appendChild(row);
});
// 更新分页控件
updatePagination();
}
// 更新分页控件
function updatePagination() {
const totalPages = Math.ceil(currentRecords.length / RECORDS_PER_PAGE);
const pagination = document.getElementById('recordsPagination');
pagination.innerHTML = '';
if (totalPages <= 1) {
return;
}
// 上一页按钮
const prevLi = document.createElement('li');
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage - 1})">上一页</a>`;
pagination.appendChild(prevLi);
// 页码按钮
const maxVisiblePages = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
if (startPage > 1) {
const firstLi = document.createElement('li');
firstLi.className = 'page-item';
firstLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(1)">1</a>`;
pagination.appendChild(firstLi);
if (startPage > 2) {
const ellipsisLi = document.createElement('li');
ellipsisLi.className = 'page-item disabled';
ellipsisLi.innerHTML = `<a class="page-link" href="#">...</a>`;
pagination.appendChild(ellipsisLi);
}
}
for (let i = startPage; i <= endPage; i++) {
const pageLi = document.createElement('li');
pageLi.className = `page-item ${i === currentPage ? 'active' : ''}`;
pageLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${i})">${i}</a>`;
pagination.appendChild(pageLi);
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
const ellipsisLi = document.createElement('li');
ellipsisLi.className = 'page-item disabled';
ellipsisLi.innerHTML = `<a class="page-link" href="#">...</a>`;
pagination.appendChild(ellipsisLi);
}
const lastLi = document.createElement('li');
lastLi.className = 'page-item';
lastLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${totalPages})">${totalPages}</a>`;
pagination.appendChild(lastLi);
}
// 下一页按钮
const nextLi = document.createElement('li');
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage + 1})">下一页</a>`;
pagination.appendChild(nextLi);
}
// 切换页面
function changePage(page) {
const totalPages = Math.ceil(currentRecords.length / RECORDS_PER_PAGE);
if (page < 1 || page > totalPages) {
return;
}
currentPage = page;
displayRecords();
}
</script>
</body>
</html>