Kamixitong/app/web/templates/base.html
2025-11-22 22:59:31 +08:00

534 lines
22 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-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 '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 '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>
<!-- Common Functions -->
<script>
// 设置前端域名全局变量
window.FRONTEND_DOMAIN = '{{ config.FRONTEND_DOMAIN or "" }}';
// 显示加载动画
function showLoading() {
document.getElementById('loading').style.display = 'block';
}
// 隐藏加载动画
function hideLoading() {
document.getElementById('loading').style.display = 'none';
}
// 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: 权限不足');
});
}
// 其他错误状态码
return response.json().then(errorData => {
throw new Error(`${response.status}: ${errorData.message || response.statusText}`);
}).catch(() => {
// 如果无法解析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;
});
}
// 格式化日期
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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return String(text).replace(/[&<>"']/g, m => map[m]);
}
// 显示通知 - 统一使用静态JS中的函数
function showNotification(message, type = 'info') {
// 创建通知元素
const alertDiv = document.createElement('div');
const alertType = type === 'error' ? 'danger' : type;
alertDiv.className = `alert alert-${alertType} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
alertDiv.style.marginBottom = '10px';
// 插入到通知容器中
const container = document.getElementById('notification-container') || document.querySelector('main');
if (container) {
container.insertBefore(alertDiv, container.firstChild);
// 自动隐藏
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
}
// 移动端导航菜单控制
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>