TenantDrive/templates/admin.html

669 lines
32 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;
}
</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">
<div class="card-body">
<h5 class="card-title">网盘账号总数</h5>
<p class="card-text display-4">0</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">活跃外链数</h5>
<p class="card-text display-4">0</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">今日访问量</h5>
<p class="card-text display-4">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" id="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"><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="saveAccount">保存</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" required>
<option value="">请选择网盘类型</option>
<option value="aliyun">阿里云盘</option>
<option value="baidu">百度网盘</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">账号</label>
<select class="form-select" name="account_id" required>
<option value="">请选择账号</option>
<!-- 这里应该通过JavaScript动态填充账号列表 -->
<option value="aliyun">阿里云盘</option>
<option value="baidu">百度网盘</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">剩余次数</label>
<input type="number" class="form-control" name="remaining_count" required>
</div>
<div class="mb-3">
<label class="form-label">有效期</label>
<input type="date" class="form-control" name="expiry_date" required>
</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>
<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');
//添加保存外链事件
$('#saveExlink').on('click', function() {
// 获取表单数据
const formData = new FormData(document.querySelector('#addExlinkModal form'));
const formValues = {
disk_type: formData.get('disk_type'),
account_id: formData.get('account_id'),
remaining_count: formData.get('remaining_count'),
expiry_date: formData.get('expiry_date')
};
// 检查表单是否填写完整
if (!formValues.disk_type || !formValues.remaining_count || !formValues.account_id || !formValues.expiry_date) {
showMessage('请填写所有必填字段!', 'error');
return; // 停止提交
}
// 表单数据准备发送
const submitData = {
account_id: formValues.account_id,
remaining_count: formValues.remaining_count,
access_token: formValues.access_token
};
console.log('准备提交的数据:', submitData);
// 发送数据到服务器
$.ajax({
url: '/admin/exlink',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
data: formValues
}),
success: function(data) {
if (data.status) {
showMessage('账号保存成功',"success");
// 清空表单
$('#accountName').val('');
$('#accountToken').val('');
} else {
showMessage('账号保存失败: ' + data.message,"warning");
}
},
error: function(error) {
console.error('保存账号出错:', error);
showMessage('保存账号时发生错误,请查看控制台',"error");
}
});
});
// 初始化图表
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
}]
}
});
const storageCtx = document.getElementById('storageChart').getContext('2d');
new Chart(storageCtx, {
type: 'doughnut',
data: {
labels: ['阿里云盘', '百度网盘'],
datasets: [{
data: [0, 0],
backgroundColor: ['#ff6384', '#36a2eb']
}]
}
});
// 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(); // 初始化所有编辑器
});
// 设置默认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);
}
});
// 设置编辑用户驱动JSON模板 (Edit Modal specific)
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) {
let defaultJson = {};
// 解析返回的JSON字符串为JavaScript对象
const allUserDrivesStr = '{{ alluser_drives | tojson | safe }}';
const user_drives = JSON.parse(allUserDrivesStr); // Use safe filter
const selectedUdrive = user_drives.find(user_drive => user_drive.id == selectedtid);
if (selectedUdrive && selectedUdrive.login_config) {
defaultJson = selectedUdrive.login_config;
} else {
defaultJson = {
"provider_name": selectedtid, // Keep provider_name for context if no config
"config": { "api_key": "", "secret_key": "", "redirect_uri": "" },
"auth": { "token": "", "expires_in": 0 }
};
}
editor.setValue(JSON.stringify(defaultJson, null, 2), -1);
// Store the selected ID in a data attribute for the save button to access
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');
// 如果需要,可以在这里刷新页面或表格数据
// 局部刷新表格内容
} 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');
}
});
</script>
</body>
</html>