init
This commit is contained in:
2018
static/css/bootstrap-icons.css
vendored
Normal file
2018
static/css/bootstrap-icons.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
static/css/bootstrap.min.css
vendored
Normal file
6
static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/css/fonts/bootstrap-icons.woff
Normal file
BIN
static/css/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
static/css/fonts/bootstrap-icons.woff2
Normal file
BIN
static/css/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
74
static/css/style.css
Normal file
74
static/css/style.css
Normal file
@@ -0,0 +1,74 @@
|
||||
body {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
|
||||
border: none;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.badge-status {
|
||||
padding: 0.35em 0.65em;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status-claimed {
|
||||
background-color: #17a2b8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #28a745;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.priority-high {
|
||||
color: #dc3545;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.priority-medium {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.priority-low {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
#task-detail dt {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
#task-detail dd {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
}
|
||||
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
20
static/js/chart.umd.min.js
vendored
Normal file
20
static/js/chart.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
121
static/js/common.js
Normal file
121
static/js/common.js
Normal file
@@ -0,0 +1,121 @@
|
||||
// 通用工具函数
|
||||
// AJAX全局设置
|
||||
$.ajaxSetup({
|
||||
beforeSend: function(xhr, settings) {
|
||||
if (!settings.url.includes('/api/login')) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// 检查登录状态
|
||||
function checkAuth() {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) {
|
||||
window.location.href = '/';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取当前用户信息
|
||||
function getCurrentUser() {
|
||||
if (!checkAuth()) return;
|
||||
|
||||
$.ajax({
|
||||
url: '/api/me',
|
||||
method: 'GET',
|
||||
success: function(data) {
|
||||
$('#current-username').text(data.username);
|
||||
localStorage.setItem('current_user', JSON.stringify(data));
|
||||
|
||||
if (data.role === 'admin') {
|
||||
$('.admin-only').show();
|
||||
$('#nav-users').show();
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status === 401 || xhr.status === 422) {
|
||||
logout();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
function logout() {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('current_user');
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('zh-CN');
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
function showError(message) {
|
||||
alert(message);
|
||||
}
|
||||
|
||||
// 显示成功消息
|
||||
function showSuccess(message) {
|
||||
alert(message);
|
||||
}
|
||||
|
||||
// AJAX错误处理
|
||||
function handleAjaxError(xhr) {
|
||||
if (xhr.status === 401) {
|
||||
alert('登录已过期,请重新登录');
|
||||
logout();
|
||||
} else if (xhr.status === 403 && xhr.responseJSON?.code === 'USER_DISABLED') {
|
||||
alert('您的账号已被禁用,请联系管理员');
|
||||
logout();
|
||||
} else {
|
||||
const error = xhr.responseJSON?.error || '操作失败';
|
||||
showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态徽章HTML
|
||||
function getStatusBadge(status) {
|
||||
const statusMap = {
|
||||
'pending': { text: '待认领', class: 'status-pending' },
|
||||
'claimed': { text: '已认领', class: 'status-claimed' },
|
||||
'completed': { text: '已完成', class: 'status-completed' }
|
||||
};
|
||||
|
||||
const info = statusMap[status] || { text: status, class: '' };
|
||||
return `<span class="badge badge-status ${info.class}">${info.text}</span>`;
|
||||
}
|
||||
|
||||
// 获取优先级样式类
|
||||
function getPriorityClass(priority) {
|
||||
const map = {
|
||||
'高': 'priority-high',
|
||||
'中': 'priority-medium',
|
||||
'低': 'priority-low'
|
||||
};
|
||||
return map[priority] || '';
|
||||
}
|
||||
|
||||
// 页面加载时初始化
|
||||
$(document).ready(function() {
|
||||
// 如果不是登录页,检查认证
|
||||
if (!window.location.pathname.match(/^\/?$/)) {
|
||||
checkAuth();
|
||||
getCurrentUser();
|
||||
}
|
||||
});
|
||||
2
static/js/jquery.min.js
vendored
Normal file
2
static/js/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
33
static/js/login.js
Normal file
33
static/js/login.js
Normal file
@@ -0,0 +1,33 @@
|
||||
$(document).ready(function() {
|
||||
// 如果已登录,跳转到任务列表
|
||||
if (localStorage.getItem('access_token')) {
|
||||
window.location.href = '/tasks';
|
||||
return;
|
||||
}
|
||||
|
||||
$('#login-form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = $('#username').val();
|
||||
const password = $('#password').val();
|
||||
|
||||
$.ajax({
|
||||
url: '/api/login',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
username: username,
|
||||
password: password
|
||||
}),
|
||||
success: function(response) {
|
||||
localStorage.setItem('access_token', response.access_token);
|
||||
localStorage.setItem('current_user', JSON.stringify(response.user));
|
||||
window.location.href = '/tasks';
|
||||
},
|
||||
error: function(xhr) {
|
||||
const error = xhr.responseJSON?.error || '登录失败';
|
||||
$('#error-msg').text(error).show();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
76
static/js/profile.js
Normal file
76
static/js/profile.js
Normal file
@@ -0,0 +1,76 @@
|
||||
$(document).ready(function() {
|
||||
loadProfile();
|
||||
|
||||
$('#profile-form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
updateProfile();
|
||||
});
|
||||
|
||||
$('#password-form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
changePassword();
|
||||
});
|
||||
});
|
||||
|
||||
function loadProfile() {
|
||||
$.ajax({
|
||||
url: '/api/users/profile',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||
},
|
||||
success: function(data) {
|
||||
$('#username').val(data.username);
|
||||
$('#email').val(data.email);
|
||||
$('#uid').val(data.uid);
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
|
||||
function updateProfile() {
|
||||
$.ajax({
|
||||
url: '/api/users/profile',
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||
},
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
email: $('#email').val(),
|
||||
uid: $('#uid').val()
|
||||
}),
|
||||
success: function(data) {
|
||||
alert(data.message);
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
|
||||
function changePassword() {
|
||||
const newPassword = $('#new-password').val();
|
||||
const confirmPassword = $('#confirm-password').val();
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
alert('两次密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/api/users/change-password',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||
},
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
old_password: $('#old-password').val(),
|
||||
new_password: newPassword
|
||||
}),
|
||||
success: function(data) {
|
||||
alert(data.message);
|
||||
$('#password-form')[0].reset();
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
32
static/js/register.js
Normal file
32
static/js/register.js
Normal file
@@ -0,0 +1,32 @@
|
||||
$(document).ready(function() {
|
||||
$('#register-form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const password = $('#password').val();
|
||||
const confirmPassword = $('#confirm-password').val();
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
alert('两次密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/api/register',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
username: $('#username').val(),
|
||||
email: $('#email').val(),
|
||||
uid: $('#uid').val(),
|
||||
password: password
|
||||
}),
|
||||
success: function(data) {
|
||||
alert(data.message);
|
||||
window.location.href = '/';
|
||||
},
|
||||
error: function(xhr) {
|
||||
alert(xhr.responseJSON?.error || '注册失败');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
316
static/js/statistics.js
Normal file
316
static/js/statistics.js
Normal 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);
|
||||
});
|
||||
}
|
||||
329
static/js/task_list.js
Normal file
329
static/js/task_list.js
Normal file
@@ -0,0 +1,329 @@
|
||||
let currentPage = 1;
|
||||
let claimModal, completeModal, detailModal;
|
||||
|
||||
$(document).ready(function() {
|
||||
// 初始化模态框
|
||||
claimModal = new bootstrap.Modal(document.getElementById('claimModal'));
|
||||
completeModal = new bootstrap.Modal(document.getElementById('completeModal'));
|
||||
detailModal = new bootstrap.Modal(document.getElementById('detailModal'));
|
||||
|
||||
// 加载任务列表
|
||||
loadTasks();
|
||||
});
|
||||
|
||||
// 加载任务列表
|
||||
function loadTasks(page = 1) {
|
||||
const status = $('#filter-status').val();
|
||||
const scope = $('#filter-scope').val();
|
||||
|
||||
let params = {
|
||||
page: page,
|
||||
per_page: 20
|
||||
};
|
||||
|
||||
if (status) {
|
||||
params.status = status;
|
||||
}
|
||||
|
||||
if (scope === 'my') {
|
||||
const user = JSON.parse(localStorage.getItem('current_user'));
|
||||
params.user_id = user.id;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/api/groups/1/tasks',
|
||||
method: 'GET',
|
||||
data: params,
|
||||
success: function(response) {
|
||||
renderTasks(response.tasks);
|
||||
renderPagination(response.page, response.pages);
|
||||
currentPage = page;
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染任务列表
|
||||
function renderTasks(tasks) {
|
||||
const tbody = $('#task-list');
|
||||
tbody.empty();
|
||||
|
||||
if (tasks.length === 0) {
|
||||
tbody.append('<tr><td colspan="9" class="text-center">暂无数据</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUser = JSON.parse(localStorage.getItem('current_user'));
|
||||
|
||||
tasks.forEach(function(task) {
|
||||
const priorityClass = getPriorityClass(task.priority);
|
||||
|
||||
let actions = '';
|
||||
|
||||
// 待认领状态 - 所有人可认领
|
||||
if (task.status === 'pending') {
|
||||
actions = `<button class="btn btn-sm btn-primary" onclick="openClaimModal(${task.id})">
|
||||
<i class="bi bi-hand-thumbs-up"></i> 认领
|
||||
</button>`;
|
||||
}
|
||||
|
||||
// 已认领状态 - 认领人可完成或取消
|
||||
if (task.status === 'claimed' && task.claimed_by_id === currentUser.id) {
|
||||
actions = `
|
||||
<button class="btn btn-sm btn-success" onclick="openCompleteModal(${task.id})">
|
||||
<i class="bi bi-check-circle"></i> 完成
|
||||
</button>
|
||||
<button class="btn btn-sm btn-warning" onclick="cancelClaim(${task.id})">
|
||||
<i class="bi bi-x-circle"></i> 取消
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// 查看详情按钮
|
||||
actions += ` <button class="btn btn-sm btn-info" onclick="viewDetail(${task.id})">
|
||||
<i class="bi bi-eye"></i> 详情
|
||||
</button>`;
|
||||
|
||||
// 管理员可删除
|
||||
if (currentUser.role === 'admin') {
|
||||
actions += ` <button class="btn btn-sm btn-danger" onclick="deleteTask(${task.id})">
|
||||
<i class="bi bi-trash"></i> 删除
|
||||
</button>`;
|
||||
}
|
||||
|
||||
const row = `
|
||||
<tr>
|
||||
<td>${task.id}</td>
|
||||
<td>
|
||||
${task.series_link ?
|
||||
`<a href="${task.series_link}" target="_blank">${task.series_name}</a>` :
|
||||
task.series_name
|
||||
}
|
||||
</td>
|
||||
<td>${formatDate(task.series_date)}</td>
|
||||
<td><span class="${priorityClass}">${task.priority || '-'}</span></td>
|
||||
<td>${getStatusBadge(task.status)}</td>
|
||||
<td>${task.claimed_by || '-'}</td>
|
||||
<td>${task.torrent_id || '-'}</td>
|
||||
<td>${formatDateTime(task.created_at)}</td>
|
||||
<td>${actions}</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
tbody.append(row);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染分页
|
||||
function renderPagination(current, total) {
|
||||
const pagination = $('#pagination');
|
||||
pagination.empty();
|
||||
|
||||
if (total <= 1) return;
|
||||
|
||||
// 上一页
|
||||
pagination.append(`
|
||||
<li class="page-item ${current === 1 ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadTasks(${current - 1}); return false;">上一页</a>
|
||||
</li>
|
||||
`);
|
||||
|
||||
// 页码
|
||||
for (let i = 1; i <= total; i++) {
|
||||
if (i === 1 || i === total || (i >= current - 2 && i <= current + 2)) {
|
||||
pagination.append(`
|
||||
<li class="page-item ${i === current ? 'active' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadTasks(${i}); return false;">${i}</a>
|
||||
</li>
|
||||
`);
|
||||
} else if (i === current - 3 || i === current + 3) {
|
||||
pagination.append('<li class="page-item disabled"><span class="page-link">...</span></li>');
|
||||
}
|
||||
}
|
||||
|
||||
// 下一页
|
||||
pagination.append(`
|
||||
<li class="page-item ${current === total ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadTasks(${current + 1}); return false;">下一页</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
|
||||
// 打开认领模态框
|
||||
function openClaimModal(taskId) {
|
||||
$('#claim-task-id').val(taskId);
|
||||
$('#claim-note').val('');
|
||||
claimModal.show();
|
||||
}
|
||||
|
||||
// 提交认领
|
||||
function submitClaim() {
|
||||
const taskId = $('#claim-task-id').val();
|
||||
const note = $('#claim-note').val();
|
||||
|
||||
$.ajax({
|
||||
url: `/api/tasks/${taskId}/claim`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify({
|
||||
claim_note: note
|
||||
}),
|
||||
success: function() {
|
||||
claimModal.hide();
|
||||
showSuccess('认领成功!');
|
||||
loadTasks(currentPage);
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
|
||||
// 打开完成模态框
|
||||
function openCompleteModal(taskId) {
|
||||
$('#complete-task-id').val(taskId);
|
||||
$('#torrent-id').val('');
|
||||
$('#complete-note').val('');
|
||||
completeModal.show();
|
||||
}
|
||||
|
||||
// 提交完成
|
||||
function submitComplete() {
|
||||
const taskId = $('#complete-task-id').val();
|
||||
const torrentId = $('#torrent-id').val();
|
||||
const note = $('#complete-note').val();
|
||||
|
||||
if (!torrentId) {
|
||||
alert('请填写种子ID');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: `/api/tasks/${taskId}/complete`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify({
|
||||
torrent_id: torrentId,
|
||||
complete_note: note
|
||||
}),
|
||||
success: function() {
|
||||
completeModal.hide();
|
||||
showSuccess('任务完成!');
|
||||
loadTasks(currentPage);
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
|
||||
// 取消认领
|
||||
function cancelClaim(taskId) {
|
||||
if (!confirm('确定要取消认领吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: `/api/tasks/${taskId}/cancel-claim`,
|
||||
method: 'POST',
|
||||
success: function() {
|
||||
showSuccess('已取消认领');
|
||||
loadTasks(currentPage);
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function viewDetail(taskId) {
|
||||
$.ajax({
|
||||
url: `/api/tasks/${taskId}`,
|
||||
method: 'GET',
|
||||
success: function(task) {
|
||||
const html = `
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">任务ID</dt>
|
||||
<dd class="col-sm-9">${task.id}</dd>
|
||||
|
||||
<dt class="col-sm-3">剧集名称</dt>
|
||||
<dd class="col-sm-9">${task.series_name}</dd>
|
||||
|
||||
<dt class="col-sm-3">剧集链接</dt>
|
||||
<dd class="col-sm-9">
|
||||
${task.series_link ?
|
||||
`<a href="${task.series_link}" target="_blank">${task.series_link}</a>` :
|
||||
'-'
|
||||
}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">更新日期</dt>
|
||||
<dd class="col-sm-9">${formatDate(task.series_date)}</dd>
|
||||
|
||||
<dt class="col-sm-3">优先级</dt>
|
||||
<dd class="col-sm-9"><span class="${getPriorityClass(task.priority)}">${task.priority || '-'}</span></dd>
|
||||
|
||||
<dt class="col-sm-3">状态</dt>
|
||||
<dd class="col-sm-9">${getStatusBadge(task.status)}</dd>
|
||||
|
||||
<dt class="col-sm-3">创建人</dt>
|
||||
<dd class="col-sm-9">${task.creator}</dd>
|
||||
|
||||
<dt class="col-sm-3">创建时间</dt>
|
||||
<dd class="col-sm-9">${formatDateTime(task.created_at)}</dd>
|
||||
|
||||
${task.claimed_by ? `
|
||||
<dt class="col-sm-3">认领人</dt>
|
||||
<dd class="col-sm-9">${task.claimed_by}</dd>
|
||||
|
||||
<dt class="col-sm-3">认领时间</dt>
|
||||
<dd class="col-sm-9">${formatDateTime(task.claimed_at)}</dd>
|
||||
|
||||
<dt class="col-sm-3">认领备注</dt>
|
||||
<dd class="col-sm-9">${task.claim_note || '-'}</dd>
|
||||
` : ''}
|
||||
|
||||
${task.torrent_id ? `
|
||||
<dt class="col-sm-3">种子ID</dt>
|
||||
<dd class="col-sm-9">${task.torrent_id}</dd>
|
||||
|
||||
<dt class="col-sm-3">完成时间</dt>
|
||||
<dd class="col-sm-9">${formatDateTime(task.completed_at)}</dd>
|
||||
|
||||
<dt class="col-sm-3">完成备注</dt>
|
||||
<dd class="col-sm-9">${task.complete_note || '-'}</dd>
|
||||
` : ''}
|
||||
</dl>
|
||||
`;
|
||||
|
||||
$('#task-detail').html(html);
|
||||
detailModal.show();
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
function deleteTask(taskId) {
|
||||
if (!confirm('确定要删除这个任务吗?此操作不可恢复!')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: `/api/tasks/${taskId}`,
|
||||
method: 'DELETE',
|
||||
success: function() {
|
||||
showSuccess('任务已删除');
|
||||
loadTasks(currentPage);
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
|
||||
// 重置筛选器
|
||||
function resetFilters() {
|
||||
$('#filter-status').val('');
|
||||
$('#filter-scope').val('all');
|
||||
loadTasks(1);
|
||||
}
|
||||
|
||||
|
||||
164
static/js/users.js
Normal file
164
static/js/users.js
Normal file
@@ -0,0 +1,164 @@
|
||||
let currentStatus = '';
|
||||
|
||||
$(document).ready(function() {
|
||||
loadUsers();
|
||||
|
||||
$('.nav-tabs .nav-link').click(function(e) {
|
||||
e.preventDefault();
|
||||
$('.nav-tabs .nav-link').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
currentStatus = $(this).data('status');
|
||||
loadUsers();
|
||||
});
|
||||
});
|
||||
|
||||
function loadUsers() {
|
||||
$.ajax({
|
||||
url: '/api/users',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||
},
|
||||
data: { status: currentStatus },
|
||||
success: function(data) {
|
||||
renderUsers(data.users);
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
|
||||
function renderUsers(users) {
|
||||
const tbody = $('#users-table');
|
||||
tbody.empty();
|
||||
|
||||
users.forEach(function(user) {
|
||||
const statusBadge = {
|
||||
'pending': '<span class="badge bg-warning">待审核</span>',
|
||||
'active': '<span class="badge bg-success">已激活</span>',
|
||||
'disabled': '<span class="badge bg-secondary">已禁用</span>'
|
||||
}[user.status];
|
||||
|
||||
const roleBadge = user.role === 'admin' ?
|
||||
'<span class="badge bg-danger">管理员</span>' :
|
||||
'<span class="badge bg-primary">用户</span>';
|
||||
|
||||
let actions = '';
|
||||
if (user.status === 'pending') {
|
||||
actions = `
|
||||
<button class="btn btn-sm btn-success" onclick="approveUser(${user.id}, 'approve')">
|
||||
<i class="bi bi-check"></i> 通过
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="approveUser(${user.id}, 'reject')">
|
||||
<i class="bi bi-x"></i> 拒绝
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
actions = `
|
||||
<button class="btn btn-sm btn-primary" onclick="editUser(${user.id})">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteUser(${user.id})">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
tbody.append(`
|
||||
<tr>
|
||||
<td>${user.username}</td>
|
||||
<td>${user.email}</td>
|
||||
<td>${user.uid || '-'}</td>
|
||||
<td>${roleBadge}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>${user.tags || '-'}</td>
|
||||
<td>${user.created_at}</td>
|
||||
<td>${actions}</td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
function approveUser(userId, action) {
|
||||
const message = action === 'approve' ? '确认通过审核?' : '确认拒绝申请?';
|
||||
if (!confirm(message)) return;
|
||||
|
||||
$.ajax({
|
||||
url: `/api/users/${userId}/approve`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||
},
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ action: action }),
|
||||
success: function(data) {
|
||||
alert(data.message);
|
||||
loadUsers();
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
|
||||
function editUser(userId) {
|
||||
$.ajax({
|
||||
url: `/api/users`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||
},
|
||||
success: function(data) {
|
||||
const user = data.users.find(u => u.id === userId);
|
||||
$('#edit-user-id').val(user.id);
|
||||
$('#edit-email').val(user.email);
|
||||
$('#edit-uid').val(user.uid);
|
||||
$('#edit-role').val(user.role);
|
||||
$('#edit-status').val(user.status);
|
||||
$('#edit-tags').val(user.tags);
|
||||
$('#edit-note').val(user.note);
|
||||
$('#editModal').modal('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveUser() {
|
||||
const userId = $('#edit-user-id').val();
|
||||
|
||||
$.ajax({
|
||||
url: `/api/users/${userId}`,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||
},
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
email: $('#edit-email').val(),
|
||||
uid: $('#edit-uid').val(),
|
||||
role: $('#edit-role').val(),
|
||||
status: $('#edit-status').val(),
|
||||
tags: $('#edit-tags').val(),
|
||||
note: $('#edit-note').val()
|
||||
}),
|
||||
success: function(data) {
|
||||
alert(data.message);
|
||||
$('#editModal').modal('hide');
|
||||
loadUsers();
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
|
||||
function deleteUser(userId) {
|
||||
if (!confirm('确认删除该用户?')) return;
|
||||
|
||||
$.ajax({
|
||||
url: `/api/users/${userId}`,
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('access_token')
|
||||
},
|
||||
success: function(data) {
|
||||
alert(data.message);
|
||||
loadUsers();
|
||||
},
|
||||
error: handleAjaxError
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user