Kamixitong/app/web/templates/user/base.html

353 lines
14 KiB
HTML
Raw Normal View History

2025-11-19 22:49:24 +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 %}{{ 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 || '';
2025-11-22 20:32:49 +08:00
// 修复URL构建逻辑避免重复域名问题
2025-11-19 22:49:24 +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-19 22:49:24 +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-19 22:49:24 +08:00
}
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) {
2025-11-22 20:32:49 +08:00
showNotification('会话已过期,请重新登录', 'warning');
// 延迟重定向,给用户看到提示的时间
setTimeout(() => {
window.location.href = '/login';
}, 1500);
throw new Error('SESSION_EXPIRED');
2025-11-19 22:49:24 +08:00
}
// 对于403错误显示权限不足的提示
else if (response.status === 403) {
2025-11-22 20:32:49 +08:00
showNotification('权限不足,无法执行此操作', 'error');
throw new Error('403: 权限不足');
2025-11-19 22:49:24 +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}`);
});
}
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];
}
2025-12-12 11:35:14 +08:00
// 显示通知 - 统一的消息弹窗函数
2025-11-19 22:49:24 +08:00
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);
2025-12-12 11:35:14 +08:00
} else {
// 如果找不到容器使用console输出作为备选方案
console.log(`[${type.toUpperCase()}] ${message}`);
2025-11-19 22:49:24 +08:00
}
}
// 页面加载完成后隐藏加载动画
document.addEventListener('DOMContentLoaded', function() {
hideLoading();
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>