Kamixitong/app/web/templates/license/list.html
2025-11-11 21:39:12 +08:00

570 lines
21 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.generate_license') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>
生成卡密
</a>
<a href="{{ url_for('web.import_license') }}" class="btn btn-outline-success">
<i class="fas fa-file-import 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-3">
<input type="text" class="form-control" id="search-keyword" placeholder="搜索卡密或设备信息...">
</div>
<div class="col-md-2">
<select class="form-select" id="search-status">
<option value="">全部状态</option>
<option value="0">未激活</option>
<option value="1">已激活</option>
<option value="2">已过期</option>
<option value="3">已禁用</option>
</select>
</div>
<div class="col-md-2">
<select class="form-select" id="search-type">
<option value="">全部类型</option>
<option value="0">试用卡密</option>
<option value="1">正式卡密</option>
</select>
</div>
<div class="col-md-3">
<input type="text" class="form-control" id="search-product" placeholder="产品ID或名称...">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-outline-primary">
<i class="fas fa-search me-2"></i>
搜索
</button>
</div>
</form>
</div>
</div>
<!-- 卡密列表 -->
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>卡密</th>
<th>产品</th>
<th>类型</th>
<th>状态</th>
<th>设备信息</th>
<th>激活时间</th>
<th>过期时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="license-list">
<tr>
<td colspan="8" class="text-center text-muted">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
加载中...
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<nav aria-label="卡密列表分页">
<ul class="pagination justify-content-center" id="pagination">
<!-- 分页将通过JavaScript动态生成 -->
</ul>
</nav>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentPage = 1;
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
loadLicenses();
initEventListeners();
});
// 初始化事件监听器
function initEventListeners() {
// 搜索表单
document.getElementById('search-form').addEventListener('submit', function(e) {
e.preventDefault();
currentPage = 1;
loadLicenses();
});
}
// 加载卡密列表
function loadLicenses(page = 1) {
const params = new URLSearchParams({
page: page,
per_page: 10
});
// 添加搜索参数
const keyword = document.getElementById('search-keyword').value.trim();
const status = document.getElementById('search-status').value;
const type = document.getElementById('search-type').value;
const product = document.getElementById('search-product').value.trim();
if (keyword) params.append('keyword', keyword);
if (status) params.append('status', status);
if (type) params.append('type', type);
if (product) params.append('product', product);
apiRequest(`/api/v1/licenses?${params}`)
.then(data => {
if (data.success) {
renderLicenseList(data.data.licenses);
renderPagination(data.data.pagination);
} else {
showNotification(data.message || '加载卡密列表失败', 'error');
}
})
.catch(error => {
console.error('Failed to load licenses:', error);
showNotification('加载卡密列表失败', 'error');
});
}
// 渲染卡密列表
function renderLicenseList(licenses) {
const tbody = document.getElementById('license-list');
if (licenses.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无数据</td></tr>';
return;
}
tbody.innerHTML = licenses.map(license => `
<tr>
<td>
<code>${formatLicenseKey(license.license_key)}</code>
<br><small class="text-muted">${license.license_key}</small>
</td>
<td>
${license.product_name || '-'}
<br><small class="text-muted">${license.product_id || '-'}</small>
</td>
<td>
<span class="badge ${license.type === 1 ? 'bg-primary' : 'bg-warning'}">
${license.type === 1 ? '正式卡密' : '试用卡密'}
</span>
</td>
<td>
<span class="badge ${getLicenseStatusClass(license.status)}">
${getLicenseStatusText(license.status)}
</span>
</td>
<td>
${license.device_info ? `
<small>
${license.device_info.machine_code || '-'}<br>
${license.device_info.ip_address || '-'}
</small>
` : '-'}
</td>
<td>
<small>${license.activate_time ? formatDate(license.activate_time) : '-'}</small>
</td>
<td>
<small>${license.expire_time ? formatDate(license.expire_time) : '-'}</small>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary btn-detail"
data-license-key="${license.license_key}"
title="详情">
<i class="fas fa-eye"></i>
</button>
${license.status === 1 ? `
<button type="button" class="btn btn-outline-warning btn-disable"
data-license-key="${license.license_key}"
title="禁用">
<i class="fas fa-ban"></i>
</button>
` : license.status === 3 ? `
<button type="button" class="btn btn-outline-success btn-enable"
data-license-key="${license.license_key}"
title="启用">
<i class="fas fa-check"></i>
</button>
` : ''}
<button type="button" class="btn btn-outline-danger btn-delete"
data-license-key="${license.license_key}"
title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`).join('');
// 绑定事件
document.querySelectorAll('.btn-detail').forEach(btn => {
btn.addEventListener('click', function() {
const licenseKey = this.dataset.licenseKey;
showLicenseDetail(licenseKey);
});
});
document.querySelectorAll('.btn-disable').forEach(btn => {
btn.addEventListener('click', function() {
const licenseKey = this.dataset.licenseKey;
updateLicenseStatus(licenseKey, 3); // 禁用
});
});
document.querySelectorAll('.btn-enable').forEach(btn => {
btn.addEventListener('click', function() {
const licenseKey = this.dataset.licenseKey;
updateLicenseStatus(licenseKey, 1); // 启用
});
});
document.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', function() {
const licenseKey = this.dataset.licenseKey;
deleteLicense(licenseKey);
});
});
}
// 获取卡密状态文本
function getLicenseStatusText(status) {
const statusMap = {
0: '未激活',
1: '已激活',
2: '已过期',
3: '已禁用'
};
return statusMap[status] || '未知';
}
// 获取卡密状态样式类
function getLicenseStatusClass(status) {
const classMap = {
0: 'bg-secondary',
1: 'bg-success',
2: 'bg-warning',
3: 'bg-danger'
};
return classMap[status] || 'bg-secondary';
}
// 渲染分页
function renderPagination(pagination) {
const paginationEl = document.getElementById('pagination');
if (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;
loadLicenses(page);
}
});
});
}
// 显示卡密详情
function showLicenseDetail(licenseKey) {
// 根据卡密key获取卡密详情
apiRequest(`/api/v1/licenses?keyword=${licenseKey}`)
.then(data => {
if (data.success && data.data.licenses.length > 0) {
const license = data.data.licenses[0];
// 显示卡密详情弹窗
showLicenseDetailModal(license);
} else {
showNotification('未找到卡密信息', 'error');
}
})
.catch(error => {
console.error('Failed to load license detail:', error);
showNotification('加载卡密详情失败', 'error');
});
}
// 显示卡密详情弹窗
function showLicenseDetailModal(license) {
// 创建模态框HTML
const modalHtml = `
<div class="modal fade" id="licenseDetailModal" tabindex="-1" aria-labelledby="licenseDetailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="licenseDetailModalLabel">卡密详情</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<table class="table table-borderless">
<tr>
<td class="fw-bold">卡密:</td>
<td><code>${formatLicenseKey(license.license_key)}</code></td>
</tr>
<tr>
<td class="fw-bold">产品:</td>
<td>${license.product_name || '-'}</td>
</tr>
<tr>
<td class="fw-bold">类型:</td>
<td>
<span class="badge ${license.type === 1 ? 'bg-primary' : 'bg-warning'}">
${license.type === 1 ? '正式卡密' : '试用卡密'}
</span>
</td>
</tr>
<tr>
<td class="fw-bold">状态:</td>
<td>
<span class="badge ${getLicenseStatusClass(license.status)}">
${getLicenseStatusText(license.status)}
</span>
</td>
</tr>
<tr>
<td class="fw-bold">有效期:</td>
<td>${license.valid_days === -1 ? '永久' : license.valid_days + '天'}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-borderless">
<tr>
<td class="fw-bold">激活时间:</td>
<td>${license.activate_time || '-'}</td>
</tr>
<tr>
<td class="fw-bold">过期时间:</td>
<td>${license.expire_time || '-'}</td>
</tr>
<tr>
<td class="fw-bold">绑定机器码:</td>
<td>${license.bind_machine_code || '-'}</td>
</tr>
<tr>
<td class="fw-bold">创建时间:</td>
<td>${license.create_time}</td>
</tr>
<tr>
<td class="fw-bold">更新时间:</td>
<td>${license.update_time}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
`;
// 如果模态框已存在,先移除
const existingModal = document.getElementById('licenseDetailModal');
if (existingModal) {
existingModal.remove();
}
// 添加模态框到页面
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('licenseDetailModal'));
modal.show();
// 模态框关闭后移除
document.getElementById('licenseDetailModal').addEventListener('hidden.bs.modal', function () {
this.remove();
});
}
// 更新卡密状态
function updateLicenseStatus(licenseKey, status) {
const action = status === 1 ? '启用' : '禁用';
if (!confirm(`确定要${action}卡密 ${licenseKey} 吗?`)) {
return;
}
// 先获取卡密信息以获取license_id
apiRequest(`/api/v1/licenses?keyword=${licenseKey}`)
.then(data => {
if (data.success && data.data.licenses.length > 0) {
const licenseId = data.data.licenses[0].license_id;
// 使用正确的API路由更新卡密状态
return apiRequest(`/api/v1/licenses/${licenseId}`, {
method: 'PUT',
body: JSON.stringify({ status: status })
});
} else {
throw new Error('未找到卡密');
}
})
.then(data => {
if (data.success) {
showNotification(`${action}成功`, 'success');
loadLicenses(currentPage);
} else {
showNotification(data.message || `${action}失败`, 'error');
}
})
.catch(error => {
console.error('Failed to update license status:', error);
showNotification(`${action}失败`, 'error');
});
}
// 删除卡密
function deleteLicense(licenseKey) {
if (!confirm(`确定要删除卡密 ${licenseKey} 吗?`)) {
return;
}
// 首先尝试正常删除
apiRequest(`/api/v1/licenses/${licenseKey}`, {
method: 'DELETE'
})
.then(data => {
if (data.success) {
showNotification('删除成功', 'success');
loadLicenses(currentPage);
} else {
// 检查是否是因为卡密已激活导致的删除失败
if (data.message && data.message.includes('已激活')) {
// 提供更明确的操作指导
if (confirm('该卡密已激活,不能直接删除。\n点击"确定"将强制删除该卡密(包括解绑设备)\n点击"取消"将保留该卡密')) {
// 强制删除卡密
apiRequest(`/api/v1/licenses/${licenseKey}?force=true`, {
method: 'DELETE'
})
.then(forceData => {
if (forceData.success) {
showNotification('卡密已强制删除成功', 'success');
loadLicenses(currentPage);
} else {
showNotification(forceData.message || '强制删除失败', 'error');
}
})
.catch(error => {
console.error('Failed to force delete license:', error);
showNotification('强制删除失败: ' + error.message, 'error');
});
}
} else {
showNotification(data.message || '删除失败', 'error');
}
}
})
.catch(error => {
console.error('Failed to delete license:', error);
showNotification('删除失败: ' + error.message, 'error');
});
}
// 格式化卡密显示为XXXX-XXXX-XXXX-XXXX格式
function formatLicenseKey(licenseKey) {
// 如果已经是XXXX-XXXX-XXXX-XXXX格式直接返回
if (licenseKey && licenseKey.includes('-') && licenseKey.split('-').length === 4) {
const parts = licenseKey.split('-');
if (parts.every(part => part.length === 8)) {
return licenseKey;
}
}
// 否则格式化为XXXX-XXXX-XXXX-XXXX格式
let cleanKey = licenseKey ? licenseKey.replace(/-/g, '').toUpperCase() : '';
// 如果长度不足32位右补0
if (cleanKey.length < 32) {
cleanKey = cleanKey.padEnd(32, '0');
}
// 如果长度超过32位截取前32位
else if (cleanKey.length > 32) {
cleanKey = cleanKey.substring(0, 32);
}
// 格式化为XXXX-XXXX-XXXX-XXXX格式
if (cleanKey.length >= 32) {
return cleanKey.substring(0, 8) + '-' +
cleanKey.substring(8, 16) + '-' +
cleanKey.substring(16, 24) + '-' +
cleanKey.substring(24, 32);
}
// 如果长度不足,按原样返回
return licenseKey || '';
}
</script>
{% endblock %}