Kamixitong/app/web/templates/base.html

518 lines
22 KiB
HTML
Raw Normal View History

2025-11-11 21:39:12 +08:00
<!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%);
}
2025-11-16 19:06:49 +08:00
/* 消息通知样式 */
.notification-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
width: 300px;
}
2025-11-22 16:48:45 +08:00
/* 移动端导航按钮 */
.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;
}
}
2025-11-11 21:39:12 +08:00
</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>
2025-11-16 19:06:49 +08:00
<!-- Notification Container -->
<div class="notification-container" id="notification-container"></div>
2025-11-11 21:39:12 +08:00
{% 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>
2025-11-19 22:49:24 +08:00
<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>
2025-11-11 21:39:12 +08:00
<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>
2025-11-15 23:57:05 +08:00
<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>
2025-11-11 21:39:12 +08:00
{% 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>
2025-11-22 16:48:45 +08:00
<!-- 移动端导航按钮 -->
<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>
2025-11-11 21:39:12 +08:00
{% 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>
2025-11-16 19:06:49 +08:00
// 设置前端域名全局变量
window.FRONTEND_DOMAIN = '{{ config.FRONTEND_DOMAIN or "" }}';
2025-11-11 21:39:12 +08:00
// 显示加载动画
function showLoading() {
document.getElementById('loading').style.display = 'block';
}
// 隐藏加载动画
function hideLoading() {
document.getElementById('loading').style.display = 'none';
}
// AJAX请求封装
function apiRequest(url, options = {}) {
2025-11-16 19:06:49 +08:00
// 自动构建完整的API URL
let fullUrl = url;
// 检查是否配置了前端域名
const frontendDomain = window.FRONTEND_DOMAIN || '';
2025-11-22 20:32:49 +08:00
// 修复URL构建逻辑避免重复域名问题
2025-11-16 19:06:49 +08:00
if (url.startsWith('/')) {
2025-11-22 20:32:49 +08:00
// 如果是绝对路径,则使用配置的域名或当前主机
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;
}
2025-11-16 19:06:49 +08:00
} else if (!url.startsWith('http')) {
// 如果不是完整URL且不以http开头则添加API前缀
2025-11-22 20:32:49 +08:00
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;
}
2025-11-16 19:06:49 +08:00
}
2025-11-22 20:32:49 +08:00
2025-11-11 21:39:12 +08:00
showLoading();
const defaultOptions = {
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin' // 重要: 使用session cookies
};
2025-11-16 19:06:49 +08:00
return fetch(fullUrl, { ...defaultOptions, ...options })
2025-11-11 21:39:12 +08:00
.then(response => {
hideLoading();
if (!response.ok) {
// 对于401错误表示需要重新登录
if (response.status === 401) {
2025-11-19 22:49:24 +08:00
return response.json().then(data => {
showNotification(data.message || '会话已过期,请重新登录', 'warning');
// 延迟重定向,给用户看到提示的时间
setTimeout(() => {
window.location.href = '/login';
}, 1500);
throw new Error('SESSION_EXPIRED');
});
2025-11-11 21:39:12 +08:00
}
2025-11-15 23:57:05 +08:00
// 对于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: 权限不足');
});
}
// 其他错误状态码
2025-11-13 16:51:51 +08:00
return response.json().then(errorData => {
throw new Error(`${response.status}: ${errorData.message || response.statusText}`);
}).catch(() => {
// 如果无法解析JSON错误信息则使用默认消息
throw new Error(`${response.status}: ${response.statusText}`);
});
2025-11-11 21:39:12 +08:00
}
return response.json();
})
.catch(error => {
hideLoading();
2025-11-19 22:49:24 +08:00
// 只有非会话过期的错误才需要在控制台显示
if (error.message !== 'SESSION_EXPIRED') {
console.error('API request failed:', error);
}
2025-11-11 21:39:12 +08:00
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];
}
2025-11-16 19:06:49 +08:00
// 显示通知 - 统一使用静态JS中的函数
2025-11-11 21:39:12 +08:00
function showNotification(message, type = 'info') {
2025-11-16 19:06:49 +08:00
// 创建通知元素
2025-11-11 21:39:12 +08:00
const alertDiv = document.createElement('div');
2025-11-16 19:06:49 +08:00
const alertType = type === 'error' ? 'danger' : type;
alertDiv.className = `alert alert-${alertType} alert-dismissible fade show`;
2025-11-11 21:39:12 +08:00
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
2025-11-16 19:06:49 +08:00
alertDiv.style.marginBottom = '10px';
// 插入到通知容器中
const container = document.getElementById('notification-container') || document.querySelector('main');
2025-11-11 21:39:12 +08:00
if (container) {
container.insertBefore(alertDiv, container.firstChild);
2025-11-16 19:06:49 +08:00
2025-11-11 21:39:12 +08:00
// 自动隐藏
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
}
2025-11-22 16:48:45 +08:00
// 移动端导航菜单控制
2025-11-11 21:39:12 +08:00
document.addEventListener('DOMContentLoaded', function() {
2025-11-22 16:48:45 +08:00
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';
}
});
}
// 页面加载完成后隐藏加载动画
2025-11-11 21:39:12 +08:00
hideLoading();
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>