Kamixitong/app/web/templates/base.html
2025-11-16 19:06:49 +08:00

343 lines
14 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;
}
</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 '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>
{% 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 || '';
if (url.startsWith('/')) {
// 如果是相对路径,则使用配置的域名或当前主机
fullUrl = (frontendDomain || window.location.origin) + url;
} else if (!url.startsWith('http')) {
// 如果不是完整URL且不以http开头则添加API前缀
fullUrl = (frontendDomain || window.location.origin) + '/api/v1/' + url;
}
showLoading();
const defaultOptions = {
headers: {
'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) {
showNotification('会话已过期,请重新登录', 'warning');
// 延迟重定向,给用户看到提示的时间
setTimeout(() => {
window.location.href = '/login';
}, 1500);
}
// 对于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();
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];
}
// 显示通知 - 统一使用静态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() {
hideLoading();
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>