<!DOCTYPE html>
<html lang="zh-CN">
<head>
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#545BE8">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="/icons/icon-192.png">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据分析 - 超协体管理后台</title>
<script src="/js/auth.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
background: linear-gradient(135deg, var(--brand-navy) 0%, var(--brand-navy) 100%);
min-height: 100vh;
}
.admin-nav {
background: linear-gradient(135deg, var(--brand-purple) 0%, var(--brand-purple-dark) 100%);
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--surface-50);
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
}
.admin-logo {
font-size: 20px;
font-weight: bold;
display: flex;
align-items: center;
gap: 10px;
}
.admin-nav-links {
display: flex;
gap: 10px;
}
.admin-nav-link {
color: var(--surface-50);
text-decoration: none;
padding: 10px 18px;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 6px;
}
.admin-nav-link:hover,
.admin-nav-link.active {
background: rgba(255, 255, 255, 0.2);
}
.admin-nav-link.back {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.admin-container {
max-width: 1400px;
margin: 0 auto;
padding: 30px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.page-title {
color: var(--surface-50);
font-size: 28px;
font-weight: bold;
}
.time-selector {
display: flex;
gap: 10px;
}
.time-btn {
padding: 8px 16px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.3);
color: var(--surface-50);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s;
}
.time-btn:hover,
.time-btn.active {
background: rgba(255,255,255,0.2);
}
/* 图表网格 */
.charts-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 1024px) {
.charts-grid {
grid-template-columns: 1fr;
}
}
.chart-card {
background: rgba(255, 255, 255, 0.95);
padding: 25px;
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
}
.chart-card.full-width {
grid-column: span 2;
}
@media (max-width: 1024px) {
.chart-card.full-width {
grid-column: span 1;
}
}
.chart-title {
font-size: 16px;
font-weight: 600;
color: var(--text-main);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.chart-container {
position: relative;
height: 300px;
}
/* 任务完成率进度条 */
.progress-section {
margin-bottom: 15px;
}
.progress-label {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
font-size: 14px;
color: var(--text-secondary);
}
.progress-bar {
height: 12px;
background: var(--surface-200);
border-radius: 6px;
overflow: hidden;
}
.progress-fill {
height: 100%;
transition: width 0.5s;
}
.progress-fill.completed { background: linear-gradient(90deg, var(--brand-mint), var(--success)); }
.progress-fill.in-progress { background: linear-gradient(90deg, var(--warning), var(--warning)); }
.progress-fill.pending { background: linear-gradient(90deg, var(--brand-purple), var(--brand-purple-dark)); }
.progress-fill.blocked { background: linear-gradient(90deg, var(--error), var(--error)); }
/* 积分统计卡片 */
.points-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.points-stats {
grid-template-columns: 1fr;
}
}
.points-stat-item {
background: var(--surface-50);
padding: 20px;
border-radius: 12px;
text-align: center;
}
.points-stat-value {
font-size: 28px;
font-weight: bold;
margin-bottom: 5px;
}
.points-stat-value.issued { color: var(--brand-mint); }
.points-stat-value.consumed { color: var(--error); }
.points-stat-value.circulation { color: var(--brand-purple); }
.points-stat-label {
font-size: 13px;
color: var(--text-secondary);
}
/* 交易类型列表 */
.transaction-list {
margin-top: 20px;
}
.transaction-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--surface-200);
}
.transaction-item:last-child {
border-bottom: none;
}
.transaction-type {
font-size: 14px;
color: var(--text-main);
}
.transaction-stats {
display: flex;
gap: 20px;
font-size: 13px;
}
.transaction-count {
color: var(--text-secondary);
}
.transaction-total {
font-weight: 600;
}
.transaction-total.positive { color: var(--brand-mint); }
.transaction-total.negative { color: var(--error); }
/* AI使用统计 */
.ai-stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin-bottom: 20px;
}
@media (max-width: 900px) {
.ai-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.ai-stat-item {
background: linear-gradient(135deg, var(--brand-purple) 0%, var(--brand-purple-dark) 100%);
color: var(--surface-50);
padding: 20px;
border-radius: 12px;
text-align: center;
}
.ai-stat-value {
font-size: 32px;
font-weight: bold;
margin-bottom: 5px;
}
.ai-stat-label {
font-size: 12px;
opacity: 0.9;
}
/* 五行雷达图容器 */
.wuxing-container {
display: flex;
align-items: center;
gap: 30px;
}
@media (max-width: 768px) {
.wuxing-container {
flex-direction: column;
}
}
.wuxing-chart {
flex: 1;
min-width: 280px;
}
.wuxing-legend {
display: grid;
gap: 12px;
}
.wuxing-legend-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 15px;
background: var(--surface-50);
border-radius: 8px;
}
.wuxing-icon {
font-size: 20px;
}
.wuxing-name {
font-size: 14px;
color: var(--text-main);
width: 30px;
}
.wuxing-value {
font-weight: bold;
color: var(--brand-purple);
}
/* 加载状态 */
.loading {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--surface-50);
border-top: 3px solid var(--brand-purple);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式 */
@media (max-width: 768px) {
.admin-nav {
flex-direction: column;
gap: 15px;
}
.admin-nav-links {
flex-wrap: wrap;
justify-content: center;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.time-selector {
flex-wrap: wrap;
}
}
</style>
<link rel="stylesheet" href="https://unpkg.com/@phosphor-icons/web@2.1.1/src/bold/style.css">
<link rel="stylesheet" href="https://unpkg.com/@phosphor-icons/web@2.1.1/src/fill/style.css">
<link rel="stylesheet" href="/css/theme.css">
</head>
<body>
<!-- 导航栏 -->
<nav class="admin-nav">
<div class="admin-logo">
<span><i class="ph-bold ph-gear"></i></span>
<span>超协体管理后台</span>
</div>
<div class="admin-nav-links">
<a href="/admin.html" class="admin-nav-link">
<span><i class="ph-bold ph-chart-line"></i></span> 概览
</a>
<a href="/admin/users.html" class="admin-nav-link">
<span><i class="ph-bold ph-users"></i></span> 用户
</a>
<a href="/admin/tasks.html" class="admin-nav-link">
<span><i class="ph-bold ph-clipboard-text"></i></span> 任务
</a>
<a href="/admin/analytics.html" class="admin-nav-link active">
<span><i class="ph-bold ph-chart-bar"></i></span> 分析
</a>
<a href="/admin/settings.html" class="admin-nav-link">
<span><i class="ph-bold ph-gear"></i></span> 配置
</a>
<a href="/dashboard.html" class="admin-nav-link back">
<span><i class="ph-bold ph-arrow-left"></i></span> 返回工作台
</a>
</div>
</nav>
<!-- 主容器 -->
<div class="admin-container">
<div class="page-header">
<h1 class="page-title">数据分析</h1>
<div class="time-selector">
<button class="time-btn" data-days="7" onclick="setTimeRange(7)">最近7天</button>
<button class="time-btn active" data-days="30" onclick="setTimeRange(30)">最近30天</button>
<button class="time-btn" data-days="90" onclick="setTimeRange(90)">最近90天</button>
</div>
</div>
<!-- 图表网格 -->
<div class="charts-grid">
<!-- 用户增长趋势 -->
<div class="chart-card full-width">
<div class="chart-title">
<span><i class="ph-bold ph-chart-line"></i></span> 用户增长趋势
</div>
<div class="chart-container">
<canvas id="userGrowthChart"></canvas>
</div>
</div>
<!-- 任务完成率 -->
<div class="chart-card">
<div class="chart-title">
<span><i class="ph-bold ph-clipboard-text"></i></span> 任务完成率
</div>
<div id="taskCompletionContainer">
<div class="loading">
<div class="spinner"></div>
</div>
</div>
</div>
<!-- 五行能量分布 -->
<div class="chart-card">
<div class="chart-title">
<span><i class="ph-bold ph-yin-yang"></i></span> 五行能量分布
</div>
<div class="wuxing-container">
<div class="wuxing-chart">
<canvas id="wuxingChart"></canvas>
</div>
<div class="wuxing-legend" id="wuxingLegend">
<div class="loading">
<div class="spinner"></div>
</div>
</div>
</div>
</div>
<!-- 积分流转统计 -->
<div class="chart-card full-width">
<div class="chart-title">
<span><i class="ph-bold ph-money"></i></span> 积分流转统计
</div>
<div id="pointsStatsContainer">
<div class="loading">
<div class="spinner"></div>
</div>
</div>
</div>
<!-- AI使用统计 -->
<div class="chart-card full-width">
<div class="chart-title">
<span><i class="ph-bold ph-robot"></i></span> AI使用统计
</div>
<div id="aiUsageContainer">
<div class="loading">
<div class="spinner"></div>
</div>
</div>
</div>
</div>
</div>
<script>
const API_BASE = window.location.origin;
let userGrowthChart = null;
let wuxingChart = null;
let currentDays = 30;
// 检查管理员权限
async function checkAdminAccess() {
const token = localStorage.getItem('token');
if (!token) {
window.location.href = '/login.html';
return false;
}
try {
const response = await authFetch('/api/auth/me');
const data = await response.json();
if (!data.success || data.user.role !== 'admin') {
alert('您没有管理员权限');
window.location.href = '/dashboard.html';
return false;
}
return true;
} catch (error) {
window.location.href = '/login.html';
return false;
}
}
// 设置时间范围
function setTimeRange(days) {
currentDays = days;
document.querySelectorAll('.time-btn').forEach(btn => {
btn.classList.remove('active');
if (parseInt(btn.dataset.days) === days) {
btn.classList.add('active');
}
});
loadUserGrowth();
}
// 加载用户增长数据
async function loadUserGrowth() {
try {
const response = await authFetch(`/api/admin/analytics/users?days=${currentDays}`);
const data = await response.json();
if (data.success) {
renderUserGrowthChart(data.data);
}
} catch (error) {
console.error('加载用户增长数据失败:', error);
}
}
// 渲染用户增长图表
function renderUserGrowthChart(growthData) {
const ctx = document.getElementById('userGrowthChart').getContext('2d');
if (userGrowthChart) {
userGrowthChart.destroy();
}
const labels = growthData.map(d => d.date.substring(5)); // 只显示月-日
const newUsers = growthData.map(d => d.newUsers);
const totalUsers = growthData.map(d => d.totalUsers);
userGrowthChart = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [
{
label: '累计用户',
data: totalUsers,
borderColor: '#545BE8',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
fill: true,
tension: 0.4,
yAxisID: 'y'
},
{
label: '新增用户',
data: newUsers,
borderColor: '#74E8AC',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.4,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
scales: {
y: {
type: 'linear',
position: 'left',
title: {
display: true,
text: '累计用户'
}
},
y1: {
type: 'linear',
position: 'right',
title: {
display: true,
text: '新增用户'
},
grid: {
drawOnChartArea: false
}
}
},
plugins: {
legend: {
position: 'top'
}
}
}
});
}
// 加载任务统计
async function loadTaskStats() {
try {
const response = await authFetch('/api/admin/analytics/tasks');
const data = await response.json();
if (data.success) {
renderTaskCompletion(data.data);
}
} catch (error) {
console.error('加载任务统计失败:', error);
}
}
// 渲染任务完成率
function renderTaskCompletion(stats) {
const container = document.getElementById('taskCompletionContainer');
const total = stats.total || 1;
const completedPercent = Math.round((stats.completed / total) * 100);
const inProgressPercent = Math.round((stats.inProgress / total) * 100);
const pendingPercent = Math.round((stats.pending / total) * 100);
const blockedPercent = Math.round((stats.blocked / total) * 100);
container.innerHTML = `
<div style="text-align: center; margin-bottom: 20px;">
<div style="font-size: 48px; font-weight: bold; color: var(--brand-purple);">${stats.completionRate}%</div>
<div style="color: var(--text-secondary); font-size: 14px;">整体完成率</div>
</div>
<div class="progress-section">
<div class="progress-label">
<span>已完成</span>
<span>${stats.completed} (${completedPercent}%)</span>
</div>
<div class="progress-bar">
<div class="progress-fill completed" style="width: ${completedPercent}%"></div>
</div>
</div>
<div class="progress-section">
<div class="progress-label">
<span>进行中</span>
<span>${stats.inProgress} (${inProgressPercent}%)</span>
</div>
<div class="progress-bar">
<div class="progress-fill in-progress" style="width: ${inProgressPercent}%"></div>
</div>
</div>
<div class="progress-section">
<div class="progress-label">
<span>待处理</span>
<span>${stats.pending} (${pendingPercent}%)</span>
</div>
<div class="progress-bar">
<div class="progress-fill pending" style="width: ${pendingPercent}%"></div>
</div>
</div>
<div class="progress-section">
<div class="progress-label">
<span>已阻塞</span>
<span>${stats.blocked} (${blockedPercent}%)</span>
</div>
<div class="progress-bar">
<div class="progress-fill blocked" style="width: ${blockedPercent}%"></div>
</div>
</div>
`;
}
// 加载积分统计
async function loadPointsStats() {
try {
const response = await authFetch('/api/admin/analytics/points');
const data = await response.json();
if (data.success) {
renderPointsStats(data.data);
}
} catch (error) {
console.error('加载积分统计失败:', error);
}
}
// 渲染积分统计
function renderPointsStats(stats) {
const container = document.getElementById('pointsStatsContainer');
let transactionListHtml = '';
if (stats.topTypes && stats.topTypes.length > 0) {
transactionListHtml = stats.topTypes.map(t => `
<div class="transaction-item">
<span class="transaction-type">${t.name}</span>
<div class="transaction-stats">
<span class="transaction-count">${t.count}次</span>
<span class="transaction-total ${t.total >= 0 ? 'positive' : 'negative'}">
${t.total >= 0 ? '+' : ''}${t.total}
</span>
</div>
</div>
`).join('');
}
container.innerHTML = `
<div class="points-stats">
<div class="points-stat-item">
<div class="points-stat-value issued">+${stats.totalIssued}</div>
<div class="points-stat-label">总积分发放</div>
</div>
<div class="points-stat-item">
<div class="points-stat-value consumed">-${stats.totalConsumed}</div>
<div class="points-stat-label">总积分消耗</div>
</div>
<div class="points-stat-item">
<div class="points-stat-value circulation">${stats.totalCirculation}</div>
<div class="points-stat-label">当前流通</div>
</div>
</div>
<div style="text-align: center; color: var(--text-secondary); font-size: 13px; margin-bottom: 15px;">
共 ${stats.transactionCount} 笔交易记录
</div>
<div style="font-weight: 600; margin-bottom: 10px; color: var(--text-main);">Top交易类型</div>
<div class="transaction-list">
${transactionListHtml || '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">暂无交易记录</div>'}
</div>
`;
}
// 加载AI使用统计
async function loadAIUsage() {
try {
const response = await authFetch('/api/admin/analytics/ai-usage');
const data = await response.json();
if (data.success) {
renderAIUsage(data.data);
}
} catch (error) {
console.error('加载AI使用统计失败:', error);
}
}
// 渲染AI使用统计
function renderAIUsage(stats) {
const container = document.getElementById('aiUsageContainer');
let aiMembersHtml = '';
if (stats.aiMembers && stats.aiMembers.length > 0) {
aiMembersHtml = stats.aiMembers.map(ai => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background: var(--surface-50); border-radius: 8px;">
<div style="display: flex; align-items: center; gap: 10px;">
<div style="width: 36px; height: 36px; background: linear-gradient(135deg, var(--brand-mint), var(--success)); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--surface-50); font-weight: bold;">AI</div>
<div>
<div style="font-weight: 600; color: var(--text-main);">${ai.username}</div>
<div style="font-size: 12px; color: var(--text-secondary);">${ai.email}</div>
</div>
</div>
<div style="text-align: right;">
<div style="font-weight: bold; color: var(--brand-purple);">${ai.stats?.assigned || 0} 任务</div>
<div style="font-size: 12px; color: var(--brand-mint);">${ai.stats?.completed || 0} 已完成</div>
</div>
</div>
`).join('');
}
container.innerHTML = `
<div class="ai-stats-grid">
<div class="ai-stat-item">
<div class="ai-stat-value">${stats.aiMemberCount}</div>
<div class="ai-stat-label">AI成员数</div>
</div>
<div class="ai-stat-item" style="background: linear-gradient(135deg, var(--brand-mint), var(--success));">
<div class="ai-stat-value">${stats.aiTaskCount}</div>
<div class="ai-stat-label">AI执行任务数</div>
</div>
<div class="ai-stat-item" style="background: linear-gradient(135deg, var(--warning), var(--warning));">
<div class="ai-stat-value">${stats.aiCompletedTaskCount}</div>
<div class="ai-stat-label">AI完成任务数</div>
</div>
<div class="ai-stat-item" style="background: linear-gradient(135deg, var(--brand-purple-dark), var(--brand-purple-dark));">
<div class="ai-stat-value">${stats.multiAICount}</div>
<div class="ai-stat-label">多AI协作次数</div>
</div>
</div>
${stats.mostActiveAI ? `
<div style="background: rgba(245, 158, 11, 0.18); padding: 15px; border-radius: 12px; margin-bottom: 20px;">
<div style="font-size: 13px; color: var(--warning); margin-bottom: 5px;">最活跃AI成员</div>
<div style="font-weight: 600; color: var(--text-main);">${stats.mostActiveAI.name} - ${stats.mostActiveAI.assigned}次任务</div>
</div>
` : ''}
<div style="font-weight: 600; margin-bottom: 15px; color: var(--text-main);">AI成员列表</div>
<div style="display: grid; gap: 10px;">
${aiMembersHtml || '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">暂无AI成员</div>'}
</div>
`;
}
// 加载五行分布
async function loadWuxingStats() {
try {
const response = await authFetch('/api/admin/analytics/wuxing');
const data = await response.json();
if (data.success) {
renderWuxingChart(data.data);
}
} catch (error) {
console.error('加载五行数据失败:', error);
}
}
// 渲染五行雷达图
function renderWuxingChart(stats) {
const ctx = document.getElementById('wuxingChart').getContext('2d');
const legend = document.getElementById('wuxingLegend');
if (wuxingChart) {
wuxingChart.destroy();
}
const avg = stats.average || { fire: 0, metal: 0, wood: 0, water: 0, earth: 0 };
wuxingChart = new Chart(ctx, {
type: 'radar',
data: {
labels: ['火', '金', '木', '水', '土'],
datasets: [{
label: '团队平均',
data: [avg.fire, avg.metal, avg.wood, avg.water, avg.earth],
borderColor: '#545BE8',
backgroundColor: 'rgba(102, 126, 234, 0.2)',
borderWidth: 2,
pointBackgroundColor: '#545BE8'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
r: {
beginAtZero: true,
max: 100,
ticks: {
stepSize: 20
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
// 渲染图例
const wuxingData = [
{ icon: '<i class="ph-bold ph-fire"></i>', name: '火', value: avg.fire, color: '#EF4444' },
{ icon: '<i class="ph-bold ph-gear"></i>', name: '金', value: avg.metal, color: '#F59E0B' },
{ icon: '<i class="ph-bold ph-tree"></i>', name: '木', value: avg.wood, color: '#74E8AC' },
{ icon: '<i class="ph-bold ph-drop"></i>', name: '水', value: avg.water, color: '#3B82F6' },
{ icon: '<i class="ph-bold ph-mountains"></i>', name: '土', value: avg.earth, color: '#3A40B5' }
];
legend.innerHTML = wuxingData.map(w => `
<div class="wuxing-legend-item">
<span class="wuxing-icon">${w.icon}</span>
<span class="wuxing-name">${w.name}</span>
<span class="wuxing-value" style="color: ${w.color};">${w.value}</span>
</div>
`).join('');
// 添加统计信息
legend.innerHTML += `
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--surface-200);">
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 8px;">样本数: ${stats.userCount} 人</div>
<div style="font-size: 12px; color: var(--brand-mint);">最强: ${stats.strongest?.element || '-'} (${stats.strongest?.value || 0})</div>
<div style="font-size: 12px; color: var(--error);">最弱: ${stats.weakest?.element || '-'} (${stats.weakest?.value || 0})</div>
</div>
`;
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', async () => {
if (await checkAdminAccess()) {
loadUserGrowth();
loadTaskStats();
loadPointsStats();
loadAIUsage();
loadWuxingStats();
}
});
</script>
<script src="/js/pwa.js" defer></script>
</body>
</html>