317 lines
8.7 KiB
JavaScript
317 lines
8.7 KiB
JavaScript
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);
|
||
});
|
||
}
|