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

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

74
static/css/style.css Normal file
View 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

File diff suppressed because one or more lines are too long

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
View 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

File diff suppressed because one or more lines are too long

33
static/js/login.js Normal file
View 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
View 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
View 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
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);
});
}

329
static/js/task_list.js Normal file
View 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
View 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
});
}