Files
ns2.0/templates/admin_dashboard.html

618 lines
18 KiB
HTML
Executable File

{% extends "base.html" %}
{% block content %}
{% include 'admin_nav.html' %}
<div class="admin-container">
<div class="admin-content">
<div class="dashboard-header">
<h2>网站概览</h2>
<div class="refresh-btn" onclick="location.reload()">
<i class="fas fa-sync-alt"></i>
刷新数据
</div>
</div>
<!-- 数据卡片 -->
<div class="stat-cards">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-users"></i>
</div>
<div class="stat-info">
<div class="stat-title">总用户数</div>
<div class="stat-value">{{ stats.total_users }}</div>
<div class="stat-subtitle">
今日新增 {{ stats.today_users }}
<br>
今日邀请 {{ stats.today_inviters or 0 }} / 被邀请 {{ stats.today_invitees or 0 }}
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-heart"></i>
</div>
<div class="stat-info">
<div class="stat-title">心愿单总数</div>
<div class="stat-value">{{ stats.total_wishlists }}</div>
<div class="stat-subtitle">今日新增 {{ stats.today_wishlists }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-bell"></i>
</div>
<div class="stat-info">
<div class="stat-title">通知状态</div>
<div class="stat-value">{{ stats.notified_wishlists }}</div>
<div class="stat-subtitle">
已通知 {{ stats.notified_wishlists }} / 未通知 {{ stats.unnotified_wishlists }}
<br>
今日通知 {{ stats.today_notified }}
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-mobile-alt"></i>
</div>
<div class="stat-info">
<div class="stat-title">应用总数</div>
<div class="stat-value">{{ stats.total_apps }}</div>
<div class="stat-subtitle">
手机端 {{ stats.mobile_apps }} / 平板端 {{ stats.tablet_apps }}
<br>
今日新增 {{ stats.today_apps }}
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-chart-line"></i>
</div>
<div class="stat-info">
<div class="stat-title">通知成功率</div>
{% set success_rate = (stats.notified_wishlists / stats.total_wishlists * 100)|round(2) if stats.total_wishlists > 0 else 0 %}
<div class="stat-value">{{ success_rate }}%</div>
<div class="stat-subtitle">
总通知数 {{ stats.total_wishlists }}
<br>
成功 {{ stats.notified_wishlists }}
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-user-check"></i>
</div>
<div class="stat-info">
<div class="stat-title">活跃用户</div>
<div class="stat-value">{{ stats.user_activity.active_users }}</div>
<div class="stat-subtitle">
活跃率 {{ stats.user_activity.active_rate }}%
<br>
总用户 {{ stats.total_users }}
</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="chart-section">
<div class="chart-card">
<h3>最近7天注册趋势</h3>
<canvas id="trendChart"></canvas>
</div>
<div class="chart-card">
<h3>热门应用 TOP10</h3>
<canvas id="popularChart"></canvas>
</div>
<div class="chart-card">
<h3>应用分类分布</h3>
<canvas id="categoryChart"></canvas>
</div>
<div class="chart-card">
<h3>最近7天心愿单趋势</h3>
<canvas id="wishlistTrendChart"></canvas>
</div>
<div class="chart-card">
<h3>最近30天应用增长趋势</h3>
<canvas id="appGrowthChart"></canvas>
</div>
<div class="chart-card">
<h3>用户活跃度分析</h3>
<canvas id="userActivityChart"></canvas>
</div>
</div>
</div>
</div>
<style>
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.refresh-btn {
padding: 8px 16px;
background: #f5f5f5;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.refresh-btn:hover {
background: #e8e8e8;
}
.stat-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 20px;
display: flex;
align-items: center;
gap: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: rgba(24, 144, 255, 0.1);
color: #1890ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.stat-info {
flex: 1;
}
.stat-title {
color: #666;
font-size: 14px;
margin-bottom: 4px;
}
.stat-value {
font-size: 24px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
}
.stat-subtitle {
color: #999;
font-size: 12px;
line-height: 1.5;
}
.stat-subtitle br {
content: '';
display: block;
margin: 2px 0;
}
.chart-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
}
.chart-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.chart-card h3 {
font-size: 16px;
color: #333;
}
/* 暗色模式适配 */
[data-theme="dark"] .stat-card,
[data-theme="dark"] .chart-card {
background: #242424;
}
[data-theme="dark"] .refresh-btn {
background: #333;
color: #fff;
}
[data-theme="dark"] .refresh-btn:hover {
background: #444;
}
[data-theme="dark"] .stat-title {
color: #999;
}
[data-theme="dark"] .stat-value {
color: #fff;
}
[data-theme="dark"] .chart-card h3 {
color: #fff;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<script>
// 全局统计数据
const stats = {{ stats|tojson|safe }};
// 趋势图
const trendCtx = document.getElementById('trendChart').getContext('2d');
const trendDates = {{ stats.daily_stats|map(attribute='date')|list|tojson|safe }}.reverse();
const trendCounts = {{ stats.daily_stats|map(attribute='count')|list|tojson|safe }}.reverse();
new Chart(trendCtx, {
type: 'line',
data: {
labels: trendDates,
datasets: [{
label: '每日注册',
data: trendCounts,
borderColor: '#1890ff',
backgroundColor: 'rgba(24, 144, 255, 0.1)',
fill: true,
tension: 0.1,
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
title: function(context) {
return '日期:' + context[0].label;
},
label: function(context) {
return '注册人数:' + context.raw;
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
// 热门应用图
const popularCtx = document.getElementById('popularChart').getContext('2d');
new Chart(popularCtx, {
type: 'bar',
data: {
labels: {{ stats.popular_apps|map(attribute='app_name')|list|tojson|safe }},
datasets: [{
label: '关注人数',
data: {{ stats.popular_apps|map(attribute='count')|list|tojson|safe }},
backgroundColor: '#1890ff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
aspectRatio: 1,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
title: function(context) {
return context[0].label;
},
label: function(context) {
return `关注人数: ${context.raw}`;
}
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
beginAtZero: true,
grid: {
color: 'rgba(0, 0, 0, 0.1)'
},
ticks: {
stepSize: 1
}
}
}
}
});
// 分类分布图
const categoryCtx = document.getElementById('categoryChart').getContext('2d');
new Chart(categoryCtx, {
type: 'doughnut',
data: {
labels: {{ stats.category_stats|map(attribute='name')|list|tojson|safe }},
datasets: [{
data: {{ stats.category_stats|map(attribute='count')|list|tojson|safe }},
backgroundColor: [
'#1890ff', '#13c2c2', '#52c41a', '#faad14', '#f5222d',
'#722ed1', '#eb2f96', '#fa8c16', '#a0d911', '#fadb14'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
boxWidth: 12,
padding: 10
}
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = Math.round((value / total) * 100);
return `${label}: ${value} (${percentage}%)`;
}
}
}
}
}
});
// 心愿单趋势图
const wishlistTrendCtx = document.getElementById('wishlistTrendChart').getContext('2d');
const wishlistDates = {{ stats.wishlist_stats|map(attribute='date')|list|tojson|safe }}.reverse();
const wishlistCounts = {{ stats.wishlist_stats|map(attribute='count')|list|tojson|safe }}.reverse();
new Chart(wishlistTrendCtx, {
type: 'line',
data: {
labels: wishlistDates,
datasets: [{
label: '新增心愿',
data: wishlistCounts,
borderColor: '#13c2c2',
backgroundColor: 'rgba(19, 194, 194, 0.1)',
fill: true,
tension: 0.1,
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
title: function(context) {
return '日期:' + context[0].label;
},
label: function(context) {
return '新增心愿:' + context.raw;
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
// 应用增长趋势图
const appGrowthCtx = document.getElementById('appGrowthChart').getContext('2d');
new Chart(appGrowthCtx, {
type: 'line',
data: {
labels: {{ stats.app_growth|map(attribute='date')|list|tojson|safe }},
datasets: [{
label: '总数',
data: {{ stats.app_growth|map(attribute='count')|list|tojson|safe }},
borderColor: '#1890ff',
backgroundColor: 'rgba(24, 144, 255, 0.1)',
fill: true
}, {
label: '手机端',
data: {{ stats.app_growth|map(attribute='mobile_count')|list|tojson|safe }},
borderColor: '#52c41a',
backgroundColor: 'rgba(82, 196, 26, 0.1)',
fill: true
}, {
label: '平板端',
data: {{ stats.app_growth|map(attribute='tablet_count')|list|tojson|safe }},
borderColor: '#722ed1',
backgroundColor: 'rgba(114, 46, 209, 0.1)',
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
y: {
beginAtZero: true,
stacked: false
}
}
}
});
// 用户活跃度分析图
const userActivityCtx = document.getElementById('userActivityChart').getContext('2d');
new Chart(userActivityCtx, {
type: 'bar',
data: {
labels: {{ stats.user_activity.details|map(attribute='activity_level')|list|tojson|safe }},
datasets: [{
label: '用户数',
data: {{ stats.user_activity.details|map(attribute='user_count')|list|tojson|safe }},
backgroundColor: [
'#ff4d4f', // 不活跃 - 红色
'#faad14', // 轻度活跃 - 橙色
'#52c41a', // 中度活跃 - 绿色
'#1890ff' // 高度活跃 - 蓝色
],
order: 1
}, {
label: '平均心愿单数',
data: {{ stats.user_activity.details|map(attribute='avg_total_wishlists')|list|tojson|safe }},
type: 'line',
borderColor: '#722ed1',
backgroundColor: 'rgba(114, 46, 209, 0.1)',
yAxisID: 'y1',
order: 0,
fill: false,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: {
display: true,
position: 'top'
},
tooltip: {
callbacks: {
label: function(context) {
const details = {{ stats.user_activity.details|tojson|safe }};
const data = details[context.dataIndex];
if (context.dataset.label === '用户数') {
const total = {{ stats.total_users }};
const percentage = Math.round((context.raw / total) * 100);
return [
`用户数: ${context.raw} 人 (${percentage}%)`,
`心愿单范围: ${data.min_wishlists} - ${data.max_wishlists}`,
`平均每日添加: ${data.avg_daily_activity}`,
`平均活跃天数: ${data.avg_active_days}`,
`平均活跃周数: ${data.avg_active_weeks}`,
`最近7天活跃: ${data.active_users_last_week}`,
`最近24小时活跃: ${data.active_users_last_day}`,
`平均账龄: ${data.avg_user_age_days}`,
`上次活动: ${data.avg_days_since_last_activity || 'N/A'} 天前`
];
} else {
return `平均心愿单数: ${context.raw.toFixed(1)}`;
}
}
}
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '用户数'
}
},
y1: {
beginAtZero: true,
position: 'right',
title: {
display: true,
text: '平均心愿单数'
},
grid: {
drawOnChartArea: false
}
}
}
}
});
// 设置图表容器高度
document.querySelectorAll('.chart-card').forEach(card => {
card.style.height = '400px';
});
// 检查暗色模式并更新图表
function updateChartsTheme() {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
Chart.defaults.color = isDark ? '#999' : '#666';
Chart.defaults.borderColor = isDark ? '#333' : '#eee';
}
updateChartsTheme();
document.addEventListener('themeChanged', updateChartsTheme);
</script>
{% endblock %}