第一次提交
This commit is contained in:
353
app/web/templates/user/base.html
Normal file
353
app/web/templates/user/base.html
Normal file
@@ -0,0 +1,353 @@
|
||||
<!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>
|
||||
165
app/web/templates/user/index.html
Normal file
165
app/web/templates/user/index.html
Normal file
@@ -0,0 +1,165 @@
|
||||
{% extends "user/base.html" %}
|
||||
|
||||
{% block title %}太一软件系统 - 首页{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* 首页特有样式 */
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 4rem 0;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.quick-access-btn {
|
||||
transition: all 0.3s ease;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.quick-access-btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.emergency-contact {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.wechat-qrcode {
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.system-value {
|
||||
text-align: center;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.system-value h3 {
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.system-value p {
|
||||
color: #6c757d;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- 系统价值区 -->
|
||||
<div class="hero-section">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8 text-center">
|
||||
<h1 class="display-4 fw-bold mb-3">用心做软件</h1>
|
||||
<p class="lead mb-4">何以解忧,唯有代码,不忘初心,方得始终</p>
|
||||
|
||||
<!-- 核心功能图标 -->
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
</div>
|
||||
<h5>文档管理</h5>
|
||||
<p class="text-light">集中存储,版本控制</p>
|
||||
</div>
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-tasks"></i>
|
||||
</div>
|
||||
<h5>任务协作</h5>
|
||||
<p class="text-light">实时同步,进度追踪</p>
|
||||
</div>
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="feature-icon">
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
</div>
|
||||
<h5>数据分析</h5>
|
||||
<p class="text-light">智能报表,决策支持</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速入口区 -->
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 text-center mb-5">
|
||||
<h2 class="mb-4">快速开始</h2>
|
||||
<div class="d-flex flex-wrap justify-content-center">
|
||||
<a href="{{ url_for('user.user_products') }}" class="btn btn-primary btn-lg quick-access-btn">
|
||||
<i class="fas fa-box me-2"></i>查看产品
|
||||
</a>
|
||||
<a href="{{ url_for('user.user_license_purchase') }}" class="btn btn-success btn-lg quick-access-btn">
|
||||
<i class="fas fa-shopping-cart me-2"></i>购买授权
|
||||
</a>
|
||||
<a href="{{ url_for('user.user_tickets') }}" class="btn btn-info btn-lg quick-access-btn">
|
||||
<i class="fas fa-headset me-2"></i>售后咨询
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 紧急咨询区 -->
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="emergency-contact text-center">
|
||||
<h3 class="mb-3">
|
||||
<i class="fas fa-exclamation-circle text-danger me-2"></i>
|
||||
紧急需求?
|
||||
</h3>
|
||||
<p class="mb-4">扫码添加微信,快速响应需求</p>
|
||||
<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#wechatModal">
|
||||
<i class="fab fa-weixin me-2"></i>紧急咨询
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 微信二维码模态框 -->
|
||||
<div class="modal fade" id="wechatModal" tabindex="-1" aria-labelledby="wechatModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="wechatModalLabel">紧急咨询</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<p>扫码添加微信,快速响应需求</p>
|
||||
<img src="/static/images/taiyiagi.png" alt="微信二维码" class="wechat-qrcode img-fluid">
|
||||
<p class="mt-2 text-muted small">微信ID: taiyi1224</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// 首页特有JavaScript
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('太一软件系统首页加载完成');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
466
app/web/templates/user/license_purchase.html
Normal file
466
app/web/templates/user/license_purchase.html
Normal file
@@ -0,0 +1,466 @@
|
||||
{% extends "user/base.html" %}
|
||||
|
||||
{% block title %}购买授权 - {{ config.SITE_NAME or '软件授权管理系统' }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.product-card {
|
||||
transition: transform 0.3s ease;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.product-image {
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.plan-card:hover {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.plan-card.selected {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.plan-price {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.payment-method {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.payment-method:hover {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.payment-method.selected {
|
||||
border-color: #667eea;
|
||||
background-color: rgba(102, 126, 234, 0.05);
|
||||
}
|
||||
|
||||
.order-summary {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">购买授权</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- 产品选择 -->
|
||||
<div class="col-lg-8 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">选择产品</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row" id="productsContainer">
|
||||
<!-- 产品列表将通过JavaScript动态加载 -->
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<p class="mt-2">正在加载产品列表...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 套餐选择 -->
|
||||
<div class="card shadow-sm mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">选择套餐</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row" id="plansContainer">
|
||||
<!-- 套餐列表将根据选择的产品动态加载 -->
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-info-circle fa-2x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">请选择一个产品</h4>
|
||||
<p class="text-muted">选择产品后将显示可用的授权套餐</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单摘要和支付 -->
|
||||
<div class="col-lg-4 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">订单信息</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="order-summary">
|
||||
<h6>订单摘要</h6>
|
||||
<hr>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>产品:</span>
|
||||
<span id="selectedProduct">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>套餐:</span>
|
||||
<span id="selectedPlan">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>数量:</span>
|
||||
<span id="selectedQuantity">1</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between fw-bold">
|
||||
<span>总计:</span>
|
||||
<span id="totalAmount" class="plan-price">¥0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h6>支付方式</h6>
|
||||
<div class="payment-methods">
|
||||
<div class="payment-method mb-2 selected" data-method="alipay">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fab fa-alipay fa-2x me-3 text-primary"></i>
|
||||
<div>
|
||||
<div class="fw-bold">支付宝</div>
|
||||
<small class="text-muted">推荐使用</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="payment-method mb-2" data-method="wechat">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fab fa-weixin fa-2x me-3 text-success"></i>
|
||||
<div>
|
||||
<div class="fw-bold">微信支付</div>
|
||||
<small class="text-muted">即时到账</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100 mt-4" id="payBtn" disabled>
|
||||
<i class="fas fa-credit-card me-2"></i>立即支付
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// 卡密购买页面JavaScript
|
||||
let selectedProduct = null;
|
||||
let selectedPlan = null;
|
||||
let selectedQuantity = 1;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('卡密购买页面加载完成');
|
||||
|
||||
// 初始化产品列表
|
||||
loadProducts();
|
||||
|
||||
// 支付方式选择
|
||||
document.querySelectorAll('.payment-method').forEach(method => {
|
||||
method.addEventListener('click', function() {
|
||||
document.querySelectorAll('.payment-method').forEach(m => m.classList.remove('selected'));
|
||||
this.classList.add('selected');
|
||||
});
|
||||
});
|
||||
|
||||
// 支付按钮事件
|
||||
document.getElementById('payBtn').addEventListener('click', function() {
|
||||
processPayment();
|
||||
});
|
||||
});
|
||||
|
||||
// 加载产品列表
|
||||
function loadProducts() {
|
||||
// 显示加载状态
|
||||
document.getElementById('productsContainer').innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<p class="mt-2">正在加载产品列表...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 调用API获取产品列表
|
||||
apiRequest('/api/v1/user/products')
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
renderProducts(data.data.products || []);
|
||||
} else {
|
||||
showNotification('加载产品列表失败: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('加载产品列表失败,请稍后重试', 'error');
|
||||
console.error('加载产品列表失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染产品列表
|
||||
function renderProducts(products) {
|
||||
const container = document.getElementById('productsContainer');
|
||||
|
||||
if (products.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-box-open fa-3x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">暂无产品</h4>
|
||||
<p class="text-muted">当前没有可购买的产品</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let productsHtml = '';
|
||||
products.forEach(product => {
|
||||
productsHtml += `
|
||||
<div class="col-lg-4 col-md-6 mb-3">
|
||||
<div class="card product-card h-100" onclick="selectProduct(${JSON.stringify(product).replace(/"/g, '"')})">
|
||||
<img src="${product.image_path || '/static/images/product-default.png'}"
|
||||
class="card-img-top product-image"
|
||||
alt="${product.product_name}">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">${product.product_name}</h6>
|
||||
<p class="card-text small text-muted">${product.description || '暂无描述'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = productsHtml;
|
||||
}
|
||||
|
||||
// 选择产品
|
||||
function selectProduct(product) {
|
||||
selectedProduct = product;
|
||||
document.getElementById('selectedProduct').textContent = product.product_name;
|
||||
|
||||
// 更新UI选中状态
|
||||
document.querySelectorAll('.product-card').forEach(card => {
|
||||
card.classList.remove('border-primary');
|
||||
});
|
||||
event.currentTarget.classList.add('border-primary');
|
||||
|
||||
// 加载该产品的套餐
|
||||
loadPlans(product.product_id);
|
||||
|
||||
// 更新支付按钮状态
|
||||
updatePayButton();
|
||||
}
|
||||
|
||||
// 加载套餐
|
||||
function loadPlans(productId) {
|
||||
// 显示加载状态
|
||||
document.getElementById('plansContainer').innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<p class="mt-2">正在加载套餐信息...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 调用API获取套餐信息
|
||||
apiRequest(`/api/v1/user/licenses/packages?product_id=${productId}`)
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
renderPlans(data.data.packages || []);
|
||||
} else {
|
||||
showNotification('加载套餐信息失败: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('加载套餐信息失败,请稍后重试', 'error');
|
||||
console.error('加载套餐信息失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染套餐
|
||||
function renderPlans(plans) {
|
||||
const container = document.getElementById('plansContainer');
|
||||
|
||||
if (plans.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-tags fa-2x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">暂无套餐</h4>
|
||||
<p class="text-muted">该产品暂无可用的授权套餐</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let plansHtml = '';
|
||||
plans.forEach(plan => {
|
||||
plansHtml += `
|
||||
<div class="col-lg-6 mb-3">
|
||||
<div class="plan-card p-3" onclick="selectPlan(${JSON.stringify(plan).replace(/"/g, '"')})">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1">${plan.name}</h6>
|
||||
<p class="small text-muted mb-2">${plan.description || '暂无描述'}</p>
|
||||
<div class="plan-price">¥${plan.price.toFixed(2)}</div>
|
||||
<!-- 显示库存信息 -->
|
||||
${plan.stock !== undefined && plan.stock !== -1 ?
|
||||
`<div class="small text-muted mt-1">剩余库存: ${plan.stock}</div>` :
|
||||
'<div class="small text-muted mt-1">库存: 无限</div>'}
|
||||
</div>
|
||||
<span class="badge bg-primary">${plan.duration_text || getDurationType(plan.duration)}</span>
|
||||
</div>
|
||||
<ul class="small mt-3 mb-0">
|
||||
<li>有效期: ${plan.duration_text || getDurationType(plan.duration)}</li>
|
||||
<li>设备数: ${plan.max_devices || '无限制'}</li>
|
||||
${plan.stock !== undefined && plan.stock === 0 ?
|
||||
'<li class="text-danger">库存不足</li>' : ''}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = plansHtml;
|
||||
}
|
||||
|
||||
// 选择套餐
|
||||
function selectPlan(plan) {
|
||||
selectedPlan = plan;
|
||||
document.getElementById('selectedPlan').textContent = plan.name;
|
||||
|
||||
// 更新UI选中状态
|
||||
document.querySelectorAll('.plan-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
event.currentTarget.classList.add('selected');
|
||||
|
||||
// 更新总价
|
||||
updateTotalAmount();
|
||||
|
||||
// 更新支付按钮状态
|
||||
updatePayButton();
|
||||
}
|
||||
|
||||
// 获取时长类型
|
||||
function getDurationType(days) {
|
||||
if (days === -1) {
|
||||
return '永久卡';
|
||||
} else if (days === 1) {
|
||||
return '天卡';
|
||||
} else if (days === 30) {
|
||||
return '月卡';
|
||||
} else if (days === 90) {
|
||||
return '季卡';
|
||||
} else if (days === 365) {
|
||||
return '年卡';
|
||||
} else {
|
||||
return `${days}天卡`;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新总价
|
||||
function updateTotalAmount() {
|
||||
if (selectedPlan) {
|
||||
const total = selectedPlan.price * selectedQuantity;
|
||||
document.getElementById('totalAmount').textContent = `¥${total.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新支付按钮状态
|
||||
function updatePayButton() {
|
||||
const payBtn = document.getElementById('payBtn');
|
||||
if (selectedProduct && selectedPlan) {
|
||||
payBtn.disabled = false;
|
||||
} else {
|
||||
payBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理支付
|
||||
function processPayment() {
|
||||
if (!selectedProduct || !selectedPlan) {
|
||||
showNotification('请选择产品和套餐', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const paymentMethod = document.querySelector('.payment-method.selected').dataset.method;
|
||||
|
||||
// 显示加载状态
|
||||
const payBtn = document.getElementById('payBtn');
|
||||
const originalText = payBtn.innerHTML;
|
||||
payBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status"></span>处理中...';
|
||||
payBtn.disabled = true;
|
||||
|
||||
// 调用API创建订单
|
||||
const orderData = {
|
||||
product_id: selectedProduct.product_id,
|
||||
package_id: selectedPlan.package_id,
|
||||
quantity: selectedQuantity,
|
||||
payment_method: paymentMethod
|
||||
};
|
||||
|
||||
apiRequest('/user/orders', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(orderData)
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification('订单创建成功!请完成支付', 'success');
|
||||
// 这里应该跳转到支付页面或显示支付二维码
|
||||
} else {
|
||||
showNotification('创建订单失败: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('创建订单失败,请稍后重试', 'error');
|
||||
console.error('创建订单失败:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
payBtn.innerHTML = originalText;
|
||||
payBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
508
app/web/templates/user/product_detail.html
Normal file
508
app/web/templates/user/product_detail.html
Normal file
@@ -0,0 +1,508 @@
|
||||
{% extends "user/base.html" %}
|
||||
|
||||
{% block title %}产品详情 - {{ config.SITE_NAME or '软件授权管理系统' }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.product-detail-image {
|
||||
max-height: 400px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.version-table th {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.download-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.feature-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('user.user_index') }}">首页</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('user.user_products') }}">产品中心</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">产品详情</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mb-4 mb-lg-0">
|
||||
<img src="/static/images/product-default.png"
|
||||
class="img-fluid product-detail-image"
|
||||
alt="产品图片" id="productImage">
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<h1 class="mb-3" id="productName">产品名称</h1>
|
||||
<p class="lead" id="productDescription">产品描述信息,这里是产品的详细介绍内容。</p>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4"><strong>产品版本:</strong></div>
|
||||
<div class="col-sm-8" id="productVersion">v1.0.0</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4"><strong>发布状态:</strong></div>
|
||||
<div class="col-sm-8">
|
||||
<span class="badge bg-success" id="productStatus">已发布</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4"><strong>分类:</strong></div>
|
||||
<div class="col-sm-8" id="productCategory">办公软件</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-sm-4"><strong>更新时间:</strong></div>
|
||||
<div class="col-sm-8" id="updateTime">2023-10-01 10:30:00</div>
|
||||
</div>
|
||||
|
||||
<div class="download-section">
|
||||
<h4 class="mb-3">
|
||||
<i class="fas fa-download me-2"></i>下载产品
|
||||
</h4>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control"
|
||||
id="licenseKey"
|
||||
placeholder="请输入卡密">
|
||||
<button class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
id="verifyLicenseBtn">
|
||||
验证
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="downloadSection" style="display: none;">
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
卡密验证成功!
|
||||
</div>
|
||||
<button class="btn btn-success w-100" id="downloadBtn">
|
||||
<i class="fas fa-download me-2"></i>
|
||||
立即下载
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-center">
|
||||
<a href="{{ url_for('user.user_license_purchase') }}" class="btn btn-link">
|
||||
<i class="fas fa-shopping-cart me-1"></i>购买卡密
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4" id="featuresSection" style="display: none;">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">产品功能特性</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="feature-list" id="featuresList">
|
||||
<!-- 功能特性将通过JavaScript动态加载 -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">版本历史</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped version-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>版本</th>
|
||||
<th>发布日期</th>
|
||||
<th>更新内容</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="versionHistory">
|
||||
<tr>
|
||||
<td>v1.0.0</td>
|
||||
<td>2023-10-01</td>
|
||||
<td>初始版本发布</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-download me-1"></i>下载
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// 产品详情页面JavaScript
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('产品详情页面加载完成');
|
||||
|
||||
// 从URL中获取产品ID
|
||||
const urlPath = window.location.pathname;
|
||||
const productId = urlPath.split('/').pop();
|
||||
|
||||
// 加载产品详情
|
||||
loadProductDetail(productId);
|
||||
|
||||
// 验证卡密按钮事件
|
||||
document.getElementById('verifyLicenseBtn').addEventListener('click', function() {
|
||||
const licenseKey = document.getElementById('licenseKey').value.trim();
|
||||
|
||||
if (!licenseKey) {
|
||||
showNotification('请输入卡密', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
showLoading();
|
||||
|
||||
// 调用API验证卡密
|
||||
apiRequest('/user/licenses/verify', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ license_key: licenseKey })
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 验证成功,显示下载按钮
|
||||
document.getElementById('downloadSection').style.display = 'block';
|
||||
showNotification('卡密验证成功!', 'success');
|
||||
} else {
|
||||
showNotification('卡密验证失败: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('验证卡密失败,请稍后重试', 'error');
|
||||
console.error('验证卡密失败:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
});
|
||||
|
||||
// 下载按钮事件
|
||||
document.getElementById('downloadBtn').addEventListener('click', function() {
|
||||
showNotification('开始下载...', 'info');
|
||||
// 这里应该触发实际的下载逻辑
|
||||
});
|
||||
});
|
||||
|
||||
// 加载产品详情
|
||||
function loadProductDetail(productId) {
|
||||
if (!productId) {
|
||||
showNotification('产品ID无效', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
showLoading();
|
||||
|
||||
// 调用API获取产品详情
|
||||
apiRequest(`/api/v1/user/products/${productId}`)
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
renderProductDetail(data.data);
|
||||
} else {
|
||||
showNotification('加载产品详情失败: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('加载产品详情失败,请稍后重试', 'error');
|
||||
console.error('加载产品详情失败:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
|
||||
// 转义HTML特殊字符
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>'"]/g, function(m) { return map[m]; });
|
||||
}
|
||||
|
||||
// 渲染产品详情
|
||||
function renderProductDetail(product) {
|
||||
// 更新页面内容
|
||||
document.getElementById('productName').textContent = product.product_name;
|
||||
document.getElementById('productDescription').textContent = product.description || '暂无描述';
|
||||
document.getElementById('productVersion').textContent = product.latest_version || '1.0.0';
|
||||
document.getElementById('productStatus').textContent = product.status_name;
|
||||
document.getElementById('productStatus').className = `badge ${product.status === 1 ? 'bg-success' : 'bg-secondary'}`;
|
||||
document.getElementById('updateTime').textContent = product.update_time;
|
||||
|
||||
// 更新产品图片
|
||||
const productImage = document.getElementById('productImage');
|
||||
if (product.image_url) {
|
||||
productImage.src = product.image_url;
|
||||
} else {
|
||||
productImage.src = '/static/images/product-default.png';
|
||||
}
|
||||
|
||||
// 更新功能特性
|
||||
const featuresList = document.getElementById('featuresList');
|
||||
const featuresSection = document.getElementById('featuresSection');
|
||||
if (product.features) {
|
||||
// 将功能特性按行分割并渲染
|
||||
const features = product.features.split('\n').filter(f => f.trim() !== '');
|
||||
if (features.length > 0) {
|
||||
let featuresHtml = '';
|
||||
features.forEach(feature => {
|
||||
featuresHtml += `<li><i class="fas fa-check-circle text-success me-2"></i>${escapeHtml(feature)}</li>`;
|
||||
});
|
||||
featuresList.innerHTML = featuresHtml;
|
||||
featuresSection.style.display = 'block';
|
||||
} else {
|
||||
featuresSection.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
featuresSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// 更新版本历史
|
||||
const versionHistory = document.getElementById('versionHistory');
|
||||
if (product.recent_updates && product.recent_updates.length > 0) {
|
||||
let versionHtml = '';
|
||||
product.recent_updates.forEach(update => {
|
||||
// 安全地处理下载URL
|
||||
const downloadUrl = update.download_url ? escapeHtml(update.download_url) : '';
|
||||
console.log('Rendering version update:', update);
|
||||
versionHtml += `
|
||||
<tr>
|
||||
<td>${escapeHtml(update.version_num)}</td>
|
||||
<td>${formatDate(update.update_time)}</td>
|
||||
<td>${escapeHtml(update.update_log || '无更新说明')}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary download-version-btn"
|
||||
data-product="${escapeHtml(product.product_id)}"
|
||||
data-version="${escapeHtml(update.version_num)}"
|
||||
data-url="${downloadUrl}">
|
||||
<i class="fas fa-download me-1"></i>下载
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
versionHistory.innerHTML = versionHtml;
|
||||
|
||||
// 为每个下载按钮添加点击事件
|
||||
document.querySelectorAll('.download-version-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const productId = this.getAttribute('data-product');
|
||||
const versionNum = this.getAttribute('data-version');
|
||||
const downloadUrl = this.getAttribute('data-url');
|
||||
console.log('Download button clicked:', {productId, versionNum, downloadUrl});
|
||||
downloadVersion(productId, versionNum, downloadUrl);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加下载版本的函数
|
||||
function downloadVersion(productId, versionNum, downloadUrl) {
|
||||
console.log('Download version called:', {productId, versionNum, downloadUrl});
|
||||
showLoading();
|
||||
|
||||
// 如果有直接的下载URL,直接处理
|
||||
if (downloadUrl && downloadUrl.trim() !== '') {
|
||||
try {
|
||||
console.log('Processing direct download URL:', downloadUrl);
|
||||
// 检查URL是否是本地上传的文件(相对路径)
|
||||
if (downloadUrl.startsWith('/')) {
|
||||
// 本地文件,直接下载
|
||||
window.open(downloadUrl, '_blank');
|
||||
showNotification('开始下载...', 'info');
|
||||
} else {
|
||||
// 外部链接,询问用户是否跳转
|
||||
if (confirm(`此版本是外部链接,将跳转到:${downloadUrl}\n是否继续?`)) {
|
||||
window.open(downloadUrl, '_blank');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('下载处理出错:', e);
|
||||
showNotification('下载处理出错,请稍后重试', 'error');
|
||||
}
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有直接的下载URL,检查下载权限
|
||||
console.log('Checking download permissions for product:', productId);
|
||||
apiRequest(`/api/v1/user/downloads/check?product_id=${productId}`)
|
||||
.then(data => {
|
||||
console.log('Download permission check result:', data);
|
||||
if (data.success) {
|
||||
const downloadInfo = data.data;
|
||||
console.log('Download info:', downloadInfo);
|
||||
|
||||
// 如果有下载链接,直接处理
|
||||
if (downloadInfo.download_url && downloadInfo.download_url.trim() !== '') {
|
||||
try {
|
||||
console.log('Processing download URL from API:', downloadInfo.download_url);
|
||||
// 检查URL是否是本地上传的文件(相对路径)
|
||||
if (downloadInfo.download_url.startsWith('/')) {
|
||||
// 本地文件,直接下载
|
||||
window.open(downloadInfo.download_url, '_blank');
|
||||
showNotification('开始下载...', 'info');
|
||||
} else {
|
||||
// 外部链接,询问用户是否跳转
|
||||
if (confirm(`此版本是外部链接,将跳转到:${downloadInfo.download_url}\n是否继续?`)) {
|
||||
window.open(downloadInfo.download_url, '_blank');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('下载处理出错:', e);
|
||||
showNotification('下载处理出错,请稍后重试', 'error');
|
||||
}
|
||||
} else {
|
||||
console.log('No download URL available in API response');
|
||||
// 检查是否是付费产品
|
||||
if (downloadInfo.is_paid) {
|
||||
// 显示卡密输入模态框
|
||||
showLicenseVerificationModal(productId, versionNum, downloadInfo.download_url);
|
||||
} else {
|
||||
showNotification('暂无下载链接', 'warning');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showNotification('检查下载权限失败: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('检查下载权限失败:', error);
|
||||
showNotification('检查下载权限失败,请稍后重试', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
|
||||
// 显示卡密验证模态框
|
||||
function showLicenseVerificationModal(productId, versionNum, downloadUrl) {
|
||||
// 创建模态框HTML
|
||||
const modalHtml = `
|
||||
<div class="modal fade" id="licenseVerifyModal" tabindex="-1" aria-labelledby="licenseVerifyModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="licenseVerifyModalLabel">卡密验证</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="licenseKeyInput" class="form-label">请输入您的卡密</label>
|
||||
<input type="text" class="form-control" id="licenseKeyInput" placeholder="请输入卡密">
|
||||
<div class="form-text">版本: ${versionNum}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" id="verifyLicenseBtn">验证并下载</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 添加到页面中
|
||||
if (!document.getElementById('licenseVerifyModal')) {
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
}
|
||||
|
||||
// 显示模态框
|
||||
const modal = new bootstrap.Modal(document.getElementById('licenseVerifyModal'));
|
||||
modal.show();
|
||||
|
||||
// 绑定验证按钮事件
|
||||
document.getElementById('verifyLicenseBtn').onclick = function() {
|
||||
const licenseKey = document.getElementById('licenseKeyInput').value.trim();
|
||||
if (!licenseKey) {
|
||||
showNotification('请输入卡密', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
verifyAndDownload(productId, licenseKey, downloadUrl);
|
||||
};
|
||||
}
|
||||
|
||||
// 验证卡密并下载
|
||||
function verifyAndDownload(productId, licenseKey, downloadUrl) {
|
||||
showLoading();
|
||||
|
||||
apiRequest(`/api/v1/user/licenses/verify?license_key=${encodeURIComponent(licenseKey)}&product_id=${productId}`)
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 验证成功,开始下载
|
||||
if (data.data.download_url) {
|
||||
window.open(data.data.download_url, '_blank');
|
||||
showNotification('验证成功,开始下载...', 'success');
|
||||
|
||||
// 关闭模态框
|
||||
const modalElement = document.getElementById('licenseVerifyModal');
|
||||
if (modalElement) {
|
||||
const modal = bootstrap.Modal.getInstance(modalElement);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showNotification('暂无下载链接', 'warning');
|
||||
}
|
||||
} else {
|
||||
showNotification('卡密验证失败: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('卡密验证失败,请稍后重试', 'error');
|
||||
console.error('卡密验证失败:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
255
app/web/templates/user/products.html
Normal file
255
app/web/templates/user/products.html
Normal file
@@ -0,0 +1,255 @@
|
||||
{% extends "user/base.html" %}
|
||||
|
||||
{% block title %}产品中心 - {{ config.SITE_NAME or '软件授权管理系统' }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.product-card {
|
||||
transition: transform 0.3s ease;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.product-image {
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.search-filters {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">产品中心</h1>
|
||||
|
||||
<!-- 搜索和筛选区 -->
|
||||
<div class="search-filters">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="搜索产品名称...">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="statusFilter">
|
||||
<option value="">所有状态</option>
|
||||
<option value="1">已发布</option>
|
||||
<option value="0">未发布</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button class="btn btn-primary w-100" id="searchBtn">
|
||||
<i class="fas fa-search me-1"></i>搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 产品列表 -->
|
||||
<div class="row" id="productsContainer">
|
||||
<!-- 产品项将通过JavaScript动态加载 -->
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<p class="mt-2">正在加载产品列表...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="产品分页" class="mt-4">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript动态生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// 产品列表页面JavaScript
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('产品列表页面加载完成');
|
||||
|
||||
// 初始化产品列表
|
||||
loadProducts();
|
||||
|
||||
// 搜索按钮事件
|
||||
document.getElementById('searchBtn').addEventListener('click', function() {
|
||||
loadProducts();
|
||||
});
|
||||
|
||||
// 回车搜索
|
||||
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
loadProducts();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 加载产品列表
|
||||
function loadProducts(page = 1) {
|
||||
const searchInput = document.getElementById('searchInput').value;
|
||||
const statusFilter = document.getElementById('statusFilter').value;
|
||||
|
||||
// 构建查询参数
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', page);
|
||||
if (searchInput) params.append('keyword', searchInput);
|
||||
if (statusFilter) params.append('status', statusFilter);
|
||||
|
||||
// 显示加载状态
|
||||
document.getElementById('productsContainer').innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<p class="mt-2">正在加载产品列表...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 调用API获取产品列表
|
||||
apiRequest(`/api/v1/user/products?${params.toString()}`)
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
renderProducts(data.data.products, data.data.pagination);
|
||||
} else {
|
||||
showNotification('加载产品列表失败: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('加载产品列表失败,请稍后重试', 'error');
|
||||
console.error('加载产品列表失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染产品列表
|
||||
function renderProducts(products, pagination) {
|
||||
const container = document.getElementById('productsContainer');
|
||||
|
||||
if (products.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-box-open fa-3x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">暂无产品</h4>
|
||||
<p class="text-muted">当前没有符合条件的产品</p>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('pagination').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成产品卡片HTML
|
||||
let productsHtml = '';
|
||||
if (!Array.isArray(products) || products.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-box-open fa-3x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">暂无产品</h4>
|
||||
<p class="text-muted">当前没有符合条件的产品</p>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('pagination').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
products.forEach(product => {
|
||||
// 处理图片路径,如果没有图片则使用默认图片
|
||||
const imagePath = product.image_path || '/static/images/product-default.png';
|
||||
productsHtml += `
|
||||
<div class="col-lg-4 col-md-6 mb-4">
|
||||
<div class="card product-card h-100">
|
||||
<img src="${imagePath}"
|
||||
class="card-img-top product-image"
|
||||
alt="${product.product_name}">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">${product.product_name}</h5>
|
||||
<p class="card-text flex-grow-1">${product.description || '暂无描述'}</p>
|
||||
<div class="mt-auto">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted">版本: ${product.latest_version || '1.0.0'}</span>
|
||||
<span class="badge ${product.status === 1 ? 'bg-success' : 'bg-secondary'}">
|
||||
${product.status === 1 ? '已发布' : '未发布'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<a href="/index/products/${product.product_id}" class="btn btn-primary w-100">
|
||||
<i class="fas fa-info-circle me-1"></i>查看详情
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = productsHtml;
|
||||
|
||||
// 渲染分页
|
||||
renderPagination(pagination);
|
||||
}
|
||||
|
||||
// 渲染分页
|
||||
function renderPagination(pagination) {
|
||||
const paginationContainer = document.getElementById('pagination');
|
||||
|
||||
if (pagination.total_pages <= 1) {
|
||||
paginationContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let paginationHtml = '';
|
||||
|
||||
// 上一页
|
||||
if (pagination.current_page > 1) {
|
||||
paginationHtml += `
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="#" onclick="loadProducts(${pagination.current_page - 1}); return false;">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 页码
|
||||
for (let i = Math.max(1, pagination.current_page - 2);
|
||||
i <= Math.min(pagination.total_pages, pagination.current_page + 2);
|
||||
i++) {
|
||||
paginationHtml += `
|
||||
<li class="page-item ${i === pagination.current_page ? 'active' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadProducts(${i}); return false;">${i}</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
if (pagination.current_page < pagination.total_pages) {
|
||||
paginationHtml += `
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="#" onclick="loadProducts(${pagination.current_page + 1}); return false;">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
paginationContainer.innerHTML = paginationHtml;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
677
app/web/templates/user/ticket.html
Normal file
677
app/web/templates/user/ticket.html
Normal file
@@ -0,0 +1,677 @@
|
||||
{% extends "user/base.html" %}
|
||||
|
||||
{% block title %}售后服务 - {{ config.SITE_NAME or '软件授权管理系统' }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.ticket-form {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.ticket-history {
|
||||
border-left: 3px solid #667eea;
|
||||
}
|
||||
|
||||
.ticket-item {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ticket-status-open {
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
.ticket-status-closed {
|
||||
background-color: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
}
|
||||
|
||||
.emergency-contact {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="mb-4">售后服务</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选项卡导航 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs" id="serviceTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="ticket-tab" data-bs-toggle="tab" data-bs-target="#ticket-panel" type="button" role="tab">
|
||||
<i class="fas fa-ticket-alt me-2"></i>工单查询
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="order-tab" data-bs-toggle="tab" data-bs-target="#order-panel" type="button" role="tab">
|
||||
<i class="fas fa-file-invoice me-2"></i>订单查询
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选项卡内容 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="tab-content" id="serviceTabContent">
|
||||
<!-- 工单查询面板 -->
|
||||
<div class="tab-pane fade show active" id="ticket-panel" role="tabpanel">
|
||||
<div class="row mt-3">
|
||||
<!-- 紧急通道 -->
|
||||
<div class="col-lg-4 mb-4">
|
||||
<div class="emergency-contact">
|
||||
<h3 class="mb-3">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
紧急通道
|
||||
</h3>
|
||||
<p class="mb-4">遇到紧急问题?立即联系我们</p>
|
||||
<button class="btn btn-light" data-bs-toggle="modal" data-bs-target="#wechatModal">
|
||||
<i class="fab fa-weixin me-2"></i>微信紧急联系
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工单提交表单 -->
|
||||
<div class="col-lg-8 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">提交工单</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="ticketForm" class="ticket-form">
|
||||
<div class="mb-3">
|
||||
<label for="contactName" class="form-label">联系人姓名</label>
|
||||
<input type="text" class="form-control" id="contactName" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="contactPhone" class="form-label">联系电话</label>
|
||||
<input type="tel" class="form-control" id="contactPhone" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="contactEmail" class="form-label">联系邮箱</label>
|
||||
<input type="email" class="form-control" id="contactEmail" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="productSelect" class="form-label">相关产品</label>
|
||||
<select class="form-select" id="productSelect" required>
|
||||
<option value="">请选择相关产品</option>
|
||||
<option value="1">产品一</option>
|
||||
<option value="2">产品二</option>
|
||||
<option value="3">产品三</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="ticketTitle" class="form-label">工单标题</label>
|
||||
<input type="text" class="form-control" id="ticketTitle" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="ticketContent" class="form-label">问题描述</label>
|
||||
<textarea class="form-control" id="ticketContent" rows="5" required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="prioritySelect" class="form-label">优先级</label>
|
||||
<select class="form-select" id="prioritySelect">
|
||||
<option value="1">低</option>
|
||||
<option value="2" selected>中</option>
|
||||
<option value="3">高</option>
|
||||
<option value="4">紧急</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100" id="submitTicketBtn">
|
||||
<i class="fas fa-paper-plane me-2"></i>提交工单
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工单历史 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">工单历史</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- 工单查询输入框 -->
|
||||
<div class="mb-3">
|
||||
<label for="ticketQueryPhone" class="form-label">手机号查询</label>
|
||||
<div class="input-group">
|
||||
<input type="tel" class="form-control" id="ticketQueryPhone" placeholder="请输入手机号查询工单历史">
|
||||
<button class="btn btn-outline-primary" type="button" id="queryTicketsBtn" onclick="queryTicketsHistory()">
|
||||
<i class="fas fa-search me-1"></i>查询
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">输入手机号查询该手机号的所有工单记录</div>
|
||||
</div>
|
||||
|
||||
<!-- 工单列表 -->
|
||||
<div id="ticketsContainer">
|
||||
<!-- 工单列表将通过JavaScript动态加载 -->
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-info-circle fa-2x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">请输入手机号查询工单历史</h4>
|
||||
<p class="text-muted">输入手机号后将显示该手机号的所有工单记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单查询面板 -->
|
||||
<div class="tab-pane fade" id="order-panel" role="tabpanel">
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">查询订单</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="orderQueryForm">
|
||||
<div class="row">
|
||||
<div class="col-md-7 mb-3">
|
||||
<label for="queryPhone" class="form-label">手机号</label>
|
||||
<input type="tel" class="form-control" id="queryPhone" placeholder="请输入手机号" required>
|
||||
<div class="form-text">输入手机号查询该手机号的所有订单</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100" id="queryOrderBtn">
|
||||
<i class="fas fa-search me-1"></i>查询所有订单
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-secondary w-100" id="clearOrderBtn" onclick="clearOrderQuery()">
|
||||
<i class="fas fa-eraser me-1"></i>清空
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单查询结果 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div id="orderResultContainer" class="mt-3">
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-info-circle fa-2x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">请输入手机号查询订单</h4>
|
||||
<p class="text-muted">输入手机号后将显示该手机号的所有订单记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 微信二维码模态框 -->
|
||||
<div class="modal fade" id="wechatModal" tabindex="-1" aria-labelledby="wechatModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="wechatModalLabel">紧急联系</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<p>扫码添加微信,快速响应紧急需求</p>
|
||||
<img src="/static/images/wechat-qrcode.jpg" alt="微信二维码" class="img-fluid mb-3" style="max-width: 200px;">
|
||||
<p class="text-muted small">微信ID: TaiYiSupport</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// 售后服务页面JavaScript
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('售后服务页面加载完成');
|
||||
|
||||
// 初始化产品列表
|
||||
loadProducts();
|
||||
|
||||
// 工单提交表单事件
|
||||
document.getElementById('ticketForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
submitTicket();
|
||||
});
|
||||
|
||||
// 订单查询表单事件
|
||||
document.getElementById('orderQueryForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
queryOrder();
|
||||
});
|
||||
|
||||
// 工单历史查询输入框回车事件
|
||||
document.getElementById('ticketQueryPhone').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
queryTicketsHistory();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 提交工单
|
||||
function submitTicket() {
|
||||
const formData = {
|
||||
product_id: document.getElementById('productSelect').value,
|
||||
contact_person: document.getElementById('contactName').value.trim(),
|
||||
phone: document.getElementById('contactPhone').value.trim(),
|
||||
title: document.getElementById('ticketTitle').value.trim(),
|
||||
description: document.getElementById('ticketContent').value.trim(),
|
||||
priority: document.getElementById('prioritySelect').value
|
||||
};
|
||||
|
||||
// 基础验证
|
||||
if (!formData.contact_person || !formData.phone || !formData.product_id ||
|
||||
!formData.title || !formData.description) {
|
||||
showNotification('请填写所有必填字段', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(formData.phone)) {
|
||||
showNotification('请输入正确的手机号码', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
const submitBtn = document.getElementById('submitTicketBtn');
|
||||
const originalText = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status"></span>提交中...';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// 调用API提交工单
|
||||
apiRequest('/api/v1/user/tickets', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification('工单提交成功!', 'success');
|
||||
document.getElementById('ticketForm').reset();
|
||||
// 工单提交成功后,如果工单查询框有手机号,则刷新工单历史
|
||||
const queryPhone = document.getElementById('ticketQueryPhone').value.trim();
|
||||
if (queryPhone) {
|
||||
loadTickets(queryPhone);
|
||||
}
|
||||
} else {
|
||||
showNotification('工单提交失败: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('工单提交失败,请稍后重试', 'error');
|
||||
console.error('提交工单失败:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
submitBtn.innerHTML = originalText;
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 加载工单历史(带手机号参数)
|
||||
function loadTickets(phone) {
|
||||
// 验证手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(phone)) {
|
||||
document.getElementById('ticketsContainer').innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-exclamation-circle fa-2x text-warning mb-3"></i>
|
||||
<h4 class="text-warning">手机号格式不正确</h4>
|
||||
<p class="text-muted">请输入正确的11位手机号码</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
document.getElementById('ticketsContainer').innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<p class="mt-2">正在加载工单历史...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 调用API获取工单列表
|
||||
apiRequest(`/api/v1/user/tickets?phone=${phone}`)
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
renderTickets(data.data);
|
||||
} else {
|
||||
showNotification('加载工单历史失败: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('加载工单历史失败,请稍后重试', 'error');
|
||||
console.error('加载工单历史失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 查询工单历史(处理查询按钮点击)
|
||||
function queryTicketsHistory() {
|
||||
const phone = document.getElementById('ticketQueryPhone').value.trim();
|
||||
|
||||
// 验证输入
|
||||
if (!phone) {
|
||||
showNotification('请输入手机号', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证手机号格式并加载工单
|
||||
loadTickets(phone);
|
||||
}
|
||||
|
||||
// 渲染工单列表
|
||||
function renderTickets(tickets) {
|
||||
const container = document.getElementById('ticketsContainer');
|
||||
|
||||
if (tickets.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-ticket-alt fa-3x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">暂无工单记录</h4>
|
||||
<p class="text-muted">您还没有提交过工单</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let ticketsHtml = '';
|
||||
tickets.forEach(ticket => {
|
||||
const statusClass = ticket.status === 1 ? 'ticket-status-open' : 'ticket-status-closed';
|
||||
const statusText = ticket.status === 1 ? '处理中' : '已关闭';
|
||||
const priorityText = ['未知', '低', '中', '高', '紧急'][ticket.priority] || '未知';
|
||||
|
||||
ticketsHtml += `
|
||||
<div class="ticket-item ${statusClass}">
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<h5 class="mb-2">${ticket.title}</h5>
|
||||
<p class="mb-2">${ticket.description.substring(0, 100)}${ticket.description.length > 100 ? '...' : ''}</p>
|
||||
<div class="d-flex flex-wrap gap-3 text-muted">
|
||||
<small><i class="fas fa-calendar me-1"></i> ${formatDate(ticket.create_time)}</small>
|
||||
<small><i class="fas fa-user me-1"></i> ${ticket.contact_person}</small>
|
||||
<small><i class="fas fa-box me-1"></i> ${ticket.product_name || '未知产品'}</small>
|
||||
<small><i class="fas fa-exclamation-circle me-1"></i> ${priorityText}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 text-md-end">
|
||||
<span class="badge ${ticket.status === 1 ? 'bg-info' : 'bg-success'}">
|
||||
${statusText}
|
||||
</span>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="viewTicket('${ticket.ticket_number}')">
|
||||
<i class="fas fa-eye me-1"></i>查看
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = ticketsHtml;
|
||||
}
|
||||
|
||||
// 查看工单详情
|
||||
function viewTicket(ticketId) {
|
||||
showNotification('查看工单详情功能待实现', 'info');
|
||||
// 这里可以跳转到工单详情页面或显示模态框
|
||||
}
|
||||
|
||||
// 加载产品列表
|
||||
function loadProducts() {
|
||||
// 调用API获取产品列表
|
||||
apiRequest('/api/v1/user/products')
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
renderProducts(data.data.products || []);
|
||||
} else {
|
||||
showNotification('加载产品列表失败: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('加载产品列表失败,请稍后重试', 'error');
|
||||
console.error('加载产品列表失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染产品列表
|
||||
function renderProducts(products) {
|
||||
const select = document.getElementById('productSelect');
|
||||
|
||||
// 清空现有选项(保留默认选项)
|
||||
select.innerHTML = '<option value="">请选择相关产品</option>';
|
||||
|
||||
// 添加产品选项
|
||||
products.forEach(product => {
|
||||
const option = document.createElement('option');
|
||||
option.value = product.product_id;
|
||||
option.textContent = product.product_name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// 移除之前的监听器,因为现在有了专门的工单查询输入框
|
||||
|
||||
// 查询订单
|
||||
function queryOrder() {
|
||||
const phone = document.getElementById('queryPhone').value.trim();
|
||||
|
||||
// 验证输入
|
||||
if (!phone) {
|
||||
showNotification('请输入手机号', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(phone)) {
|
||||
showNotification('请输入正确的手机号码', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
const container = document.getElementById('orderResultContainer');
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<p class="mt-2">正在查询订单...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 调用API查询订单(通过手机号查询所有订单)
|
||||
apiRequest(`/api/v1/user/orders?phone=${phone}`)
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
renderOrderList(data.data.orders);
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-times-circle fa-3x text-danger mb-3"></i>
|
||||
<h4 class="text-danger">查询失败</h4>
|
||||
<p class="text-muted">${data.message || '暂无订单记录'}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-exclamation-triangle fa-3x text-warning mb-3"></i>
|
||||
<h4 class="text-warning">查询失败</h4>
|
||||
<p class="text-muted">网络错误,请稍后重试</p>
|
||||
</div>
|
||||
`;
|
||||
console.error('查询订单失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 清空订单查询
|
||||
function clearOrderQuery() {
|
||||
document.getElementById('queryPhone').value = '';
|
||||
const container = document.getElementById('orderResultContainer');
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-info-circle fa-2x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">请输入手机号查询订单</h4>
|
||||
<p class="text-muted">输入手机号后将显示该手机号的所有订单记录</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 渲染订单列表
|
||||
function renderOrderList(orders) {
|
||||
const container = document.getElementById('orderResultContainer');
|
||||
|
||||
if (orders.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-file-invoice fa-3x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">暂无订单记录</h4>
|
||||
<p class="text-muted">该手机号下暂无订单记录</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const statusClass = {
|
||||
0: 'warning',
|
||||
1: 'success',
|
||||
2: 'danger',
|
||||
3: 'secondary'
|
||||
};
|
||||
const statusText = {
|
||||
0: '待支付',
|
||||
1: '已支付',
|
||||
2: '已取消',
|
||||
3: '已完成'
|
||||
};
|
||||
|
||||
let ordersHtml = '';
|
||||
orders.forEach(order => {
|
||||
ordersHtml += `
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-file-invoice me-2"></i>
|
||||
${order.order_number}
|
||||
<span class="badge bg-${statusClass[order.status]} ms-2">
|
||||
${statusText[order.status]}
|
||||
</span>
|
||||
</h5>
|
||||
<div class="row mt-3">
|
||||
<div class="col-sm-6">
|
||||
<p class="mb-2">
|
||||
<strong>产品:</strong> ${order.product_name || '未知产品'}
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
<strong>套餐:</strong> ${order.package_id}
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
<strong>数量:</strong> ${order.quantity}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<p class="mb-2">
|
||||
<strong>联系人:</strong> ${order.contact_person}
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
<strong>手机号:</strong> ${order.phone}
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
<strong>创建时间:</strong> ${order.create_time || '-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end">
|
||||
<div class="mb-3">
|
||||
<h4 class="text-success mb-0">¥${order.amount.toFixed(2)}</h4>
|
||||
<small class="text-muted">订单金额</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<p class="mb-1">
|
||||
<strong>支付方式:</strong> ${order.payment_method || '未支付'}
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>支付时间:</strong> ${order.payment_time || '未支付'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="viewOrderDetail('${order.order_number}')">
|
||||
<i class="fas fa-eye me-1"></i>查看详情
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-list me-2"></i>
|
||||
订单列表 (共 ${orders.length} 条记录)
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
${ordersHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 查看订单详情
|
||||
function viewOrderDetail(orderNumber) {
|
||||
// 这里可以跳转到订单详情页面或显示模态框
|
||||
showNotification(`订单详情功能待实现,订单号: ${orderNumber}`, 'info');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user