TenantDrive/templates/admin.html

1354 lines
67 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网盘租户系统 - 管理员面板</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
<style>
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
padding: 20px;
background-color: #f8f9fa;
}
.main-content {
margin-left: 250px;
padding: 20px;
}
.nav-link {
color: #333;
padding: 10px 15px;
border-radius: 5px;
margin-bottom: 5px;
}
.nav-link:hover {
background-color: #e9ecef;
}
.nav-link.active {
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>
<div class="container-fluid">
<div class="row">
<!-- 侧边栏 -->
<nav class="col-md-3 col-lg-2 d-md-block sidebar">
<div class="position-sticky">
<h3 class="mb-4">管理员面板</h3>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active" href="#dashboard">
<i class="bi bi-speedometer2 me-2"></i>仪表盘
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#accounts">
<i class="bi bi-cloud me-2"></i>网盘账号管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#links">
<i class="bi bi-link-45deg me-2"></i>外链管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#statistics">
<i class="bi bi-graph-up me-2"></i>统计分析
</a>
</li>
</ul>
</div>
</nav>
<!-- 主要内容区域 -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
<!-- 仪表盘 -->
<div id="dashboard" class="tab-content">
<h2 class="mb-4">系统概览</h2>
<div class="row">
<div class="col-md-4">
<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="kpi-value">0</p>
</div>
</div>
</div>
<div class="col-md-4">
<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="kpi-value">0</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card kpi-card">
<div class="card-body">
<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>
</div>
</div>
<!-- 网盘账号管理 -->
<div id="accounts" class="tab-content d-none">
<h2 class="mb-4">网盘账号管理</h2>
<!--
添加网盘账号按钮
btn: Bootstrap按钮基础类
btn-primary: 蓝色主题按钮样式
mb-3: margin-bottom为3个单位提供底部间距
data-bs-toggle="modal": 指示此按钮将触发模态框
data-bs-target="#addAccountModal": 指定要打开的模态框ID
-->
<button class="btn btn-primary mb-3" data-bs-toggle="modal" data-bs-target="#addAccountModal">
<!--
bi bi-plus-circle: Bootstrap图标库中的加号图标
me-2: margin-end为2个单位在图标右侧提供间距
-->
<i class="bi bi-plus-circle me-2"></i>添加网盘账号
</button>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>网盘类型</th>
<th>账号名称</th>
<th>状态</th>
<th>剩余容量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<!-- 账号列表将通过JavaScript动态填充 -->
<tr>
{% for user_drive in alluser_drives %}
<td>{{user_drive.provider_name}}</td>
<td>{{user_drive.provider_name}}</td>
<td><span class="badge bg-success">正常</span></td>
<td>1.5TB / 2TB</td>
<td>
<button class="btn btn-sm btn-info editudrivebtn" data-bs-toggle="modal" data-bs-target="#editudriveModal" tid="{{ user_drive.id }}"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-danger deleteudrivebtn" tid="{{ user_drive.id }}"><i class="bi bi-trash"></i></button>
</td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
</div>
<!-- 外链管理 -->
<div id="links" class="tab-content d-none">
<h2 class="mb-4">外链管理</h2>
<!-- 添加外链按钮 -->
<button class="btn btn-primary mb-3" data-bs-toggle="modal" data-bs-target="#addExlinkModal">
<i class="bi bi-plus-circle me-2"></i>添加外链
</button>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>外链ID</th>
<th>关联网盘</th>
<th>创建时间</th>
<th>过期时间</th>
<th>剩余次数</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<!-- 外链列表将通过JavaScript动态填充 -->
</tbody>
</table>
</div>
</div>
<!-- 统计分析 -->
<div id="statistics" class="tab-content d-none">
<h2 class="mb-4">统计分析</h2>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">外链访问趋势</h5>
<canvas id="accessChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">网盘使用分布</h5>
<canvas id="storageChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- 添加网盘账号模态框 -->
<div class="modal fade" id="addAccountModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加网盘账号</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addAccountForm">
<div class="mb-3">
<label class="form-label">网盘类型</label>
<select class="form-select" name="type" id="driveType" required>
<option value="">请选择网盘类型</option>
{% set provider_options = providers %}
{% for provider in provider_options %}
<option value="{{ provider.provider_name }}" data-needs-api="{{ provider.provider_name }}">{{ provider.provider_name}}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">配置信息 (JSON)</label>
<div class="position-relative json-editor-area">
<div class="ace-editor border rounded" style="height: 200px; font-family: monospace;"></div>
<textarea class="form-control d-none json-config-textarea" name="config_json"></textarea>
<div class="position-absolute top-0 end-0 p-2">
<button class="btn btn-sm btn-outline-secondary format-json-btn" type="button">
<i class="bi bi-code-square"></i> 格式化
</button>
</div>
</div>
<div class="d-flex justify-content-between mt-1">
<div class="form-text">请输入有效的JSON格式配置信息</div>
<div class="form-text text-end"><span class="json-line-count">0</span> 行 | <span class="json-char-count">0</span> 字符</div>
</div>
<div class="json-error invalid-feedback"></div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveNewAccount">保存</button>
</div>
</div>
</div>
</div>
<!-- 编辑网盘账号模态框 -->
<div class="modal fade" id="editudriveModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑网盘驱动</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editudriveForm">
<div class="mb-3">
<label class="form-label">配置信息 (JSON)</label>
<div class="position-relative json-editor-area">
<div class="ace-editor border rounded" style="height: 200px; font-family: monospace;"></div>
<textarea class="form-control d-none json-config-textarea" name="config_json"></textarea>
<div class="position-absolute top-0 end-0 p-2">
<button class="btn btn-sm btn-outline-secondary format-json-btn" type="button">
<i class="bi bi-code-square"></i> 格式化
</button>
</div>
</div>
<div class="d-flex justify-content-between mt-1">
<div class="form-text">请输入有效的JSON格式配置信息</div>
<div class="form-text text-end"><span class="json-line-count">0</span> 行 | <span class="json-char-count">0</span> 字符</div>
</div>
<div class="json-error invalid-feedback"></div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveAccount">保存</button>
</div>
</div>
</div>
</div>
<!-- 添加网盘外链模态框 -->
<div class="modal fade" id="addExlinkModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加网盘外链</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addExlinkForm">
<div class="mb-3">
<label class="form-label">网盘类型</label>
<select class="form-select" name="disk_type" id="exlinkDriveType" required>
<option value="">请选择网盘类型</option>
{% for provider in providers %}
<option value="{{ provider.provider_name }}">{{ provider.provider_name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">账号</label>
<select class="form-select" name="account_id" id="exlinkAccountId" required>
<option value="">请先选择网盘类型</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">使用次数限制</label>
<input type="number" class="form-control" name="total_quota" required min="1" value="3">
<div class="form-text">设置此外链可以被使用的最大次数</div>
</div>
<div class="mb-3">
<label class="form-label">到期时间</label>
<input type="datetime-local" class="form-control" name="expiry_time" id="exlinkExpiryTime" required>
<div class="form-text">设置此外链的有效期限,超过时间后将无法访问</div>
</div>
<div class="mb-3">
<label class="form-label">备注说明</label>
<input type="text" class="form-control" name="remarks" placeholder="可选">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveExlink">保存</button>
</div>
</div>
</div>
</div>
<!-- 删除确认对话框 -->
<div class="modal fade" id="deleteConfirmModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>确定要删除此网盘账号吗?此操作不可恢复。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirmDelete">确认删除</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js" integrity="sha512-GZ1RIgZaSc8rnco/8CXfRdCpDxRCphenIiZ2ztLy3XQfCbQUSCuk8IudvNHxkRA3oUg6q0qejgN/qqyG1duv5Q==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ext-language_tools.min.js"></script>
<script>
// 导航切换
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
this.classList.add('active');
const targetId = this.getAttribute('href').substring(1);
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('d-none');
});
document.getElementById(targetId).classList.remove('d-none');
});
});
// Bootstrap消息提示函数
function showMessage(message, type = 'success', duration = 3000) {
// 创建消息元素
const messageDiv = document.createElement('div');
// 设置样式类
messageDiv.classList.add('position-fixed', 'top-0', 'start-50', 'translate-middle-x', 'mt-3', 'p-3', 'rounded', 'shadow');
// 根据类型设置不同的样式
switch(type) {
case 'success':
messageDiv.classList.add('bg-success', 'text-white');
break;
case 'warning':
messageDiv.classList.add('bg-warning', 'text-dark');
break;
case 'error':
messageDiv.classList.add('bg-danger', 'text-white');
break;
case 'info':
messageDiv.classList.add('bg-info', 'text-white');
break;
default:
messageDiv.classList.add('bg-success', 'text-white');
}
// 设置消息内容
messageDiv.textContent = message;
// 设置z-index确保显示在最上层
messageDiv.style.zIndex = '9999';
// 添加到body
document.body.appendChild(messageDiv);
// 设置淡入效果
messageDiv.style.opacity = '0';
messageDiv.style.transition = 'opacity 0.3s ease-in-out';
setTimeout(() => {
messageDiv.style.opacity = '1';
}, 10);
// 设置定时移除
setTimeout(() => {
messageDiv.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(messageDiv);
}, 300);
}, duration);
}
// 使用示例:
// showMessage('操作成功', 'success');
// showMessage('警告信息', 'warning');
// showMessage('错误信息', 'error');
// showMessage('提示信息', 'info');
// 初始化图表变量
let accessChartInstance = null;
let storageChartInstance = null;
// 初始化图表函数
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);
const editorArea = editorElement.closest('.json-editor-area');
const parentContainer = editorArea.closest('.mb-3'); // Get parent container
const configTextarea = editorArea.querySelector('.json-config-textarea');
const lineCountSpan = parentContainer.querySelector('.json-line-count'); // Search from parent
const charCountSpan = parentContainer.querySelector('.json-char-count'); // Search from parent
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/json");
editor.setOptions({
fontSize: "12pt",
showPrintMargin: false,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
});
// 同步到隐藏的textarea并更新计数
editor.getSession().on('change', function() {
const value = editor.getSession().getValue();
configTextarea.value = value;
lineCountSpan.textContent = editor.getSession().getLength();
charCountSpan.textContent = value.length;
});
// 触发一次change事件以初始化计数
editor.getSession().setValue(configTextarea.value, -1);
return editor;
}
function setupJsonEditors() {
document.querySelectorAll('.ace-editor').forEach(editorElement => {
const editor = initAceEditor(editorElement);
const editorArea = editorElement.closest('.json-editor-area');
const formatButton = editorArea.querySelector('.format-json-btn');
const parentContainer = editorArea.closest('.mb-3'); // Get parent container
const errorDisplay = parentContainer.querySelector('.json-error'); // Search from parent
// 格式化按钮事件
formatButton.addEventListener('click', function() {
try {
const json = JSON.parse(editor.getSession().getValue());
editor.setValue(JSON.stringify(json, null, 2), -1);
errorDisplay.textContent = '';
errorDisplay.style.display = 'none';
editorElement.classList.remove('border-danger');
} catch (e) {
errorDisplay.textContent = '无效的JSON格式: ' + e.message;
errorDisplay.style.display = 'block';
editorElement.classList.add('border-danger');
}
});
// 默认设置一个空的JSON结构
const emptyJson = {
"provider_name": "",
"config": {},
"auth": {}
};
const jsonString = JSON.stringify(emptyJson, null, 2);
if (!editor.getValue()) {
editor.setValue(jsonString, -1);
}
});
}
document.addEventListener('DOMContentLoaded', function() {
setupJsonEditors(); // 初始化所有编辑器
bindEditButtonEvents(); // 初始化编辑按钮事件
bindDeleteButtonEvents(); // 初始化删除按钮事件
bindAddAccountEvent(); // 初始化添加账号事件
bindAddExlinkEvents(); // 初始化添加外链事件
loadExternalLinks(); // 加载外链列表
setupModalFocusManagement(); // <-- 添加此行初始化模态框焦点管理
initCharts(); // <-- 新增初始化图表
updateDashboard(); // <-- 新增加载仪表盘数据
updateStatisticsCharts(); // <-- 新增加载统计图表数据
// 重新加载账户列表以获取初始数据如果需要
refreshAccountsList();
});
// 设置默认JSON模板 (Add Modal specific)
document.getElementById('driveType').addEventListener('change', function() {
const addModalEditorArea = document.querySelector('#addAccountModal .json-editor-area');
const editorElement = addModalEditorArea.querySelector('.ace-editor');
const editor = ace.edit(editorElement); // Get the Ace editor instance
const selectedType = this.value;
if (selectedType) {
let defaultJson = {};
const providers = JSON.parse('{{ providers | tojson | safe }}'); // Use safe filter
const selectedProvider = providers.find(provider => provider.provider_name === selectedType);
if (selectedProvider && selectedProvider.config_vars) {
defaultJson = selectedProvider.config_vars;
} else {
defaultJson = {
"provider_name": selectedType,
"config": { "api_key": "", "secret_key": "", "redirect_uri": "" },
"auth": { "token": "", "expires_in": 0 }
};
}
editor.setValue(JSON.stringify(defaultJson, null, 2), -1);
}
});
// 绑定编辑按钮事件
function bindEditButtonEvents() {
document.querySelectorAll('.editudrivebtn').forEach(button => {
button.addEventListener('click', function() {
const editModalEditorArea = document.querySelector('#editudriveModal .json-editor-area');
const editorElement = editModalEditorArea.querySelector('.ace-editor');
const editor = ace.edit(editorElement); // Get the Ace editor instance
const selectedtid = this.getAttribute('tid');
if (selectedtid) {
// 获取最新的账号信息
$.ajax({
url: '/admin/user_drive/get',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ id: selectedtid }),
success: function(response) {
if (response.status && response.data) {
let selectedUdrive = null;
// 查找匹配的用户驱动
if (Array.isArray(response.data)) {
selectedUdrive = response.data.find(drive => drive.id == selectedtid);
} else if (response.data.id == selectedtid) {
selectedUdrive = response.data;
}
if (selectedUdrive && selectedUdrive.login_config) {
let configData = selectedUdrive.login_config;
// 如果是字符串格式,解析为对象
if (typeof configData === 'string') {
try {
configData = JSON.parse(configData);
} catch (e) {
console.error('解析配置JSON失败:', e);
}
}
editor.setValue(JSON.stringify(configData, null, 2), -1);
} else {
const defaultJson = {
"provider_name": selectedtid,
"config": { "api_key": "", "secret_key": "", "redirect_uri": "" },
"auth": { "token": "", "expires_in": 0 }
};
editor.setValue(JSON.stringify(defaultJson, null, 2), -1);
}
// 存储选中的ID
document.querySelector('#editudriveModal #saveAccount').setAttribute('data-tid', selectedtid);
}
},
error: function(error) {
console.error('获取账号信息出错:', error);
showMessage('获取账号信息时发生错误', 'error');
// 使用默认值
const defaultJson = {
"provider_name": selectedtid,
"config": { "api_key": "", "secret_key": "", "redirect_uri": "" },
"auth": { "token": "", "expires_in": 0 }
};
editor.setValue(JSON.stringify(defaultJson, null, 2), -1);
document.querySelector('#editudriveModal #saveAccount').setAttribute('data-tid', selectedtid);
}
});
}
});
});
}
// 编辑用户驱动保存事件
$('#editudriveModal #saveAccount').on('click', function() {
const editModalEditorArea = document.querySelector('#editudriveModal .json-editor-area');
const editorElement = editModalEditorArea.querySelector('.ace-editor');
const editor = ace.edit(editorElement);
const configJson = editor.getValue();
const tid = this.getAttribute('data-tid');
try {
// 验证JSON格式
JSON.parse(configJson);
// 发送数据到服务器
$.ajax({
url: '/admin/user_drive/update',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
id: tid,
login_config: configJson
}),
success: function(response) {
console.log(response);
if (response.status) {
showMessage('配置更新成功', 'success');
// 清空并关闭模态框
editor.setValue(configJson, -1);
$('#editudriveModal').modal('hide');
// 刷新网盘账号列表
refreshAccountsList();
} else {
showMessage('配置更新失败: ' + response.message, 'error');
}
},
error: function(error) {
console.error('更新配置出错:', error);
showMessage('更新配置时发生错误,请查看控制台', 'error');
}
});
} catch (e) {
showMessage('无效的JSON格式: ' + e.message, 'error');
const errorDisplay = editModalEditorArea.closest('.mb-3').querySelector('.json-error');
errorDisplay.textContent = '无效的JSON格式: ' + e.message;
errorDisplay.style.display = 'block';
editorElement.classList.add('border-danger');
}
});
// 绑定删除按钮事件
function bindDeleteButtonEvents() {
let selectedDriveId = null;
const deleteConfirmModal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
// 为所有删除按钮添加点击事件
document.querySelectorAll('.deleteudrivebtn').forEach(button => {
button.addEventListener('click', function() {
selectedDriveId = this.getAttribute('tid');
// 重置对话框内容
document.querySelector('#deleteConfirmModal .modal-body p').textContent =
'确定要删除此网盘账号吗?此操作不可恢复。';
deleteConfirmModal.show(); // 显示确认对话框
});
});
// 确认删除按钮点击事件
document.getElementById('confirmDelete').addEventListener('click', function() {
if (selectedDriveId) {
// 发送删除请求
$.ajax({
url: '/admin/user_drive/delete',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ id: selectedDriveId }),
success: function(response) {
if (response.status) {
deleteConfirmModal.hide(); // 只有成功时才隐藏对话框
showMessage('网盘账号删除成功', 'success');
// 刷新网盘账号列表
refreshAccountsList();
} else {
// 显示错误消息在对话框中
document.querySelector('#deleteConfirmModal .modal-body p').textContent =
response.message || '网盘账号删除失败';
showMessage(response.message || '网盘账号删除失败', 'error');
}
},
error: function(error) {
// 显示错误消息在对话框中
document.querySelector('#deleteConfirmModal .modal-body p').textContent =
'删除网盘账号时发生错误,请稍后重试。';
console.error('删除网盘账号出错:', error);
showMessage('删除网盘账号时发生错误,请查看控制台', 'error');
}
});
}
});
}
// 刷新网盘账号列表函数也需要更新,确保新添加的行的删除按钮也有事件绑定
function refreshAccountsList() {
$.ajax({
url: '/admin/user_drive/get',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({}),
success: function(response) {
// 清空现有表格内容
const accountsTableBody = $('#accounts table tbody');
accountsTableBody.empty();
if (response.status) {
// 检查是否有数据,如果没有也显示空表格而不是错误
if (response.data && response.data.length > 0) {
// 添加新的行
response.data.forEach(function(drive) {
const row = $('<tr></tr>');
row.append(`<td>${drive.provider_name}</td>`);
row.append(`<td>${drive.provider_name}</td>`);
row.append(`<td><span class="badge bg-success">正常</span></td>`);
row.append(`<td>1.5TB / 2TB</td>`);
row.append(`
<td>
<button class="btn btn-sm btn-info editudrivebtn" data-bs-toggle="modal" data-bs-target="#editudriveModal" tid="${drive.id}">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger deleteudrivebtn" tid="${drive.id}">
<i class="bi bi-trash"></i>
</button>
</td>
`);
accountsTableBody.append(row);
});
// 重新绑定编辑和删除按钮事件
bindEditButtonEvents();
bindDeleteButtonEvents();
} else {
// 如果没有数据,显示一个提示行
const emptyRow = $('<tr></tr>');
emptyRow.append(`<td colspan="5" class="text-center">暂无网盘账号,请添加。</td>`);
accountsTableBody.append(emptyRow);
}
} else {
// 如果API返回失败显示错误消息
showMessage(response.message || '获取网盘账号列表失败', 'error');
const errorRow = $('<tr></tr>');
errorRow.append(`<td colspan="5" class="text-center text-danger">获取数据失败,请刷新页面重试。</td>`);
accountsTableBody.append(errorRow);
}
},
error: function(error) {
console.error('获取网盘账号列表出错:', error);
showMessage('获取网盘账号列表时发生错误', 'error');
// 显示错误消息在表格中
const accountsTableBody = $('#accounts table tbody');
accountsTableBody.empty();
const errorRow = $('<tr></tr>');
errorRow.append(`<td colspan="5" class="text-center text-danger">获取数据失败,请刷新页面重试。</td>`);
accountsTableBody.append(errorRow);
}
});
}
// 添加网盘账号保存事件
function bindAddAccountEvent() {
document.getElementById('saveNewAccount').addEventListener('click', function() {
const addModalEditorArea = document.querySelector('#addAccountModal .json-editor-area');
const editorElement = addModalEditorArea.querySelector('.ace-editor');
const editor = ace.edit(editorElement);
const configJson = editor.getValue();
const driveType = document.getElementById('driveType').value;
if (!driveType) {
showMessage('请选择网盘类型', 'error');
return;
}
try {
// 验证JSON格式
const configData = JSON.parse(configJson);
// 发送数据到服务器
$.ajax({
url: '/admin/user_drive/add',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
provider_name: driveType,
login_config: configData,
remarks: driveType + "的账号"
}),
success: function(response) {
if (response.status) {
showMessage('网盘账号添加成功', 'success');
// 清空并关闭模态框
document.getElementById('driveType').value = '';
editor.setValue('', -1);
$('#addAccountModal').modal('hide');
// 刷新网盘账号列表
refreshAccountsList();
} else {
showMessage('网盘账号添加失败: ' + (response.message || '未知错误'), 'error');
}
},
error: function(error) {
console.error('添加网盘账号出错:', error);
showMessage('添加网盘账号时发生错误,请查看控制台', 'error');
}
});
} catch (e) {
showMessage('无效的JSON格式: ' + e.message, 'error');
const errorDisplay = addModalEditorArea.closest('.mb-3').querySelector('.json-error');
errorDisplay.textContent = '无效的JSON格式: ' + e.message;
errorDisplay.style.display = 'block';
editorElement.classList.add('border-danger');
}
});
}
// 加载外链列表
function loadExternalLinks() {
$.ajax({
url: '/admin/exlink/get',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({}),
success: function(response) {
const linksTableBody = $('#links table tbody');
linksTableBody.empty();
if (response.status) {
if (response.data && response.data.length > 0) {
response.data.forEach(function(link) {
const row = $('<tr></tr>');
row.append(`<td>${link.link_uuid}</td>`);
row.append(`<td>${link.drive_id}</td>`);
// 创建时间和过期时间
row.append(`<td>-</td>`);
if (link.expiry_time) {
const expiryDate = new Date(link.expiry_time);
const formattedDate = expiryDate.toLocaleString();
row.append(`<td>${formattedDate}</td>`);
} else {
row.append(`<td>-</td>`);
}
// 剩余次数
const usedQuota = link.used_quota || 0;
const totalQuota = link.total_quota || 0;
const remainingCount = totalQuota - usedQuota;
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 || isExpired) {
statusBadge = '<span class="badge bg-danger">已禁用</span>';
} else {
statusBadge = '<span class="badge bg-success">正常</span>';
}
row.append(`<td>${statusBadge}</td>`);
// 操作按钮
row.append(`
<td>
<a href="/exlink/${link.link_uuid}" target="_blank" class="btn btn-sm btn-info">
<i class="bi bi-box-arrow-up-right"></i>
</a>
<button class="btn btn-sm btn-danger deleteExlinkBtn" data-uuid="${link.link_uuid}">
<i class="bi bi-trash"></i>
</button>
</td>
`);
linksTableBody.append(row);
});
// 绑定删除外链按钮事件
bindDeleteExlinkEvents();
} else {
// 如果没有数据,显示一个提示行
const emptyRow = $('<tr></tr>');
emptyRow.append(`<td colspan="7" class="text-center">暂无外链数据,请添加。</td>`);
linksTableBody.append(emptyRow);
}
} else {
showMessage('获取外链列表失败: ' + (response.message || '未知错误'), 'error');
const errorRow = $('<tr></tr>');
errorRow.append(`<td colspan="7" class="text-center text-danger">获取数据失败,请刷新页面重试。</td>`);
linksTableBody.append(errorRow);
}
},
error: function(error) {
console.error('获取外链列表出错:', error);
showMessage('获取外链列表时发生错误', 'error');
// 显示错误消息在表格中
const linksTableBody = $('#links table tbody');
linksTableBody.empty();
const errorRow = $('<tr></tr>');
errorRow.append(`<td colspan="7" class="text-center text-danger">获取数据失败,请刷新页面重试。</td>`);
linksTableBody.append(errorRow);
}
});
}
// 绑定添加外链相关事件
function bindAddExlinkEvents() {
// 当选择网盘类型时,加载对应的账号列表
document.getElementById('exlinkDriveType').addEventListener('change', function() {
const selectedType = this.value;
const accountSelect = document.getElementById('exlinkAccountId');
// 清空当前选项
accountSelect.innerHTML = '<option value="">加载中...</option>';
if (selectedType) {
// 加载对应类型的网盘账号
$.ajax({
url: '/admin/user_drive/get',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ provider_name: selectedType }),
success: function(response) {
accountSelect.innerHTML = '<option value="">请选择账号</option>';
if (response.status && response.data && response.data.length > 0) {
response.data.forEach(function(drive) {
const option = document.createElement('option');
option.value = drive.id;
option.textContent = drive.provider_name + (drive.remarks ? ` (${drive.remarks})` : '');
accountSelect.appendChild(option);
});
} else {
accountSelect.innerHTML = '<option value="">没有可用的账号,请先添加</option>';
}
},
error: function(error) {
console.error('获取网盘账号出错:', error);
accountSelect.innerHTML = '<option value="">加载失败,请重试</option>';
}
});
} else {
accountSelect.innerHTML = '<option value="">请先选择网盘类型</option>';
}
});
// 设置默认到期时间为当前时间后24小时
function setDefaultExpiryTime() {
const expiryTimeInput = document.getElementById('exlinkExpiryTime');
if (expiryTimeInput) {
const now = new Date();
now.setHours(now.getHours() + 24);
// 格式化为datetime-local输入所需的格式YYYY-MM-DDThh:mm
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const formattedDateTime = `${year}-${month}-${day}T${hours}:${minutes}`;
expiryTimeInput.value = formattedDateTime;
}
}
// 初始化表单时设置默认到期时间
setDefaultExpiryTime();
// 每次打开模态框时重置到期时间
$('#addExlinkModal').on('show.bs.modal', function() {
setDefaultExpiryTime();
});
// 绑定保存外链按钮点击事件
document.getElementById('saveExlink').addEventListener('click', function() {
// 获取表单数据
const formData = new FormData(document.getElementById('addExlinkForm'));
const drive_id = formData.get('account_id');
const total_quota = formData.get('total_quota');
const expiry_time = formData.get('expiry_time');
const remarks = formData.get('remarks') || '';
if (!drive_id || !total_quota || !expiry_time) {
showMessage('请填写必要的信息', 'error');
return;
}
// 转换日期时间格式从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({
url: '/admin/exlink/create',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
drive_id: drive_id,
total_quota: total_quota,
expiry_time: expiryTimeISO, // <-- 使用完整的 ISO UTC 字符串
remarks: remarks
}),
success: function(response) {
if (response.status) {
showMessage('外链创建成功', 'success');
// 清空表单
document.getElementById('exlinkDriveType').value = '';
document.getElementById('exlinkAccountId').innerHTML = '<option value="">请先选择网盘类型</option>';
document.querySelector('[name="total_quota"]').value = '3';
document.querySelector('[name="remarks"]').value = '';
// 关闭模态框
$('#addExlinkModal').modal('hide');
// 重新加载外链列表
loadExternalLinks();
} else {
showMessage('外链创建失败: ' + (response.message || '未知错误'), 'error');
}
},
error: function(error) {
console.error('创建外链出错:', error);
showMessage('创建外链时发生错误请查看控制台', 'error');
}
});
});
}
// 绑定删除外链按钮事件
function bindDeleteExlinkEvents() {
document.querySelectorAll('.deleteExlinkBtn').forEach(button => {
button.addEventListener('click', function() {
const uuid = this.getAttribute('data-uuid');
if (confirm('确定要删除此外链吗此操作不可恢复')) {
$.ajax({
url: '/admin/exlink/delete',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ link_uuid: uuid }),
success: function(response) {
if (response.status) {
showMessage('外链删除成功', 'success');
loadExternalLinks(); // 重新加载外链列表
} else {
showMessage('外链删除失败: ' + (response.message || '未知错误'), 'error');
}
},
error: function(error) {
console.error('删除外链出错:', error);
showMessage('删除外链时发生错误请查看控制台', 'error');
}
});
}
});
});
}
// 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>