第一次提交

This commit is contained in:
2026-03-25 15:24:22 +08:00
commit 0f8ac68d4d
156 changed files with 42365 additions and 0 deletions

View 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>

View 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 %}

View 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, '&quot;')})">
<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, '&quot;')})">
<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 %}

View 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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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 %}

View 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 %}

View 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 %}