新增仪表盘和统计分析数据API,优化外链过期时间处理,完善日志记录功能,更新前端样式以提升用户体验,确保数据统计准确性和可视化展示。
This commit is contained in:
@ -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>
|
@ -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;
|
||||
|
||||
|
Reference in New Issue
Block a user