Kamixitong/app/web/templates/product/list.html
2025-11-19 22:49:24 +08:00

640 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}产品管理 - 软件授权管理系统{% endblock %}
{% block page_title %}产品管理{% endblock %}
{% block page_actions %}
<a href="{{ url_for('web.create_product') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>
创建产品
</a>
{% 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);
});
}
// 加载产品列表
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="${product.product_id}">
</td>
<td><code>${product.product_id}</code></td>
<td>
<strong>${product.product_name}</strong>
${product.latest_version ? `<br><small class="text-muted">最新版本: ${product.latest_version}</small>` : ''}
</td>
<td>${product.description || '-'}</td>
<td>
${product.image_path ? `<img src="${product.image_path}" alt="产品图片" style="max-width: 50px; max-height: 50px;">` : '-'}
</td>
<td>
<span class="badge ${product.status === 1 ? 'bg-success' : 'bg-secondary'}">
${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();
fetch(`/api/v1/products/${productId}`, {
method: 'DELETE',
credentials: 'same-origin',
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();
fetch('/api/v1/products/batch', {
method: 'DELETE',
credentials: 'same-origin',
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();
fetch('/api/v1/products/batch/status', {
method: 'PUT',
credentials: 'same-origin',
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 %}