第一次提交

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,179 @@
{% extends "base.html" %}
{% block title %}创建产品 - 太一软件授权管理系统{% endblock %}
{% block page_title %}创建产品{% endblock %}
{% block page_actions %}
<a href="{{ url_for('web.products') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>
返回列表
</a>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-body">
<form id="product-form">
<div class="mb-3">
<label for="product_name" class="form-label">产品名称 *</label>
<input type="text" class="form-control" id="product_name" name="product_name" required>
<div class="form-text">产品的显示名称,建议使用简洁明了的名称</div>
</div>
<div class="mb-3">
<label for="product_id" class="form-label">产品ID</label>
<input type="text" class="form-control" id="product_id" name="product_id">
<div class="form-text">产品的唯一标识符,如果不填写将自动生成</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">产品描述</label>
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="features" class="form-label">功能特性</label>
<textarea class="form-control" id="features" name="features" rows="4" placeholder="每行一个功能特性,例如:&#10;高性能架构&#10;多平台支持&#10;实时数据同步"></textarea>
<div class="form-text">每行一个功能特性,用于前台展示</div>
</div>
<div class="mb-3">
<label for="image" class="form-label">产品图片</label>
<input type="file" class="form-control" id="image" name="image" accept="image/*">
<div class="form-text">支持 JPG、PNG、GIF 格式,建议尺寸 800x600</div>
</div>
<div class="mb-3">
<label class="form-label">状态</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="status" id="status_enabled" value="1" checked>
<label class="form-check-label" for="status_enabled">启用</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="status" id="status_disabled" value="0">
<label class="form-check-label" for="status_disabled">禁用</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" id="submit-btn">
<i class="fas fa-save me-2"></i>
<span id="submit-text">创建产品</span>
</button>
<a href="{{ url_for('web.products') }}" class="btn btn-secondary">取消</a>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow">
<div class="card-header">
<h6 class="mb-0">创建说明</h6>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li class="mb-2">
<i class="fas fa-info-circle text-primary me-2"></i>
<small>产品名称是必填项,建议使用简洁明了的名称</small>
</li>
<li class="mb-2">
<i class="fas fa-info-circle text-primary me-2"></i>
<small>产品ID如果不填写将自动生成唯一标识符</small>
</li>
<li class="mb-2">
<i class="fas fa-info-circle text-primary me-2"></i>
<small>产品创建后可以添加版本信息和生成卡密</small>
</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initEventListeners();
});
// 初始化事件监听器
function initEventListeners() {
// 表单提交
document.getElementById('product-form').addEventListener('submit', function(e) {
e.preventDefault();
createProduct();
});
}
// 创建产品
function createProduct() {
const submitBtn = document.getElementById('submit-btn');
const submitText = document.getElementById('submit-text');
// 获取表单数据
const formData = new FormData();
const productData = {
product_name: document.getElementById('product_name').value.trim(),
product_id: document.getElementById('product_id').value.trim(),
description: document.getElementById('description').value.trim(),
features: document.getElementById('features').value.trim(),
status: parseInt(document.querySelector('input[name="status"]:checked').value)
};
// 添加产品数据到FormData
formData.append('data', JSON.stringify(productData));
// 添加图片文件到FormData
const imageFile = document.getElementById('image').files[0];
if (imageFile) {
formData.append('image', imageFile);
}
// 基础验证
if (!productData.product_name) {
showNotification('请输入产品名称', 'warning');
return;
}
// 显示加载状态
submitBtn.disabled = true;
submitText.textContent = '创建中...';
// 发送请求
apiRequest('/api/v1/products', {
method: 'POST',
body: formData
})
.then(data => {
if (data.success) {
showNotification('产品创建成功', 'success');
// 延迟跳转到产品列表
setTimeout(() => {
window.location.href = '/products';
}, 1500);
} else {
showNotification(data.message || '创建失败', 'error');
}
})
.catch(error => {
console.error('Failed to create product:', error);
// apiRequest 已经处理了 401 等错误,这里只处理其他错误
if (error.message !== 'SESSION_EXPIRED') {
showNotification('网络错误,请稍后重试', 'error');
}
})
.finally(() => {
// 恢复按钮状态
submitBtn.disabled = false;
submitText.textContent = '创建产品';
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,172 @@
{% extends "base.html" %}
{% block title %}产品详情 - 太一软件授权管理系统{% endblock %}
{% block page_title %}产品详情{% endblock %}
{% block page_actions %}
<a href="{{ url_for('web.products') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>
返回列表
</a>
<a href="{{ url_for('web.edit_product', product_id=product.product_id) }}" class="btn btn-primary">
<i class="fas fa-edit me-2"></i>
编辑产品
</a>
{% endblock %}
{% block content %}
<div class="row">
<!-- 产品基本信息 -->
<div class="col-lg-8">
<div class="card shadow mb-4">
<div class="card-header">
<h6 class="mb-0">基本信息</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">产品ID:</dt>
<dd class="col-sm-8"><code>{{ product.product_id }}</code></dd>
<dt class="col-sm-4">产品名称:</dt>
<dd class="col-sm-8">{{ product.product_name }}</dd>
<dt class="col-sm-4">状态:</dt>
<dd class="col-sm-8">
<span class="badge {% if product.status == 1 %}bg-success{% else %}bg-secondary{% endif %}">
{{ product.status_name }}
</span>
</dd>
</dl>
</div>
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">创建时间:</dt>
<dd class="col-sm-8">{{ product.create_time }}</dd>
<dt class="col-sm-4">更新时间:</dt>
<dd class="col-sm-8">{{ product.update_time }}</dd>
</dl>
</div>
<div class="col-12">
<dl class="row">
<dt class="col-sm-2">产品描述:</dt>
<dd class="col-sm-10">{{ product.description or '-' }}</dd>
</dl>
</div>
{% if product.image_path %}
<div class="col-12">
<dl class="row">
<dt class="col-sm-2">产品图片:</dt>
<dd class="col-sm-10">
<img src="{{ product.image_path }}" alt="产品图片" style="max-width: 300px; max-height: 200px;">
</dd>
</dl>
</div>
{% endif %}
</div>
</div>
</div>
<!-- 统计信息 -->
<div class="card shadow mb-4">
<div class="card-header">
<h6 class="mb-0">统计信息</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<div class="card bg-primary text-white h-100">
<div class="card-body text-center">
<h5 class="card-title">{{ product.total_licenses or 0 }}</h5>
<p class="card-text">总卡密数</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card bg-success text-white h-100">
<div class="card-body text-center">
<h5 class="card-title">{{ product.active_licenses or 0 }}</h5>
<p class="card-text">活跃卡密</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card bg-info text-white h-100">
<div class="card-body text-center">
<h5 class="card-title">{{ product.total_devices or 0 }}</h5>
<p class="card-text">设备数</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card bg-warning text-dark h-100">
<div class="card-body text-center">
<h5 class="card-title">{{ product.latest_version or '-' }}</h5>
<p class="card-text">最新版本</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧快捷操作和说明 -->
<div class="col-lg-4">
<div class="card shadow mb-4">
<div class="card-header">
<h6 class="mb-0">快捷操作</h6>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{{ url_for('web.create_version') }}?product_id={{ product.product_id }}" class="btn btn-outline-primary">
<i class="fas fa-code-branch me-2"></i>
发布新版本
</a>
<a href="{{ url_for('web.generate_license') }}?product_id={{ product.product_id }}" class="btn btn-outline-success">
<i class="fas fa-key me-2"></i>
生成卡密
</a>
<a href="{{ url_for('web.versions') }}?product_id={{ product.product_id }}" class="btn btn-outline-info">
<i class="fas fa-list me-2"></i>
版本管理
</a>
<a href="{{ url_for('web.licenses') }}?product_id={{ product.product_id }}" class="btn btn-outline-warning">
<i class="fas fa-list me-2"></i>
卡密管理
</a>
<a href="{{ url_for('web.packages') }}?product_id={{ product.product_id }}" class="btn btn-outline-secondary">
<i class="fas fa-box me-2"></i>
套餐管理
</a>
</div>
</div>
</div>
<div class="card shadow">
<div class="card-header">
<h6 class="mb-0">操作说明</h6>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li class="mb-2">
<i class="fas fa-info-circle text-primary me-2"></i>
<small>可以为产品创建多个版本</small>
</li>
<li class="mb-2">
<i class="fas fa-info-circle text-primary me-2"></i>
<small>可以为产品生成不同类型的卡密</small>
</li>
<li class="mb-2">
<i class="fas fa-info-circle text-primary me-2"></i>
<small>产品启用状态下才能生成卡密和版本</small>
</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,187 @@
{% extends "base.html" %}
{% block title %}编辑产品 - 太一软件授权管理系统{% endblock %}
{% block page_title %}编辑产品{% endblock %}
{% block page_actions %}
<a href="{{ url_for('web.product_detail', product_id=product.product_id) }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>
返回详情
</a>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-body">
<form id="product-form">
<input type="hidden" id="product_id" value="{{ product.product_id }}">
<div class="mb-3">
<label for="product_name" class="form-label">产品名称 *</label>
<input type="text" class="form-control" id="product_name" name="product_name" value="{{ product.product_name }}" required>
<div class="form-text">产品的显示名称,建议使用简洁明了的名称</div>
</div>
<div class="mb-3">
<label class="form-label">产品ID</label>
<input type="text" class="form-control" value="{{ product.product_id }}" disabled>
<div class="form-text">产品的唯一标识符,创建后不可修改</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">产品描述</label>
<textarea class="form-control" id="description" name="description" rows="3">{{ product.description or '' }}</textarea>
</div>
<div class="mb-3">
<label for="features" class="form-label">功能特性</label>
<textarea class="form-control" id="features" name="features" rows="4">{{ product.features or '' }}</textarea>
<div class="form-text">每行一个功能特性,用于前台展示</div>
</div>
<div class="mb-3">
<label for="image" class="form-label">产品图片</label>
<input type="file" class="form-control" id="image" name="image" accept="image/*">
{% if product.image_path %}
<div class="form-text mt-2">
<small>当前图片:</small><br>
<img src="{{ product.image_path }}" alt="产品图片" style="max-width: 200px; max-height: 150px;">
</div>
{% endif %}
<div class="form-text">支持 JPG、PNG、GIF 格式,建议尺寸 800x600</div>
</div>
<div class="mb-3">
<label class="form-label">状态</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="status" id="status_enabled" value="1" {% if product.status == 1 %}checked{% endif %}>
<label class="form-check-label" for="status_enabled">启用</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="status" id="status_disabled" value="0" {% if product.status == 0 %}checked{% endif %}>
<label class="form-check-label" for="status_disabled">禁用</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" id="submit-btn">
<i class="fas fa-save me-2"></i>
<span id="submit-text">保存修改</span>
</button>
<a href="{{ url_for('web.product_detail', product_id=product.product_id) }}" class="btn btn-secondary">取消</a>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow">
<div class="card-header">
<h6 class="mb-0">编辑说明</h6>
</div>
<div class="card-body">
<ul class="list-unstyled">
<li class="mb-2">
<i class="fas fa-info-circle text-primary me-2"></i>
<small>产品名称是必填项,建议使用简洁明了的名称</small>
</li>
<li class="mb-2">
<i class="fas fa-info-circle text-primary me-2"></i>
<small>产品ID创建后不可修改</small>
</li>
<li class="mb-2">
<i class="fas fa-info-circle text-primary me-2"></i>
<small>禁用产品后将无法生成新的卡密和版本</small>
</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initEventListeners();
});
// 初始化事件监听器
function initEventListeners() {
// 表单提交
document.getElementById('product-form').addEventListener('submit', function(e) {
e.preventDefault();
updateProduct();
});
}
// 更新产品
function updateProduct() {
const submitBtn = document.getElementById('submit-btn');
const submitText = document.getElementById('submit-text');
const productId = document.getElementById('product_id').value;
// 获取表单数据
const formData = new FormData();
const productData = {
product_name: document.getElementById('product_name').value.trim(),
description: document.getElementById('description').value.trim(),
features: document.getElementById('features').value.trim(),
status: parseInt(document.querySelector('input[name="status"]:checked').value)
};
// 添加产品数据到FormData
formData.append('data', JSON.stringify(productData));
// 添加图片文件到FormData如果选择了新图片
const imageFile = document.getElementById('image').files[0];
if (imageFile) {
formData.append('image', imageFile);
}
// 基础验证
if (!productData.product_name) {
showNotification('请输入产品名称', 'warning');
return;
}
// 显示加载状态
submitBtn.disabled = true;
submitText.textContent = '保存中...';
// 发送请求
apiRequest(`/api/v1/products/${productId}`, {
method: 'PUT',
body: formData
})
.then(data => {
if (data.success) {
showNotification('产品更新成功', 'success');
// 延迟跳转到产品详情
setTimeout(() => {
window.location.href = `/products/${productId}`;
}, 1500);
} else {
showNotification(data.message || '更新失败', 'error');
}
})
.catch(error => {
console.error('Failed to update product:', error);
// apiRequest 已经处理了 401 等错误,这里只处理其他错误
if (error.message !== 'SESSION_EXPIRED') {
showNotification('网络错误,请稍后重试', 'error');
}
})
.finally(() => {
// 恢复按钮状态
submitBtn.disabled = false;
submitText.textContent = '保存修改';
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,671 @@
{% extends "base.html" %}
{% block title %}产品管理 - 太一软件授权管理系统{% endblock %}
{% block page_title %}产品管理{% endblock %}
{% block page_actions %}
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary" id="refresh-btn" title="刷新数据">
<i class="fas fa-sync-alt me-1"></i>刷新
</button>
<button type="button" class="btn btn-outline-secondary" id="auto-refresh-btn" title="切换自动刷新">
<i class="fas fa-play me-1"></i><span class="auto-refresh-text">自动刷新</span>
</button>
<a href="{{ url_for('web.create_product') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>
创建产品
</a>
</div>
{% endblock %}
{% block content %}
<!-- 搜索表单 -->
<div class="card shadow mb-4">
<div class="card-body">
<form id="search-form" class="row g-3">
<div class="col-md-4">
<label for="search-keyword" class="form-label">关键词搜索</label>
<input type="text" class="form-control" id="search-keyword" placeholder="产品名称或ID">
</div>
<div class="col-md-8 d-flex align-items-end">
<div class="btn-group" role="group">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search me-2"></i>搜索
</button>
<button type="button" class="btn btn-outline-secondary" id="reset-search">
<i class="fas fa-undo me-2"></i>重置
</button>
</div>
</div>
</form>
</div>
</div>
<!-- 批量操作栏 -->
<div class="card shadow mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<button type="button" class="btn btn-danger btn-sm" id="batch-delete-btn" style="display: none;">
<i class="fas fa-trash me-1"></i>批量删除
</button>
<div class="btn-group btn-group-sm" id="batch-status-btn" style="display: none;">
<button type="button" class="btn btn-outline-success" id="batch-enable-btn">
<i class="fas fa-check-circle me-1"></i>批量启用
</button>
<button type="button" class="btn btn-outline-secondary" id="batch-disable-btn">
<i class="fas fa-ban me-1"></i>批量禁用
</button>
</div>
</div>
<div class="text-muted small">
<span id="selected-count">已选择 0 项</span>
</div>
</div>
</div>
</div>
<!-- 产品列表 -->
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th width="50">
<input type="checkbox" id="select-all-checkbox" class="form-check-input">
</th>
<th>产品ID</th>
<th>产品名称</th>
<th>描述</th>
<th>图片</th>
<th>状态</th>
<th>卡密统计</th>
<th>设备统计</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="product-list">
<tr>
<td colspan="9" class="text-center text-muted">加载中...</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<nav aria-label="分页导航">
<ul class="pagination justify-content-center mb-0" id="pagination">
</ul>
</nav>
</div>
</div>
<!-- 删除确认模态框 -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
确定要删除产品 "<strong id="delete-product-name"></strong>" 吗?此操作不可恢复。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirm-delete">确定删除</button>
</div>
</div>
</div>
</div>
<!-- 批量删除确认模态框 -->
<div class="modal fade" id="batchDeleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">确认批量删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
确定要删除选中的 <strong id="batch-delete-count"></strong> 个产品吗?此操作不可恢复。
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirm-batch-delete">确定删除</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentPage = 1;
let currentKeyword = '';
// 页面加载完成后初始化
// 使用立即执行函数确保在DOM和所有脚本加载完成后执行
(function() {
function init() {
console.log('产品列表页面已加载,开始初始化...');
console.log('apiRequest函数是否存在:', typeof apiRequest);
if (typeof apiRequest === 'function') {
loadProducts();
initEventListeners();
} else {
console.error('apiRequest函数未定义等待脚本加载...');
// 如果apiRequest未定义等待一段时间后重试
setTimeout(function() {
if (typeof apiRequest === 'function') {
loadProducts();
initEventListeners();
} else {
console.error('apiRequest函数仍未定义请检查脚本加载顺序');
}
}, 100);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
// DOM已经加载完成直接执行
init();
}
})();
// 初始化事件监听器
function initEventListeners() {
// 搜索表单
document.getElementById('search-form').addEventListener('submit', function(e) {
e.preventDefault();
currentKeyword = document.getElementById('search-keyword').value.trim();
currentPage = 1;
loadProducts();
});
// 重置搜索
document.getElementById('reset-search').addEventListener('click', function() {
document.getElementById('search-keyword').value = '';
currentKeyword = '';
currentPage = 1;
loadProducts();
});
// 删除确认
document.getElementById('confirm-delete').addEventListener('click', function() {
const productId = document.getElementById('confirm-delete').dataset.productId;
deleteProduct(productId);
});
// 批量删除确认
document.getElementById('confirm-batch-delete').addEventListener('click', function() {
batchDeleteProducts();
});
// 全选/取消全选
document.getElementById('select-all-checkbox').addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.product-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateBatchButtons();
});
// 批量启用
document.getElementById('batch-enable-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateProductStatus(1);
});
// 批量禁用
document.getElementById('batch-disable-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateProductStatus(0);
});
// 刷新按钮
document.getElementById('refresh-btn').addEventListener('click', function() {
console.log('手动刷新数据...');
loadProducts(currentPage);
});
// 自动刷新按钮
document.getElementById('auto-refresh-btn').addEventListener('click', function() {
const isEnabled = toggleAutoRefresh(() => loadProducts(currentPage), 30000);
const icon = this.querySelector('i');
const text = this.querySelector('.auto-refresh-text');
if (isEnabled) {
icon.className = 'fas fa-pause me-1';
text.textContent = '停止刷新';
this.classList.remove('btn-outline-secondary');
this.classList.add('btn-success');
showNotification('已开启自动刷新每30秒', 'info');
} else {
icon.className = 'fas fa-play me-1';
text.textContent = '自动刷新';
this.classList.remove('btn-success');
this.classList.add('btn-outline-secondary');
showNotification('已关闭自动刷新', 'info');
}
});
}
// 加载产品列表
function loadProducts(page = 1) {
console.log('loadProducts函数被调用页码:', page);
const params = new URLSearchParams({
page: page,
per_page: 10
});
if (currentKeyword) {
params.append('keyword', currentKeyword);
}
const apiUrl = `/api/v1/products?${params}`;
console.log('请求API URL:', apiUrl);
console.log('准备请求API:', apiUrl);
// 使用apiRequest函数它已经处理了加载动画和错误处理
apiRequest(apiUrl)
.then(data => {
if (data && data.success && data.data) {
renderProductList(data.data.products);
renderPagination(data.data.pagination);
} else {
renderProductList([]);
renderPagination({ pages: 0, page: 1, has_prev: false, has_next: false });
if (data && data.message) {
showNotification(data.message, 'error');
} else {
showNotification('加载产品列表失败', 'error');
}
}
})
.catch(error => {
console.error('Failed to load products:', error);
// 确保在任何错误情况下都清除加载状态
renderProductList([]);
renderPagination({ pages: 0, page: 1, has_prev: false, has_next: false });
// apiRequest已经处理了401和403错误这里只处理其他错误
if (error.message && !error.message.includes('401') && !error.message.includes('403')) {
showNotification('加载产品列表失败: ' + error.message, 'error');
}
});
}
// 渲染产品列表
function renderProductList(products) {
const tbody = document.getElementById('product-list');
// 防御式编程确保tbody存在
if (!tbody) {
console.error('产品列表容器未找到');
return;
}
// 检查产品数据是否有效
if (!Array.isArray(products) || products.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-muted">暂无数据</td></tr>';
return;
}
tbody.innerHTML = products.map(product => `
<tr>
<td>
<input type="checkbox" class="product-checkbox" data-product-id="${escapeHtml(product.product_id)}">
</td>
<td><code>${escapeHtml(product.product_id)}</code></td>
<td>
<strong>${escapeHtml(product.product_name)}</strong>
${product.latest_version ? `<br><small class="text-muted">最新版本: ${escapeHtml(product.latest_version)}</small>` : ''}
</td>
<td>${escapeHtml(product.description || '-')}</td>
<td>
${product.image_path ? `<img src="${escapeHtml(product.image_path)}" alt="产品图片" style="max-width: 50px; max-height: 50px;">` : '-'}
</td>
<td>
<span class="badge ${product.status === 1 ? 'bg-success' : 'bg-secondary'}">
${escapeHtml(product.status_name)}
</span>
</td>
<td>
<small>
总计: ${product.total_licenses || 0}<br>
活跃: <span class="text-success">${product.active_licenses || 0}</span>
</small>
</td>
<td>
<small>
在线: <span class="text-success">${product.total_devices || 0}</span>
</small>
</td>
<td>
<small>${formatDate(product.create_time)}</small>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="/products/${product.product_id}" class="btn btn-outline-primary" title="查看">
<i class="fas fa-eye"></i>
</a>
<a href="/products/${product.product_id}/edit" class="btn btn-outline-secondary" title="编辑">
<i class="fas fa-edit"></i>
</a>
<button type="button" class="btn btn-outline-danger btn-delete"
data-product-id="${product.product_id}"
data-product-name="${product.product_name}"
title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`).join('');
// 绑定删除按钮事件
document.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', function() {
const productId = this.dataset.productId;
const productName = this.dataset.productName;
showDeleteModal(productId, productName);
});
});
// 绑定复选框事件
document.querySelectorAll('.product-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateBatchButtons);
});
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
updateBatchButtons();
}
// 渲染分页
function renderPagination(pagination) {
const paginationEl = document.getElementById('pagination');
// 防御式编程:确保分页容器存在
if (!paginationEl) {
console.error('分页容器未找到');
return;
}
// 检查分页数据是否有效
if (!pagination || typeof pagination !== 'object' || !Number.isFinite(pagination.pages) || pagination.pages <= 1) {
paginationEl.innerHTML = '';
return;
}
let html = '';
// 上一页
if (pagination.has_prev) {
html += `<li class="page-item">
<a class="page-link" href="#" data-page="${pagination.page - 1}">上一页</a>
</li>`;
}
// 页码
const startPage = Math.max(1, pagination.page - 2);
const endPage = Math.min(pagination.pages, pagination.page + 2);
if (startPage > 1) {
html += `<li class="page-item"><a class="page-link" href="#" data-page="1">1</a></li>`;
if (startPage > 2) {
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
}
}
for (let i = startPage; i <= endPage; i++) {
html += `<li class="page-item ${i === pagination.page ? 'active' : ''}">
<a class="page-link" href="#" data-page="${i}">${i}</a>
</li>`;
}
if (endPage < pagination.pages) {
if (endPage < pagination.pages - 1) {
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
}
html += `<li class="page-item"><a class="page-link" href="#" data-page="${pagination.pages}">${pagination.pages}</a></li>`;
}
// 下一页
if (pagination.has_next) {
html += `<li class="page-item">
<a class="page-link" href="#" data-page="${pagination.page + 1}">下一页</a>
</li>`;
}
paginationEl.innerHTML = html;
// 绑定分页点击事件
paginationEl.querySelectorAll('.page-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const page = parseInt(this.dataset.page);
if (page && page !== currentPage) {
currentPage = page;
loadProducts(page);
}
});
});
}
// 显示删除确认模态框
function showDeleteModal(productId, productName) {
document.getElementById('delete-product-name').textContent = productName;
document.getElementById('confirm-delete').dataset.productId = productId;
const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
modal.show();
}
// 删除产品
function deleteProduct(productId) {
// 显示加载动画
showLoading();
apiRequest(`/api/v1/products/${productId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => {
// 隐藏加载动画
hideLoading();
if (response.status === 401) {
window.location.href = '/login';
throw new Error('未授权访问');
}
return response.json();
})
.then(data => {
if (data && data.success) {
showNotification('产品删除成功', 'success');
loadProducts(currentPage);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('deleteModal'));
modal.hide();
} else {
showNotification(data.message || '删除失败', 'error');
}
})
.catch(error => {
// 隐藏加载动画
hideLoading();
console.error('Failed to delete product:', error);
showNotification('删除失败: ' + (error.message || '未知错误'), 'error');
});
}
// 更新批量操作按钮状态
function updateBatchButtons() {
const selectedCount = document.querySelectorAll('.product-checkbox:checked').length;
const batchDeleteBtn = document.getElementById('batch-delete-btn');
const batchStatusBtn = document.getElementById('batch-status-btn');
if (selectedCount > 0) {
batchDeleteBtn.style.display = 'inline-block';
batchStatusBtn.style.display = 'inline-block';
} else {
batchDeleteBtn.style.display = 'none';
batchStatusBtn.style.display = 'none';
}
// 更新选中数量显示
document.getElementById('selected-count').textContent = `已选择 ${selectedCount}`;
}
// 显示批量删除确认模态框
function showBatchDeleteModal() {
const selectedCount = document.querySelectorAll('.product-checkbox:checked').length;
document.getElementById('batch-delete-count').textContent = selectedCount;
const modal = new bootstrap.Modal(document.getElementById('batchDeleteModal'));
modal.show();
}
// 批量删除产品
function batchDeleteProducts() {
const selectedCheckboxes = document.querySelectorAll('.product-checkbox:checked');
const productIds = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.productId);
// 显示加载动画
showLoading();
apiRequest('/api/v1/products/batch', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ product_ids: productIds })
})
.then(response => {
// 隐藏加载动画
hideLoading();
if (response.status === 401) {
window.location.href = '/login';
throw new Error('未授权访问');
}
return response.json();
})
.then(data => {
if (data && data.success) {
showNotification(data.message || '批量删除成功', 'success');
loadProducts(currentPage);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量删除失败', 'error');
// 如果有无法删除的产品,显示详细信息
if (data.undeletable_products) {
const undeletableInfo = data.undeletable_products.map(p =>
`${p.product_name} (${p.license_count}个卡密)`
).join(', ');
showNotification(`以下产品无法删除: ${undeletableInfo}`, 'error');
}
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
}
})
.catch(error => {
// 隐藏加载动画
hideLoading();
console.error('Failed to batch delete products:', error);
showNotification('批量删除失败: ' + (error.message || '未知错误'), 'error');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
});
}
// 批量更新产品状态
function batchUpdateProductStatus(status) {
const selectedCheckboxes = document.querySelectorAll('.product-checkbox:checked');
const productIds = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.productId);
if (productIds.length === 0) {
showNotification('请至少选择一个产品', 'warning');
return;
}
// 显示加载动画
showLoading();
apiRequest('/api/v1/products/batch/status', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_ids: productIds,
status: status
})
})
.then(response => {
// 隐藏加载动画
hideLoading();
if (response.status === 401) {
window.location.href = '/login';
throw new Error('未授权访问');
}
return response.json();
})
.then(data => {
if (data && data.success) {
showNotification(data.message || '批量更新状态成功', 'success');
loadProducts(currentPage);
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量更新状态失败', 'error');
}
})
.catch(error => {
// 隐藏加载动画
hideLoading();
console.error('Failed to batch update product status:', error);
showNotification('批量更新状态失败: ' + (error.message || '未知错误'), 'error');
});
}
// 在页面加载完成后为批量删除按钮绑定事件
document.addEventListener('DOMContentLoaded', function() {
const batchDeleteBtn = document.getElementById('batch-delete-btn');
if (batchDeleteBtn) {
batchDeleteBtn.addEventListener('click', showBatchDeleteModal);
}
});
</script>
{% endblock %}