第一次提交
This commit is contained in:
613
app/web/templates/base.html
Normal file
613
app/web/templates/base.html
Normal file
@@ -0,0 +1,613 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}软件授权管理系统{% endblock %}</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<!-- Custom CSS -->
|
||||
<link href="/static/css/custom.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
min-height: 100vh;
|
||||
background-color: #343a40;
|
||||
}
|
||||
.sidebar .nav-link {
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
.sidebar .nav-link:hover,
|
||||
.sidebar .nav-link.active {
|
||||
background-color: #495057;
|
||||
color: #fff;
|
||||
}
|
||||
.main-content {
|
||||
min-height: 100vh;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.card-stats {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
.loading {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
}
|
||||
.spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
/* 消息通知样式 */
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10000;
|
||||
width: 300px;
|
||||
}
|
||||
/* 移动端导航按钮 */
|
||||
.mobile-nav-btn {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
border-radius: 50%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
.mobile-nav-btn i {
|
||||
font-size: 24px;
|
||||
}
|
||||
/* 移动端导航菜单 */
|
||||
.mobile-nav-menu {
|
||||
position: fixed;
|
||||
bottom: 90px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
padding: 15px;
|
||||
min-width: 200px;
|
||||
}
|
||||
.mobile-nav-menu a {
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.mobile-nav-menu a:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.mobile-nav-menu a.active {
|
||||
background-color: #e9ecef;
|
||||
font-weight: bold;
|
||||
}
|
||||
/* 在小屏幕上显示移动端导航按钮 */
|
||||
@media (max-width: 767.98px) {
|
||||
.mobile-nav-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- Loading Spinner -->
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Container -->
|
||||
<div class="notification-container" id="notification-container"></div>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<!-- Main Container -->
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block sidebar collapse">
|
||||
<div class="position-sticky pt-3">
|
||||
<div class="text-center mb-4">
|
||||
<h5 class="text-white">太一软件管理系统</h5>
|
||||
<small class="text-muted">欢迎, {{ current_user.username }}</small>
|
||||
</div>
|
||||
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'web.dashboard' }}" href="{{ url_for('web.dashboard') }}">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>
|
||||
仪表板
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint and 'product' in request.endpoint }}" href="{{ url_for('web.products') }}">
|
||||
<i class="fas fa-box me-2"></i>
|
||||
产品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint and 'license' in request.endpoint }}" href="{{ url_for('web.licenses') }}">
|
||||
<i class="fas fa-key me-2"></i>
|
||||
卡密管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint and 'version' in request.endpoint }}" href="{{ url_for('web.versions') }}">
|
||||
<i class="fas fa-code-branch me-2"></i>
|
||||
版本管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint and 'device' in request.endpoint }}" href="{{ url_for('web.devices') }}">
|
||||
<i class="fas fa-desktop me-2"></i>
|
||||
设备管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint and 'ticket' in request.endpoint }}" href="{{ url_for('web.tickets') }}">
|
||||
<i class="fas fa-ticket-alt me-2"></i>
|
||||
工单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint and 'order' in request.endpoint }}" href="{{ url_for('web.orders') }}">
|
||||
<i class="fas fa-file-invoice me-2"></i>
|
||||
订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint and 'statistics' in request.endpoint }}" href="{{ url_for('web.statistics') }}">
|
||||
<i class="fas fa-chart-bar me-2"></i>
|
||||
统计分析
|
||||
</a>
|
||||
</li>
|
||||
{% if current_user.is_super_admin() %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint and 'admin' in request.endpoint }}" href="{{ url_for('web.admins') }}">
|
||||
<i class="fas fa-user-cog me-2"></i>
|
||||
账号管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint and 'payment-config' in request.endpoint }}" href="{{ url_for('web.payment_config') }}">
|
||||
<i class="fas fa-credit-card me-2"></i>
|
||||
支付配置
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint and 'settings' in request.endpoint }}" href="{{ url_for('web.settings') }}">
|
||||
<i class="fas fa-cog me-2"></i>
|
||||
系统设置
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint and 'log' in request.endpoint }}" href="{{ url_for('web.logs') }}">
|
||||
<i class="fas fa-file-alt me-2"></i>
|
||||
日志管理
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<hr class="text-white">
|
||||
|
||||
<div class="text-center">
|
||||
<a href="{{ url_for('web.logout') }}" class="btn btn-outline-light btn-sm">
|
||||
<i class="fas fa-sign-out-alt me-1"></i>
|
||||
退出登录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">{% block page_title %}仪表板{% endblock %}</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
{% block page_actions %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Page Content -->
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端导航按钮 -->
|
||||
<button class="btn btn-primary mobile-nav-btn" id="mobileNavBtn" type="button">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
|
||||
<!-- 移动端导航菜单 -->
|
||||
<div class="mobile-nav-menu" id="mobileNavMenu">
|
||||
<a class="{{ 'active' if request.endpoint == 'web.dashboard' }}" href="{{ url_for('web.dashboard') }}">
|
||||
<i class="fas fa-tachometer-alt"></i>
|
||||
仪表板
|
||||
</a>
|
||||
<a class="{{ 'active' if request.endpoint and 'product' in request.endpoint }}" href="{{ url_for('web.products') }}">
|
||||
<i class="fas fa-box"></i>
|
||||
产品管理
|
||||
</a>
|
||||
<a class="{{ 'active' if request.endpoint and 'license' in request.endpoint }}" href="{{ url_for('web.licenses') }}">
|
||||
<i class="fas fa-key"></i>
|
||||
卡密管理
|
||||
</a>
|
||||
<a class="{{ 'active' if request.endpoint and 'version' in request.endpoint }}" href="{{ url_for('web.versions') }}">
|
||||
<i class="fas fa-code-branch"></i>
|
||||
版本管理
|
||||
</a>
|
||||
<a class="{{ 'active' if request.endpoint and 'device' in request.endpoint }}" href="{{ url_for('web.devices') }}">
|
||||
<i class="fas fa-desktop"></i>
|
||||
设备管理
|
||||
</a>
|
||||
<a class="{{ 'active' if request.endpoint and 'ticket' in request.endpoint }}" href="{{ url_for('web.tickets') }}">
|
||||
<i class="fas fa-ticket-alt"></i>
|
||||
工单管理
|
||||
</a>
|
||||
<a class="{{ 'active' if request.endpoint and 'order' in request.endpoint }}" href="{{ url_for('web.orders') }}">
|
||||
<i class="fas fa-file-invoice"></i>
|
||||
订单管理
|
||||
</a>
|
||||
<a class="{{ 'active' if request.endpoint and 'statistics' in request.endpoint }}" href="{{ url_for('web.statistics') }}">
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
统计分析
|
||||
</a>
|
||||
{% if current_user.is_super_admin() %}
|
||||
<a class="{{ 'active' if request.endpoint and 'admin' in request.endpoint }}" href="{{ url_for('web.admins') }}">
|
||||
<i class="fas fa-user-cog"></i>
|
||||
账号管理
|
||||
</a>
|
||||
<a class="{{ 'active' if request.endpoint and 'payment-config' in request.endpoint }}" href="{{ url_for('web.payment_config') }}">
|
||||
<i class="fas fa-credit-card"></i>
|
||||
支付配置
|
||||
</a>
|
||||
<a class="{{ 'active' if request.endpoint and 'settings' in request.endpoint }}" href="{{ url_for('web.settings') }}">
|
||||
<i class="fas fa-cog"></i>
|
||||
系统设置
|
||||
</a>
|
||||
<a class="{{ 'active' if request.endpoint and 'log' in request.endpoint }}" href="{{ url_for('web.logs') }}">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
日志管理
|
||||
</a>
|
||||
{% endif %}
|
||||
<hr>
|
||||
<a href="{{ url_for('web.logout') }}">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
退出登录
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Login Layout -->
|
||||
<div class="container-fluid vh-100 d-flex align-items-center justify-content-center bg-light">
|
||||
<div class="card shadow-lg" style="width: 100%; max-width: 400px;">
|
||||
<div class="card-body">
|
||||
{% block login_content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<!-- Custom JS -->
|
||||
<script src="/static/js/custom.js"></script>
|
||||
<!-- Pagination JS -->
|
||||
<script src="/static/js/pagination.js"></script>
|
||||
|
||||
<!-- Common Functions -->
|
||||
<script>
|
||||
// 设置前端域名全局变量
|
||||
window.FRONTEND_DOMAIN = '{{ config.FRONTEND_DOMAIN or "" }}';
|
||||
|
||||
// 自动刷新相关变量
|
||||
let autoRefreshTimer = null;
|
||||
let autoRefreshInterval = 30000; // 默认30秒自动刷新一次
|
||||
let isAutoRefreshEnabled = false;
|
||||
|
||||
// 显示加载动画
|
||||
function showLoading() {
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
}
|
||||
|
||||
// 隐藏加载动画
|
||||
function hideLoading() {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
}
|
||||
|
||||
// 启用自动刷新
|
||||
function enableAutoRefresh(callback, interval = 30000) {
|
||||
// 清除之前的定时器
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer);
|
||||
}
|
||||
|
||||
autoRefreshInterval = interval;
|
||||
isAutoRefreshEnabled = true;
|
||||
|
||||
// 设置新的定时器
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
if (typeof callback === 'function') {
|
||||
console.log('自动刷新数据...');
|
||||
callback();
|
||||
}
|
||||
}, autoRefreshInterval);
|
||||
|
||||
console.log(`自动刷新已启用,间隔: ${autoRefreshInterval / 1000}秒`);
|
||||
}
|
||||
|
||||
// 禁用自动刷新
|
||||
function disableAutoRefresh() {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
isAutoRefreshEnabled = false;
|
||||
console.log('自动刷新已禁用');
|
||||
}
|
||||
|
||||
// 获取自动刷新状态
|
||||
function getAutoRefreshStatus() {
|
||||
return {
|
||||
enabled: isAutoRefreshEnabled,
|
||||
interval: autoRefreshInterval
|
||||
};
|
||||
}
|
||||
|
||||
// 切换自动刷新状态
|
||||
function toggleAutoRefresh(callback, interval = 30000) {
|
||||
if (isAutoRefreshEnabled) {
|
||||
disableAutoRefresh();
|
||||
return false;
|
||||
} else {
|
||||
enableAutoRefresh(callback, interval);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// AJAX请求封装
|
||||
function apiRequest(url, options = {}) {
|
||||
// 自动构建完整的API URL
|
||||
let fullUrl = url;
|
||||
|
||||
// 检查是否配置了前端域名
|
||||
const frontendDomain = window.FRONTEND_DOMAIN || '';
|
||||
|
||||
// 修复URL构建逻辑,避免重复域名问题
|
||||
if (url.startsWith('/')) {
|
||||
// 如果是绝对路径,则使用配置的域名或当前主机
|
||||
if (frontendDomain && !url.startsWith(frontendDomain)) {
|
||||
// 确保frontendDomain不包含路径部分
|
||||
let cleanDomain = frontendDomain;
|
||||
try {
|
||||
const urlObj = new URL(frontendDomain.startsWith('http') ? frontendDomain : 'http://' + frontendDomain);
|
||||
cleanDomain = urlObj.origin;
|
||||
} catch (e) {
|
||||
// 如果解析失败,使用原始值
|
||||
if (frontendDomain.includes('/')) {
|
||||
cleanDomain = frontendDomain.split('/')[0];
|
||||
}
|
||||
}
|
||||
fullUrl = cleanDomain + url;
|
||||
} else if (!frontendDomain) {
|
||||
fullUrl = window.location.origin + url;
|
||||
}
|
||||
} else if (!url.startsWith('http')) {
|
||||
// 如果不是完整URL且不以http开头,则添加API前缀
|
||||
if (frontendDomain) {
|
||||
// 确保frontendDomain不包含路径部分
|
||||
let cleanDomain = frontendDomain;
|
||||
try {
|
||||
const urlObj = new URL(frontendDomain.startsWith('http') ? frontendDomain : 'http://' + frontendDomain);
|
||||
cleanDomain = urlObj.origin;
|
||||
} catch (e) {
|
||||
// 如果解析失败,使用原始值
|
||||
if (frontendDomain.includes('/')) {
|
||||
cleanDomain = frontendDomain.split('/')[0];
|
||||
}
|
||||
}
|
||||
fullUrl = cleanDomain + '/api/v1/' + url;
|
||||
} else {
|
||||
fullUrl = window.location.origin + '/api/v1/' + url;
|
||||
}
|
||||
}
|
||||
|
||||
showLoading();
|
||||
|
||||
// 检查body是否是FormData,如果是则不设置Content-Type(让浏览器自动设置)
|
||||
const isFormData = options.body instanceof FormData;
|
||||
const defaultOptions = {
|
||||
headers: isFormData ? {} : {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'same-origin' // 重要: 使用session cookies
|
||||
};
|
||||
|
||||
return fetch(fullUrl, { ...defaultOptions, ...options })
|
||||
.then(response => {
|
||||
hideLoading();
|
||||
if (!response.ok) {
|
||||
// 对于401错误,表示需要重新登录
|
||||
if (response.status === 401) {
|
||||
return response.json().then(data => {
|
||||
showNotification(data.message || '会话已过期,请重新登录', 'warning');
|
||||
// 延迟重定向,给用户看到提示的时间
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 1500);
|
||||
throw new Error('SESSION_EXPIRED');
|
||||
});
|
||||
}
|
||||
// 对于403错误,显示权限不足的提示
|
||||
else if (response.status === 403) {
|
||||
// 先尝试解析错误信息
|
||||
return response.json().then(errorData => {
|
||||
showNotification(errorData.message || '权限不足,无法执行此操作', 'error');
|
||||
throw new Error(`403: ${errorData.message || '权限不足'}`);
|
||||
}).catch(() => {
|
||||
// 如果无法解析JSON,使用默认消息
|
||||
showNotification('权限不足,无法执行此操作', 'error');
|
||||
throw new Error('403: 权限不足');
|
||||
});
|
||||
}
|
||||
// 对于400错误和其他错误状态码
|
||||
else {
|
||||
return response.json().then(errorData => {
|
||||
// 检查是否有具体的错误消息
|
||||
if (errorData && errorData.message) {
|
||||
// 特别处理已激活卡密的删除错误
|
||||
if (response.status === 400 && errorData.message.includes('已激活')) {
|
||||
throw new Error(`400: ${errorData.message}`);
|
||||
} else {
|
||||
throw new Error(`${response.status}: ${errorData.message}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`${response.status}: ${response.statusText}`);
|
||||
}
|
||||
}).catch(error => {
|
||||
// 如果是我们自定义的错误,直接抛出
|
||||
if (error.name !== 'SyntaxError') {
|
||||
throw error;
|
||||
} else {
|
||||
// 如果是JSON解析错误,使用状态文本
|
||||
throw new Error(`${response.status}: ${response.statusText}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch(error => {
|
||||
hideLoading();
|
||||
// 只有非会话过期的错误才需要在控制台显示
|
||||
if (error.message !== 'SESSION_EXPIRED') {
|
||||
console.error('API request failed:', error);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
// 统一的API响应处理函数
|
||||
function handleApiResponse(response, successCallback, errorCallback) {
|
||||
if (response && response.success) {
|
||||
if (typeof successCallback === 'function') {
|
||||
successCallback(response.data);
|
||||
}
|
||||
} else {
|
||||
const message = response && response.message ? response.message : '操作失败';
|
||||
showNotification(message, 'error');
|
||||
if (typeof errorCallback === 'function') {
|
||||
errorCallback(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 统一的成功处理函数
|
||||
function handleApiSuccess(data, message = '操作成功') {
|
||||
showNotification(message, 'success');
|
||||
return data;
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN');
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// HTML转义函数 - 防止XSS攻击
|
||||
function escapeHtml(text) {
|
||||
if (text == null) return '';
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return String(text).replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
|
||||
// 移动端导航菜单控制
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const mobileNavBtn = document.getElementById('mobileNavBtn');
|
||||
const mobileNavMenu = document.getElementById('mobileNavMenu');
|
||||
|
||||
if (mobileNavBtn && mobileNavMenu) {
|
||||
mobileNavBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
mobileNavMenu.style.display = mobileNavMenu.style.display === 'flex' ? 'none' : 'flex';
|
||||
});
|
||||
|
||||
// 点击其他地方隐藏菜单
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!mobileNavBtn.contains(e.target) && !mobileNavMenu.contains(e.target)) {
|
||||
mobileNavMenu.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载完成后隐藏加载动画
|
||||
hideLoading();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user