329 lines
13 KiB
HTML
329 lines
13 KiB
HTML
<!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 || '';
|
||
|
||
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();
|
||
// 首先检查响应的内容类型
|
||
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) {
|
||
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);
|
||
showNotification('请求失败: ' + error.message, '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> |