This commit is contained in:
DengDai
2025-12-09 13:08:38 +08:00
commit 02ecea06f8
36 changed files with 5876 additions and 0 deletions

316
static/js/statistics.js Normal file
View File

@@ -0,0 +1,316 @@
let myChart, trendChart;
let isAdmin = false;
$(document).ready(function() {
// 设置默认日期范围最近30天
applyQuickSelect('30days');
// 加载统计数据
loadStatistics();
});
// 快捷时间选择
function applyQuickSelect(value) {
if (!value) {
value = $('#quick-select').val();
}
const today = new Date();
let dateFrom, dateTo;
switch(value) {
case 'today':
dateFrom = dateTo = today;
break;
case 'week':
dateFrom = new Date(today);
dateFrom.setDate(today.getDate() - today.getDay());
dateTo = today;
break;
case 'month':
dateFrom = new Date(today.getFullYear(), today.getMonth(), 1);
dateTo = today;
break;
case '30days':
dateFrom = new Date(today);
dateFrom.setDate(today.getDate() - 30);
dateTo = today;
break;
default:
return;
}
$('#date-from').val(dateFrom.toISOString().split('T')[0]);
$('#date-to').val(dateTo.toISOString().split('T')[0]);
$('#quick-select').val(value);
}
// 加载统计数据
function loadStatistics() {
loadOverview();
loadUserStatistics();
loadLeaderboard();
}
// 加载概览数据
function loadOverview() {
const dateFrom = $('#date-from').val();
const dateTo = $('#date-to').val();
$.ajax({
url: '/api/statistics/overview',
method: 'GET',
data: {
group_id: 1,
date_from: dateFrom,
date_to: dateTo
},
success: function(data) {
isAdmin = data.is_admin;
$('#today-count').text(data.today_completed);
$('#month-count').text(data.month_completed);
$('#pending-count').text(data.status_counts.pending || 0);
$('#claimed-count').text(data.status_counts.claimed || 0);
// 更新标题和统计数据
if (isAdmin) {
$('#stats-title').text('全局完成统计');
$('#trend-title').text('全局完成趋势');
// 管理员使用全局数据
$('#my-total').text(data.total_completed);
$('#my-pending').text(data.claimed_pending);
// 渲染管理员图表
renderMyChart(data.total_completed, data.claimed_pending);
} else {
$('#stats-title').text('我的完成统计');
$('#trend-title').text('我的完成趋势');
}
// 渲染趋势图
renderTrendChart(data.trend);
},
error: handleAjaxError
});
}
// 加载个人统计
function loadUserStatistics() {
const userStr = localStorage.getItem('current_user');
if (isAdmin) {
return;
}
if (!userStr) {
console.error('用户信息不存在');
$('#my-total').text('0');
$('#my-pending').text('0');
return;
}
let user;
try {
user = JSON.parse(userStr);
} catch (e) {
console.error('用户信息格式错误', e);
$('#my-total').text('0');
$('#my-pending').text('0');
return;
}
if (!user || !user.id) {
console.error('用户ID不存在');
$('#my-total').text('0');
$('#my-pending').text('0');
return;
}
const dateFrom = $('#date-from').val();
const dateTo = $('#date-to').val();
$.ajax({
url: `/api/statistics/user/${user.id}`,
method: 'GET',
data: {
group_id: 1,
date_from: dateFrom,
date_to: dateTo
},
success: function(data) {
$('#my-total').text(data.total_completed);
$('#my-pending').text(data.claimed_pending);
// 渲染个人图表
renderMyChart(data.total_completed, data.claimed_pending);
},
error: handleAjaxError
});
}
// 加载排行榜
function loadLeaderboard() {
$.ajax({
url: '/api/statistics/leaderboard',
method: 'GET',
data: {
group_id: 1,
period: 'monthly'
},
success: function(data) {
renderLeaderboard(data.leaderboard);
},
error: handleAjaxError
});
}
// 渲染个人图表
function renderMyChart(totalCompleted, claimedPending) {
const ctx = document.getElementById('myChart');
if (myChart) {
myChart.destroy();
}
myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['总完成数', '待完成数'],
datasets: [{
label: '任务数量',
data: [totalCompleted, claimedPending],
backgroundColor: [
'rgba(75, 192, 192, 0.5)',
'rgba(255, 159, 64, 0.5)'
],
borderColor: [
'rgba(75, 192, 192, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
}
// 渲染趋势图
function renderTrendChart(trend) {
const ctx = document.getElementById('trendChart');
if (trendChart) {
trendChart.destroy();
}
const labels = trend.map(t => t.date);
const data = trend.map(t => t.count);
trendChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: '完成数',
data: data,
fill: true,
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
}
// 渲染排行榜
function renderLeaderboard(leaderboard) {
const tbody = $('#leaderboard');
tbody.empty();
if (leaderboard.length === 0) {
tbody.append('<tr><td colspan="3" class="text-center">暂无数据</td></tr>');
return;
}
const currentUser = JSON.parse(localStorage.getItem('current_user'));
leaderboard.forEach(function(row) {
const isCurrentUser = row.user_id === currentUser.id;
const rowClass = isCurrentUser ? 'table-primary' : '';
let rankBadge = '';
if (row.rank === 1) {
rankBadge = '<i class="bi bi-trophy-fill text-warning"></i> ';
} else if (row.rank === 2) {
rankBadge = '<i class="bi bi-trophy-fill text-secondary"></i> ';
} else if (row.rank === 3) {
rankBadge = '<i class="bi bi-trophy-fill text-danger"></i> ';
}
tbody.append(`
<tr class="${rowClass}">
<td>${rankBadge}${row.rank}</td>
<td>${row.username}${isCurrentUser ? ' <span class="badge bg-primary">我</span>' : ''}</td>
<td><strong>${row.completed_count}</strong></td>
</tr>
`);
});
}
// 导出数据
function exportData() {
const dateFrom = $('#date-from').val();
const dateTo = $('#date-to').val();
const token = localStorage.getItem('access_token');
let url = '/api/statistics/export?group_id=1';
if (dateFrom) url += `&date_from=${dateFrom}`;
if (dateTo) url += `&date_to=${dateTo}`;
// 创建隐藏的下载链接
const link = document.createElement('a');
link.href = url;
link.download = `statistics_${new Date().toISOString().split('T')[0]}.csv`;
// 添加认证头通过fetch实现
fetch(url, {
headers: {
'Authorization': 'Bearer ' + token
}
})
.then(response => response.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `statistics_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
showSuccess('数据导出成功!');
})
.catch(error => {
showError('导出失败:' + error.message);
});
}