Kamixitong/app/web/templates/license/list.html
2025-11-22 16:48:45 +08:00

864 lines
32 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-key me-2"></i>
生成卡密
</a>
<a href="{{ url_for('web.import_license') }}" class="btn btn-outline-warning">
<i class="fas fa-file-import me-2"></i>
导入卡密
</a>
<a href="{{ url_for('web.export_license') }}" class="btn btn-outline-success">
<i class="fas fa-file-export 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">
<label for="search-keyword" class="form-label">关键词搜索</label>
<input type="text" class="form-control" id="search-keyword" placeholder="卡密或产品名称">
</div>
<div class="col-md-2">
<label for="search-status" class="form-label">状态</label>
<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">
<label for="search-type" class="form-label">类型</label>
<select class="form-select" id="search-type">
<option value="">全部类型</option>
<option value="1">正式卡密</option>
<option value="2">试用卡密</option>
</select>
</div>
<div class="col-md-3">
<label for="search-product" class="form-label">产品ID</label>
<input type="text" class="form-control" id="search-product" placeholder="产品ID">
</div>
<div class="col-md-2 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>
<button type="button" class="btn btn-outline-warning" id="batch-expire-btn">
<i class="fas fa-clock 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>卡密</th>
<th>产品</th>
<th>类型</th>
<th>状态</th>
<th>绑定信息</th>
<th>激活时间</th>
<th>过期时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="license-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="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;
// 页面加载完成后初始化
// 使用立即执行函数确保在DOM和所有脚本加载完成后执行
(function() {
function init() {
console.log('卡密列表页面已加载,开始初始化...');
console.log('apiRequest函数是否存在:', typeof apiRequest);
if (typeof apiRequest === 'function') {
loadLicenses();
initEventListeners();
} else {
console.error('apiRequest函数未定义等待脚本加载...');
setTimeout(function() {
if (typeof apiRequest === 'function') {
loadLicenses();
initEventListeners();
} else {
console.error('apiRequest函数仍未定义请检查脚本加载顺序');
}
}, 100);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// 初始化事件监听器
function initEventListeners() {
// 搜索表单
document.getElementById('search-form').addEventListener('submit', function(e) {
e.preventDefault();
currentPage = 1;
loadLicenses();
});
// 重置搜索
document.getElementById('reset-search').addEventListener('click', function() {
document.getElementById('search-keyword').value = '';
document.getElementById('search-status').value = '';
document.getElementById('search-type').value = '';
document.getElementById('search-product').value = '';
currentPage = 1;
loadLicenses();
});
// 批量删除确认
document.getElementById('confirm-batch-delete').addEventListener('click', function() {
batchDeleteLicenses();
});
// 全选/取消全选
document.getElementById('select-all-checkbox').addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.license-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateBatchButtons();
});
// 批量启用
document.getElementById('batch-enable-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateLicenseStatus(1);
});
// 批量禁用
document.getElementById('batch-disable-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateLicenseStatus(3);
});
// 批量设为过期
document.getElementById('batch-expire-btn').addEventListener('click', function(e) {
e.preventDefault();
batchUpdateLicenseStatus(2);
});
}
// 加载卡密列表
function loadLicenses(page = 1) {
console.log('loadLicenses函数被调用页码:', page);
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_id', product);
const apiUrl = `/api/v1/licenses?${params}`;
console.log('请求API URL:', apiUrl);
console.log('准备请求API:', apiUrl);
apiRequest(apiUrl)
.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="9" class="text-center text-muted">暂无数据</td></tr>';
return;
}
tbody.innerHTML = licenses.map(license => `
<tr>
<td>
<input type="checkbox" class="license-checkbox" data-license-key="${license.license_key}">
</td>
<td>
<code class="license-key-copy" data-license-key="${license.license_key}" style="cursor: pointer;" title="点击复制">${formatLicenseKey(license.license_key)}</code>
<br><small class="text-muted license-full-key-copy" data-license-key="${license.license_key}" style="cursor: pointer;" title="点击复制完整卡密">${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);
});
});
// 绑定复选框事件
document.querySelectorAll('.license-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', updateBatchButtons);
});
// 添加复制功能事件监听
document.querySelectorAll('.license-key-copy, .license-full-key-copy').forEach(element => {
element.addEventListener('click', function() {
const licenseKey = this.dataset.licenseKey;
copyToClipboard(licenseKey);
});
});
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
updateBatchButtons();
}
// 获取卡密状态文本
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 + '天'} (${license.duration_type})</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 || '';
}
// 更新批量操作按钮状态
function updateBatchButtons() {
const selectedCount = document.querySelectorAll('.license-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('.license-checkbox:checked').length;
document.getElementById('batch-delete-count').textContent = selectedCount;
const modal = new bootstrap.Modal(document.getElementById('batchDeleteModal'));
modal.show();
}
// 批量删除卡密
function batchDeleteLicenses() {
const selectedCheckboxes = document.querySelectorAll('.license-checkbox:checked');
const licenseKeys = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.licenseKey);
apiRequest('/api/v1/licenses/batch', {
method: 'DELETE',
body: JSON.stringify({ license_keys: licenseKeys })
})
.then(data => {
if (data.success) {
showNotification(data.message || '批量删除成功', 'success');
loadLicenses(currentPage);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量删除失败', 'error');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
}
})
.catch(error => {
console.error('Failed to batch delete licenses:', error);
showNotification('批量删除失败', 'error');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('batchDeleteModal'));
modal.hide();
});
}
// 批量更新卡密状态
function batchUpdateLicenseStatus(status) {
const selectedCheckboxes = document.querySelectorAll('.license-checkbox:checked');
const licenseKeys = Array.from(selectedCheckboxes).map(checkbox => checkbox.dataset.licenseKey);
if (licenseKeys.length === 0) {
showNotification('请至少选择一个卡密', 'warning');
return;
}
apiRequest('/api/v1/licenses/batch/status', {
method: 'PUT',
body: JSON.stringify({
license_keys: licenseKeys,
status: status
})
})
.then(data => {
if (data.success) {
showNotification(data.message || '批量更新状态成功', 'success');
loadLicenses(currentPage);
// 重置全选复选框
document.getElementById('select-all-checkbox').checked = false;
} else {
showNotification(data.message || '批量更新状态失败', 'error');
}
})
.catch(error => {
console.error('Failed to batch update license status:', error);
showNotification('批量更新状态失败', 'error');
});
}
// 添加复制到剪贴板的函数
function copyToClipboard(text) {
// 尝试使用现代浏览器的 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => {
showNotification('卡密已复制到剪贴板', 'success');
}).catch(err => {
console.error('复制失败:', err);
showNotification('复制失败', 'error');
});
} else {
// 降级方案:使用传统的 execCommand 方法
const textArea = document.createElement("textarea");
textArea.value = text;
// 避免滚动到底部
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
showNotification('卡密已复制到剪贴板', 'success');
} else {
showNotification('复制失败', 'error');
}
} catch (err) {
console.error('复制失败:', err);
showNotification('复制失败', 'error');
}
document.body.removeChild(textArea);
}
}
// 在页面加载完成后为批量删除按钮绑定事件
document.addEventListener('DOMContentLoaded', function() {
const batchDeleteBtn = document.getElementById('batch-delete-btn');
if (batchDeleteBtn) {
batchDeleteBtn.addEventListener('click', showBatchDeleteModal);
}
});
</script>
{% endblock %}