新增仪表盘和统计分析数据API,优化外链过期时间处理,完善日志记录功能,更新前端样式以提升用户体验,确保数据统计准确性和可视化展示。

This commit is contained in:
dockermen
2025-04-11 19:38:57 +08:00
parent aad2182d0c
commit c4ce2fc5a4
5 changed files with 463 additions and 45 deletions

View File

@ -33,6 +33,89 @@
background-color: #0d6efd;
color: white;
}
/* Digital Cockpit Styles */
body {
/* Optional: Set a base background for the whole page */
/* background-color: #1a1a2e; */
/* color: #e0e0e0; */
}
.main-content {
/* background-color: #1f1f38; /* Slightly lighter background for content */
/* border-radius: 10px; */
/* margin-top: 10px; */
}
#dashboard {
/* background: linear-gradient(to bottom right, #2a2a4a, #1f1f38); */
padding: 30px;
border-radius: 8px;
/* box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); */
}
#dashboard h2, #statistics h2 {
color: #343a40; /* Keep headings readable */
border-bottom: 2px solid #0d6efd;
padding-bottom: 10px;
margin-bottom: 30px !important; /* Override default margin */
}
.kpi-card {
background-color: #ffffff; /* White card on potentially dark background */
border: none;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
margin-bottom: 20px; /* Add spacing between cards */
color: #333;
overflow: hidden; /* Ensure consistency */
position: relative; /* For potential pseudo-elements */
}
.kpi-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.15);
}
.kpi-card .card-body {
padding: 25px;
text-align: center;
}
.kpi-card .card-title {
font-size: 1rem;
font-weight: 500;
color: #6c757d; /* Muted color for title */
margin-bottom: 15px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.kpi-card .kpi-value {
font-size: 2.8rem;
font-weight: 700;
color: #0d6efd; /* Highlight color for value */
line-height: 1.2;
}
.kpi-card .kpi-icon {
font-size: 1.5rem;
margin-bottom: 10px;
color: #0d6efd;
opacity: 0.7;
display: block; /* Center icon */
}
/* Style for charts container */
#statistics .card {
background-color: #ffffff;
border: none;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
color: #333;
margin-bottom: 20px;
}
#statistics .card-body {
padding: 25px;
}
#statistics .card-title {
font-size: 1.1rem;
font-weight: 600;
color: #343a40;
margin-bottom: 20px;
text-align: center;
}
</style>
</head>
<body>
@ -74,26 +157,29 @@
<h2 class="mb-4">系统概览</h2>
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card kpi-card">
<div class="card-body">
<i class="bi bi-cloud-fill kpi-icon"></i>
<h5 class="card-title">网盘账号总数</h5>
<p class="card-text display-4">0</p>
<p class="kpi-value">0</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card kpi-card">
<div class="card-body">
<i class="bi bi-link-45deg kpi-icon"></i>
<h5 class="card-title">活跃外链数</h5>
<p class="card-text display-4">0</p>
<p class="kpi-value">0</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card kpi-card">
<div class="card-body">
<h5 class="card-title">今日访问量</h5>
<p class="card-text display-4">0</p>
<i class="bi bi-bar-chart-line-fill kpi-icon"></i>
<h5 class="card-title">外链总数</h5>
<p class="kpi-value">0</p>
</div>
</div>
</div>
@ -434,33 +520,141 @@
// showMessage('错误信息', 'error');
// showMessage('提示信息', 'info');
// 初始化图表
const accessCtx = document.getElementById('accessChart').getContext('2d');
new Chart(accessCtx, {
type: 'line',
data: {
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
datasets: [{
label: '访问量',
data: [0, 0, 0, 0, 0, 0, 0],
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
}
});
// 初始化图表变量
let accessChartInstance = null;
let storageChartInstance = null;
const storageCtx = document.getElementById('storageChart').getContext('2d');
new Chart(storageCtx, {
type: 'doughnut',
data: {
labels: ['阿里云盘', '百度网盘'],
datasets: [{
data: [0, 0],
backgroundColor: ['#ff6384', '#36a2eb']
}]
}
});
// 初始化图表函数
function initCharts() {
const accessCtx = document.getElementById('accessChart').getContext('2d');
accessChartInstance = new Chart(accessCtx, {
type: 'line',
data: {
labels: [], // 初始为空,稍后填充
datasets: [{
label: '访问量', // 暂时保留,实际数据需记录日志
data: [], // 初始为空
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
grid: { display: false },
ticks: { color: '#6c757d' } // X轴标签颜色
},
y: {
grid: { color: 'rgba(108, 117, 125, 0.2)' }, // Y轴网格线颜色
ticks: { color: '#6c757d' } // Y轴标签颜色
}
},
plugins: {
legend: {
labels: { color: '#343a40' } // 图例文字颜色
}
}
}
});
const storageCtx = document.getElementById('storageChart').getContext('2d');
storageChartInstance = new Chart(storageCtx, {
type: 'doughnut',
data: {
labels: [], // 初始为空
datasets: [{
data: [], // 初始为空
backgroundColor: [ // 提供更多颜色以备不时之需
'#ff6384',
'#36a2eb',
'#ffce56',
'#4bc0c0',
'#9966ff',
'#ff9f40'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
labels: { color: '#343a40' } // 图例文字颜色
}
}
}
});
}
// 更新仪表盘数据函数
function updateDashboard() {
$.ajax({
url: '/admin/dashboard_data',
type: 'GET',
success: function(response) {
if (response.status && response.data) {
// 更新美化后的KPI卡片值
$('#dashboard .kpi-value').eq(0).text(response.data.user_drives_count || 0);
$('#dashboard .kpi-value').eq(1).text(response.data.active_links_count || 0);
$('#dashboard .kpi-value').eq(2).text(response.data.total_links_count || 0);
} else {
showMessage('获取仪表盘数据失败', 'error');
}
},
error: function(error) {
console.error('获取仪表盘数据出错:', error);
showMessage('获取仪表盘数据时发生网络错误', 'error');
}
});
}
// 更新统计图表函数
function updateStatisticsCharts() {
$.ajax({
url: '/admin/statistics_data',
type: 'GET',
success: function(response) {
if (response.status && response.data) {
// 更新网盘使用分布图 (Doughnut Chart)
if (response.data.drives_by_provider && storageChartInstance) {
const providerData = response.data.drives_by_provider;
const labels = Object.keys(providerData);
const data = Object.values(providerData);
storageChartInstance.data.labels = labels;
storageChartInstance.data.datasets[0].data = data;
storageChartInstance.update();
}
// 更新外链访问趋势图 (Line Chart) - 需要后端实现日志记录和数据提供
// 示例:假设 response.data.access_trend = [{date: '2023-01-01', count: 10}, ...]
// if (response.data.access_trend && accessChartInstance) {
// const trendData = response.data.access_trend;
// accessChartInstance.data.labels = trendData.map(item => item.date);
// accessChartInstance.data.datasets[0].data = trendData.map(item => item.count);
// accessChartInstance.update();
// } else {
// 如果没有访问数据,显示空状态或提示
if (accessChartInstance) {
accessChartInstance.data.labels = ['无数据'];
accessChartInstance.data.datasets[0].data = [0];
accessChartInstance.update();
}
// }
} else {
showMessage('获取统计数据失败', 'error');
}
},
error: function(error) {
console.error('获取统计数据出错:', error);
showMessage('获取统计数据时发生网络错误', 'error');
}
});
}
// json编辑器处理
function initAceEditor(editorElement) {
const editor = ace.edit(editorElement);
@ -536,6 +730,14 @@
bindAddAccountEvent(); // 初始化添加账号事件
bindAddExlinkEvents(); // 初始化添加外链事件
loadExternalLinks(); // 加载外链列表
setupModalFocusManagement(); // <-- 添加此行:初始化模态框焦点管理
initCharts(); // <-- 新增:初始化图表
updateDashboard(); // <-- 新增:加载仪表盘数据
updateStatisticsCharts(); // <-- 新增:加载统计图表数据
// 重新加载账户列表以获取初始数据(如果需要)
refreshAccountsList();
});
// 设置默认JSON模板 (Add Modal specific)
@ -887,8 +1089,22 @@
row.append(`<td>${remainingCount} / ${totalQuota}</td>`);
// 状态
let isExpired = false;
if (link.expiry_time) {
try {
const expiryDate = new Date(link.expiry_time); // Directly parse ISO string
if (!isNaN(expiryDate.getTime()) && new Date() > expiryDate) {
isExpired = true;
}
} catch (e) {
console.error("Error parsing expiry_time in admin list:", link.expiry_time, e);
// Optionally treat parse error as expired or show different status
}
}
let statusBadge = '';
if (remainingCount <= 0) {
// 如果剩余次数用完 或 已过期,则禁用
if (remainingCount <= 0 || isExpired) {
statusBadge = '<span class="badge bg-danger">已禁用</span>';
} else {
statusBadge = '<span class="badge bg-success">正常</span>';
@ -1021,9 +1237,14 @@
return;
}
// 转换日期时间格式从YYYY-MM-DDThh:mm到YYYY-MM-DD hh:mm:ss
const expiryDate = new Date(expiry_time);
const formattedExpiryTime = expiryDate.toISOString().replace('T', ' ').substring(0, 19);
// 转换日期时间格式从YYYY-MM-DDThh:mm到完整的ISO 8601 UTC字符串
const expiryDate = new Date(expiry_time); // 解析本地时间输入
// 检查日期是否有效
if (isNaN(expiryDate.getTime())) {
showMessage('无效的日期时间格式', 'error');
return;
}
const expiryTimeISO = expiryDate.toISOString(); // 转换为ISO 8601 UTC字符串 (e.g., "2025-04-12T11:08:00.000Z")
// 发送创建外链请求
$.ajax({
@ -1033,7 +1254,7 @@
data: JSON.stringify({
drive_id: drive_id,
total_quota: total_quota,
expiry_time: formattedExpiryTime,
expiry_time: expiryTimeISO, // <-- 使用完整的 ISO UTC 字符串
remarks: remarks
}),
success: function(response) {
@ -1091,6 +1312,43 @@
});
});
}
// Focus management for modals to fix aria-hidden warning
function setupModalFocusManagement() {
const modals = document.querySelectorAll('.modal');
let triggerElement = null;
modals.forEach(modal => {
// Store the trigger element when the modal is about to show
modal.addEventListener('show.bs.modal', function (event) {
// event.relatedTarget is the element that triggered the modal
triggerElement = event.relatedTarget;
});
// Return focus when the modal is hidden
modal.addEventListener('hidden.bs.modal', function () {
// Small delay to ensure the modal is fully hidden and elements are interactable
setTimeout(() => {
if (triggerElement && typeof triggerElement.focus === 'function') {
// Check if trigger element still exists and is focusable
if (document.body.contains(triggerElement)) {
triggerElement.focus();
} else {
// Fallback if trigger element is gone (e.g., deleted from DOM)
// Focusing body might not be ideal UX, consider a more specific fallback if needed
document.body.focus();
console.log("Modal trigger element no longer exists, focusing body.");
}
} else {
// Fallback if no trigger element recorded or it's not focusable
document.body.focus();
console.log("No modal trigger element recorded or it's not focusable, focusing body.");
}
triggerElement = null; // Reset for the next modal
}, 0);
});
});
}
</script>
</body>
</html>

View File

@ -212,7 +212,7 @@
let remainingCount = parseInt("{{ remaining_count }}") || 0;
const expiryTimeString = "{{ expiry_time }}";
const expiryTime = expiryTimeString ? new Date(expiryTimeString.replace(/-/g, '/')) : null;
const expiryTime = expiryTimeString ? new Date(expiryTimeString) : null;
let countdownInterval = null;