Kamixitong/app/web/templates/user/base.html
2025-12-12 11:35:14 +08:00

353 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 %}{{ config.SITE_NAME or '软件授权管理系统' }}{% endblock %}</title>
<!-- Bootstrap 5 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">
<!-- 自定义CSS -->
<style>
:root {
--primary-color: #667eea;
--secondary-color: #764ba2;
--accent-color: #f093fb;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
}
.navbar-brand {
font-weight: bold;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.footer {
background-color: #ffffff;
border-top: 1px solid #dee2e6;
padding: 1rem 0;
margin-top: auto;
}
.feature-card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: none;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
border: none;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
}
/* 加载动画 */
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
display: none;
justify-content: center;
align-items: center;
z-index: 9999;
}
.spinner {
width: 3rem;
height: 3rem;
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body class="d-flex flex-column min-vh-100">
<!-- 加载动画 -->
<div id="loading">
<div class="spinner"></div>
</div>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
<div class="container">
<a class="navbar-brand" href="{{ url_for('user.user_index') }}">
<i class="fas fa-cube me-2"></i>{{ config.SITE_NAME or '软件授权管理系统' }}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('user.user_index') }}">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('user.user_products') }}">产品中心</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('user.user_license_purchase') }}">购买授权</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('user.user_tickets') }}">售后服务</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/login">
<i class="fas fa-sign-in-alt me-1"></i>管理后台
</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- 通知容器 -->
<div id="notification-container" class="container mt-3"></div>
<!-- 主要内容区域 -->
<main class="flex-shrink-0">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="container mt-3">
{% 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 %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<!-- 页脚 -->
<footer class="footer mt-auto py-4">
<div class="container">
<div class="row">
<div class="col-md-6">
<h5>联系我们</h5>
<p>
<i class="fas fa-phone me-2"></i> 客服电话400-123-4567<br>
<i class="fas fa-envelope me-2"></i> 邮箱support@taiyi-software.com
</p>
</div>
<div class="col-md-6 text-md-end">
<h5>关于我们</h5>
<p>
<a href="#" class="text-decoration-none me-3">隐私政策</a>
<a href="#" class="text-decoration-none">用户协议</a>
</p>
<p class="text-muted">© 2023 {{ config.SITE_NAME or '软件授权管理系统' }}. 保留所有权利.</p>
</div>
</div>
</div>
</footer>
<!-- 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>
<!-- 自定义JavaScript -->
<script>
// 设置前端域名全局变量
window.FRONTEND_DOMAIN = '{{ config.FRONTEND_DOMAIN or "" }}';
// 显示加载动画
function showLoading() {
document.getElementById('loading').style.display = 'flex';
}
// 隐藏加载动画
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();
const defaultOptions = {
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin' // 重要: 使用session cookies
};
return fetch(fullUrl, { ...defaultOptions, ...options })
.then(response => {
hideLoading();
// 首先检查响应的内容类型
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
// 如果不是JSON响应可能是404页面或其他HTML错误页面
throw new Error(`服务器返回了非JSON响应 (${response.status})`);
}
if (!response.ok) {
// 对于401错误表示需要重新登录
if (response.status === 401) {
showNotification('会话已过期,请重新登录', 'warning');
// 延迟重定向,给用户看到提示的时间
setTimeout(() => {
window.location.href = '/login';
}, 1500);
throw new Error('SESSION_EXPIRED');
}
// 对于403错误显示权限不足的提示
else if (response.status === 403) {
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];
}
// 显示通知 - 统一的消息弹窗函数
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);
} else {
// 如果找不到容器使用console输出作为备选方案
console.log(`[${type.toUpperCase()}] ${message}`);
}
}
// 页面加载完成后隐藏加载动画
document.addEventListener('DOMContentLoaded', function() {
hideLoading();
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>