618 lines
18 KiB
HTML
Executable File
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 %} |