|
|
@@ -555,6 +555,89 @@
|
|
|
.confirm-actions .btn {
|
|
|
min-width: 100px;
|
|
|
}
|
|
|
+
|
|
|
+ /* 统计信息卡片样式 */
|
|
|
+ .stats-container {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
+ gap: 20px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card {
|
|
|
+ background: white;
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-label {
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 14px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-value {
|
|
|
+ color: #111827;
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card.primary .stat-value {
|
|
|
+ color: #667eea;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card.success .stat-value {
|
|
|
+ color: #10b981;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card.warning .stat-value {
|
|
|
+ color: #f59e0b;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card.danger .stat-value {
|
|
|
+ color: #ef4444;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 筛选和操作栏样式 */
|
|
|
+ .filter-bar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-group {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-group label {
|
|
|
+ color: #374151;
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-select {
|
|
|
+ padding: 8px 12px;
|
|
|
+ border: 1px solid #d1d5db;
|
|
|
+ border-radius: 5px;
|
|
|
+ font-size: 14px;
|
|
|
+ background: white;
|
|
|
+ cursor: pointer;
|
|
|
+ min-width: 120px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .filter-select:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #667eea;
|
|
|
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
|
+ }
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
@@ -579,10 +662,31 @@
|
|
|
<h1>🔑 License 管理平台</h1>
|
|
|
<div style="display: flex; gap: 10px;">
|
|
|
<button class="btn btn-primary" onclick="openBatchModal()">📦 批量生成</button>
|
|
|
+ <button class="btn btn-primary" onclick="exportToCSV()">📥 导出 CSV</button>
|
|
|
<button class="btn btn-primary" onclick="openCreateModal()">+ 创建 License</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 统计信息卡片 -->
|
|
|
+ <div class="stats-container" id="stats-container" style="display: none;">
|
|
|
+ <div class="stat-card primary">
|
|
|
+ <div class="stat-label">总 License 数</div>
|
|
|
+ <div class="stat-value" id="stat-total">0</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card success">
|
|
|
+ <div class="stat-label">已激活</div>
|
|
|
+ <div class="stat-value" id="stat-activated">0</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card warning">
|
|
|
+ <div class="stat-label">未激活</div>
|
|
|
+ <div class="stat-value" id="stat-unactivated">0</div>
|
|
|
+ </div>
|
|
|
+ <div class="stat-card danger">
|
|
|
+ <div class="stat-label">总设备数</div>
|
|
|
+ <div class="stat-value" id="stat-devices">0</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<div class="card">
|
|
|
<div id="loading" class="loading" style="display: none;">加载中...</div>
|
|
|
<div id="empty-state" class="empty-state" style="display: none;">
|
|
|
@@ -592,15 +696,29 @@
|
|
|
<p>暂无 License 数据</p>
|
|
|
</div>
|
|
|
<div class="table-container" id="table-container" style="display: none;">
|
|
|
- <div style="margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;">
|
|
|
- <div style="display: flex; align-items: center; gap: 10px;">
|
|
|
- <input type="checkbox" id="select-all" onchange="toggleSelectAll()" style="width: 18px; height: 18px; cursor: pointer;">
|
|
|
- <label for="select-all" style="cursor: pointer; user-select: none;">全选</label>
|
|
|
- <span id="selected-count" style="color: #6b7280; font-size: 14px;">已选择 0 项</span>
|
|
|
+ <!-- 筛选和操作栏 -->
|
|
|
+ <div class="filter-bar">
|
|
|
+ <div class="filter-group">
|
|
|
+ <label for="status-filter">状态筛选:</label>
|
|
|
+ <select id="status-filter" class="filter-select" onchange="handleStatusFilter()">
|
|
|
+ <option value="">全部</option>
|
|
|
+ <option value="activated">已激活</option>
|
|
|
+ <option value="unactivated">未激活</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div style="display: flex; gap: 10px;">
|
|
|
+ <button class="btn btn-primary" id="batch-update-btn" onclick="openBatchUpdateModal()" style="display: none;">
|
|
|
+ ⚙️ 批量修改最大设备数
|
|
|
+ </button>
|
|
|
+ <button class="btn btn-danger" id="batch-delete-btn" onclick="batchDeleteLicenses()" style="display: none;">
|
|
|
+ 批量删除
|
|
|
+ </button>
|
|
|
</div>
|
|
|
- <button class="btn btn-danger" id="batch-delete-btn" onclick="batchDeleteLicenses()" style="display: none;">
|
|
|
- 批量删除
|
|
|
- </button>
|
|
|
+ </div>
|
|
|
+ <div style="margin-bottom: 15px; display: flex; align-items: center; gap: 10px;">
|
|
|
+ <input type="checkbox" id="select-all" onchange="toggleSelectAll()" style="width: 18px; height: 18px; cursor: pointer;">
|
|
|
+ <label for="select-all" style="cursor: pointer; user-select: none;">全选</label>
|
|
|
+ <span id="selected-count" style="color: #6b7280; font-size: 14px;">已选择 0 项</span>
|
|
|
</div>
|
|
|
<table>
|
|
|
<thead>
|
|
|
@@ -612,6 +730,7 @@
|
|
|
<th>激活码</th>
|
|
|
<th>设备详情</th>
|
|
|
<th>绑定设备数</th>
|
|
|
+ <th>备注</th>
|
|
|
<th>创建时间</th>
|
|
|
<th>操作</th>
|
|
|
</tr>
|
|
|
@@ -645,6 +764,10 @@
|
|
|
<label for="license-bound-devices">已绑定设备 (JSON 数组,可选)</label>
|
|
|
<input type="text" id="license-bound-devices" placeholder='例如: ["device-1", "device-2"]'>
|
|
|
</div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="license-remark">备注 (可选)</label>
|
|
|
+ <textarea id="license-remark" rows="3" placeholder="请输入备注信息" style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 5px; font-size: 14px; font-family: inherit; resize: vertical;"></textarea>
|
|
|
+ </div>
|
|
|
<div class="form-actions">
|
|
|
<button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
|
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|
|
@@ -700,6 +823,32 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 批量修改最大设备数 Modal -->
|
|
|
+ <div id="batchUpdateModal" class="modal">
|
|
|
+ <div class="modal-content">
|
|
|
+ <div class="modal-header">
|
|
|
+ <h2>批量修改最大设备数</h2>
|
|
|
+ <button class="close" onclick="closeBatchUpdateModal()">×</button>
|
|
|
+ </div>
|
|
|
+ <form id="batchUpdateForm" onsubmit="handleBatchUpdateSubmit(event)">
|
|
|
+ <div class="form-group">
|
|
|
+ <label>已选择 <span id="batch-update-count">0</span> 个 License</label>
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="batch-update-max-devices">新的最大设备数 *</label>
|
|
|
+ <input type="number" id="batch-update-max-devices" required min="1" value="2">
|
|
|
+ <small style="color: #6b7280; font-size: 12px; margin-top: 4px; display: block;">
|
|
|
+ 将把选中 License 的最大设备数统一修改为此值
|
|
|
+ </small>
|
|
|
+ </div>
|
|
|
+ <div class="form-actions">
|
|
|
+ <button type="button" class="btn btn-secondary" onclick="closeBatchUpdateModal()">取消</button>
|
|
|
+ <button type="submit" class="btn btn-primary">确认修改</button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<script>
|
|
|
// 使用相对路径,自动适配当前域名
|
|
|
const API_BASE = '/api';
|
|
|
@@ -707,6 +856,8 @@
|
|
|
let pageSize = 10;
|
|
|
let total = 0;
|
|
|
let editingId = null;
|
|
|
+ let currentStatusFilter = ''; // 当前状态筛选:''(全部)、'activated'(已激活)、'unactivated'(未激活)
|
|
|
+ let allLicenses = []; // 存储所有License数据用于统计
|
|
|
|
|
|
// Toast 通知函数
|
|
|
function showToast(message, type = 'info', duration = 3000) {
|
|
|
@@ -828,6 +979,9 @@
|
|
|
batchDeleteBtn.style.display = 'none';
|
|
|
}
|
|
|
|
|
|
+ // 更新批量操作按钮
|
|
|
+ updateBatchButtons();
|
|
|
+
|
|
|
// 更新全选复选框状态
|
|
|
const allCheckboxes = document.querySelectorAll('.license-checkbox');
|
|
|
const allChecked = allCheckboxes.length > 0 && checkboxes.length === allCheckboxes.length;
|
|
|
@@ -869,6 +1023,7 @@
|
|
|
|
|
|
if (result.code === 0) {
|
|
|
showToast(result.msg, 'success');
|
|
|
+ loadStatistics(); // 重新加载统计信息
|
|
|
loadLicenses(currentPage);
|
|
|
} else {
|
|
|
showToast('批量删除失败: ' + result.msg, 'error');
|
|
|
@@ -938,10 +1093,31 @@
|
|
|
// 页面加载时检查登录状态
|
|
|
window.onload = () => {
|
|
|
if (checkAuth()) {
|
|
|
+ loadStatistics();
|
|
|
loadLicenses();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ // 加载统计信息
|
|
|
+ async function loadStatistics() {
|
|
|
+ try {
|
|
|
+ const response = await apiRequest(`${API_BASE}/licenses/statistics`);
|
|
|
+ if (!response) return;
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+ if (result.code === 0 && result.data) {
|
|
|
+ const stats = result.data;
|
|
|
+ document.getElementById('stat-total').textContent = stats.total || 0;
|
|
|
+ document.getElementById('stat-activated').textContent = stats.activated || 0;
|
|
|
+ document.getElementById('stat-unactivated').textContent = stats.unactivated || 0;
|
|
|
+ document.getElementById('stat-devices').textContent = stats.total_devices || 0;
|
|
|
+ document.getElementById('stats-container').style.display = 'grid';
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('加载统计信息失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 加载 License 列表
|
|
|
async function loadLicenses(page = 1) {
|
|
|
currentPage = page;
|
|
|
@@ -954,7 +1130,13 @@
|
|
|
emptyState.style.display = 'none';
|
|
|
|
|
|
try {
|
|
|
- const response = await apiRequest(`${API_BASE}/licenses?page=${page}&page_size=${pageSize}`);
|
|
|
+ // 构建查询URL
|
|
|
+ let url = `${API_BASE}/licenses?page=${page}&page_size=${pageSize}`;
|
|
|
+ if (currentStatusFilter) {
|
|
|
+ url += `&status=${currentStatusFilter}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await apiRequest(url);
|
|
|
if (!response) return;
|
|
|
|
|
|
const result = await response.json();
|
|
|
@@ -973,6 +1155,9 @@
|
|
|
renderPagination();
|
|
|
loadingEl.style.display = 'none';
|
|
|
tableContainer.style.display = 'block';
|
|
|
+
|
|
|
+ // 更新批量操作按钮显示
|
|
|
+ updateBatchButtons();
|
|
|
} else {
|
|
|
showToast('加载失败: ' + result.msg, 'error');
|
|
|
loadingEl.style.display = 'none';
|
|
|
@@ -983,6 +1168,24 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 状态筛选处理
|
|
|
+ function handleStatusFilter() {
|
|
|
+ const filterSelect = document.getElementById('status-filter');
|
|
|
+ currentStatusFilter = filterSelect.value;
|
|
|
+ loadLicenses(1); // 重置到第一页
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新批量操作按钮显示
|
|
|
+ function updateBatchButtons() {
|
|
|
+ const checkboxes = document.querySelectorAll('.license-checkbox:checked');
|
|
|
+ const batchUpdateBtn = document.getElementById('batch-update-btn');
|
|
|
+ if (checkboxes.length > 0) {
|
|
|
+ batchUpdateBtn.style.display = 'block';
|
|
|
+ } else {
|
|
|
+ batchUpdateBtn.style.display = 'none';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 渲染表格
|
|
|
function renderTable(licenses) {
|
|
|
const tbody = document.getElementById('license-table-body');
|
|
|
@@ -1059,6 +1262,11 @@
|
|
|
${boundCount} / ${license.max_devices}
|
|
|
</span>
|
|
|
</td>
|
|
|
+ <td>
|
|
|
+ <div style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${license.remark || ''}">
|
|
|
+ ${license.remark || '<span style="color: #9ca3af;">无</span>'}
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
<td>${createdDate}</td>
|
|
|
<td>
|
|
|
<div class="actions">
|
|
|
@@ -1100,6 +1308,7 @@
|
|
|
document.getElementById('license-key').value = '';
|
|
|
document.getElementById('license-max-devices').value = '2';
|
|
|
document.getElementById('license-bound-devices').value = '';
|
|
|
+ document.getElementById('license-remark').value = '';
|
|
|
document.getElementById('license-key').disabled = false;
|
|
|
document.getElementById('licenseModal').classList.add('show');
|
|
|
}
|
|
|
@@ -1120,6 +1329,7 @@
|
|
|
document.getElementById('license-key').value = license.key;
|
|
|
document.getElementById('license-max-devices').value = license.max_devices;
|
|
|
document.getElementById('license-bound-devices').value = license.bound_devices || '[]';
|
|
|
+ document.getElementById('license-remark').value = license.remark || '';
|
|
|
document.getElementById('license-key').disabled = false;
|
|
|
document.getElementById('licenseModal').classList.add('show');
|
|
|
} else {
|
|
|
@@ -1143,6 +1353,7 @@
|
|
|
const key = document.getElementById('license-key').value;
|
|
|
const maxDevices = parseInt(document.getElementById('license-max-devices').value);
|
|
|
const boundDevices = document.getElementById('license-bound-devices').value || '[]';
|
|
|
+ const remark = document.getElementById('license-remark').value || '';
|
|
|
|
|
|
// 验证 boundDevices 是否为有效 JSON
|
|
|
try {
|
|
|
@@ -1157,7 +1368,8 @@
|
|
|
if (editingId) {
|
|
|
// 更新
|
|
|
const updateData = {
|
|
|
- max_devices: maxDevices
|
|
|
+ max_devices: maxDevices,
|
|
|
+ remark: remark // 总是发送remark字段,即使是空字符串
|
|
|
};
|
|
|
if (boundDevices) {
|
|
|
updateData.bound_devices = boundDevices;
|
|
|
@@ -1173,7 +1385,8 @@
|
|
|
body: JSON.stringify({
|
|
|
key: key,
|
|
|
max_devices: maxDevices,
|
|
|
- bound_devices: boundDevices
|
|
|
+ bound_devices: boundDevices,
|
|
|
+ remark: remark
|
|
|
})
|
|
|
});
|
|
|
}
|
|
|
@@ -1185,6 +1398,7 @@
|
|
|
if (result.code === 0) {
|
|
|
showToast(editingId ? '更新成功' : '创建成功', 'success');
|
|
|
closeModal();
|
|
|
+ loadStatistics(); // 重新加载统计信息
|
|
|
loadLicenses(currentPage);
|
|
|
} else {
|
|
|
showToast('操作失败: ' + result.msg, 'error');
|
|
|
@@ -1217,6 +1431,7 @@
|
|
|
|
|
|
if (result.code === 0) {
|
|
|
showToast('删除成功', 'success');
|
|
|
+ loadStatistics(); // 重新加载统计信息
|
|
|
loadLicenses(currentPage);
|
|
|
} else {
|
|
|
showToast('删除失败: ' + result.msg, 'error');
|
|
|
@@ -1284,6 +1499,7 @@
|
|
|
if (result.code === 0) {
|
|
|
showToast(result.msg, 'success', 4000);
|
|
|
closeBatchModal();
|
|
|
+ loadStatistics(); // 重新加载统计信息
|
|
|
loadLicenses(1); // 重新加载第一页
|
|
|
} else {
|
|
|
showToast('批量生成失败: ' + result.msg, 'error');
|
|
|
@@ -1433,10 +1649,202 @@
|
|
|
document.getElementById('deviceListModal').classList.remove('show');
|
|
|
}
|
|
|
|
|
|
+ // 导出CSV
|
|
|
+ async function exportToCSV() {
|
|
|
+ try {
|
|
|
+ // 如果有筛选条件,先获取筛选后的数据,然后前端生成CSV
|
|
|
+ if (currentStatusFilter) {
|
|
|
+ showToast('正在导出筛选后的数据...', 'info');
|
|
|
+
|
|
|
+ const listResponse = await apiRequest(`${API_BASE}/licenses?page=1&page_size=10000&status=${currentStatusFilter}`);
|
|
|
+ if (!listResponse) return;
|
|
|
+
|
|
|
+ const listResult = await listResponse.json();
|
|
|
+ if (listResult.code === 0 && listResult.data.length > 0) {
|
|
|
+ const licenses = listResult.data;
|
|
|
+ const headers = ['ID', '激活码', '最大设备数', '已绑定设备数', '绑定设备列表', '创建时间', '更新时间'];
|
|
|
+ const rows = licenses.map(license => {
|
|
|
+ let boundDevices = [];
|
|
|
+ try {
|
|
|
+ boundDevices = JSON.parse(license.bound_devices || '[]');
|
|
|
+ } catch (e) {
|
|
|
+ boundDevices = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ license.id,
|
|
|
+ license.key,
|
|
|
+ license.max_devices,
|
|
|
+ boundDevices.length,
|
|
|
+ boundDevices.join('; '),
|
|
|
+ new Date(license.created_at).toLocaleString('zh-CN'),
|
|
|
+ new Date(license.updated_at).toLocaleString('zh-CN')
|
|
|
+ ];
|
|
|
+ });
|
|
|
+
|
|
|
+ function escapeCSVField(field) {
|
|
|
+ if (field === null || field === undefined) return '';
|
|
|
+ const str = String(field);
|
|
|
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
|
+ return '"' + str.replace(/"/g, '""') + '"';
|
|
|
+ }
|
|
|
+ return str;
|
|
|
+ }
|
|
|
+
|
|
|
+ const csvContent = [
|
|
|
+ headers.map(escapeCSVField).join(','),
|
|
|
+ ...rows.map(row => row.map(escapeCSVField).join(','))
|
|
|
+ ].join('\n');
|
|
|
+
|
|
|
+ const BOM = '\uFEFF';
|
|
|
+ const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
|
+ const link = document.createElement('a');
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ link.setAttribute('href', url);
|
|
|
+
|
|
|
+ const now = new Date();
|
|
|
+ const dateStr = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
|
+ const timeStr = now.toTimeString().slice(0, 8).replace(/:/g, '');
|
|
|
+ const statusStr = currentStatusFilter ? `_${currentStatusFilter}` : '';
|
|
|
+ link.setAttribute('download', `licenses${statusStr}_${dateStr}_${timeStr}.csv`);
|
|
|
+
|
|
|
+ link.style.visibility = 'hidden';
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+
|
|
|
+ showToast(`成功导出 ${licenses.length} 条记录`, 'success');
|
|
|
+ } else {
|
|
|
+ showToast('没有数据可导出', 'warning');
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 没有筛选条件时,使用后端导出接口
|
|
|
+ const token = getAuthToken();
|
|
|
+ const url = `${API_BASE}/licenses/export?format=csv`;
|
|
|
+
|
|
|
+ // 创建一个隐藏的表单来提交带token的请求
|
|
|
+ const form = document.createElement('form');
|
|
|
+ form.method = 'GET';
|
|
|
+ form.action = url;
|
|
|
+ form.style.display = 'none';
|
|
|
+
|
|
|
+ // 添加token到请求头(通过fetch下载)
|
|
|
+ const response = await fetch(url, {
|
|
|
+ headers: getAuthHeaders()
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ showToast('导出失败', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const blob = await response.blob();
|
|
|
+ const downloadUrl = URL.createObjectURL(blob);
|
|
|
+ const link = document.createElement('a');
|
|
|
+ link.href = downloadUrl;
|
|
|
+
|
|
|
+ const now = new Date();
|
|
|
+ const dateStr = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
|
+ const timeStr = now.toTimeString().slice(0, 8).replace(/:/g, '');
|
|
|
+ link.setAttribute('download', `licenses_${dateStr}_${timeStr}.csv`);
|
|
|
+
|
|
|
+ link.style.visibility = 'hidden';
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+ URL.revokeObjectURL(downloadUrl);
|
|
|
+
|
|
|
+ showToast('导出成功', 'success');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ showToast('导出失败: ' + error.message, 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 打开批量修改最大设备数弹框
|
|
|
+ function openBatchUpdateModal() {
|
|
|
+ const checkboxes = document.querySelectorAll('.license-checkbox:checked');
|
|
|
+ const count = checkboxes.length;
|
|
|
+
|
|
|
+ if (count === 0) {
|
|
|
+ showToast('请至少选择一个 License', 'warning');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ document.getElementById('batch-update-count').textContent = count;
|
|
|
+ document.getElementById('batch-update-max-devices').value = '2';
|
|
|
+ document.getElementById('batchUpdateModal').classList.add('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭批量修改弹框
|
|
|
+ function closeBatchUpdateModal() {
|
|
|
+ document.getElementById('batchUpdateModal').classList.remove('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 批量修改最大设备数提交
|
|
|
+ async function handleBatchUpdateSubmit(event) {
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ const checkboxes = document.querySelectorAll('.license-checkbox:checked');
|
|
|
+ const selectedIds = Array.from(checkboxes).map(cb => parseInt(cb.value));
|
|
|
+ const maxDevices = parseInt(document.getElementById('batch-update-max-devices').value);
|
|
|
+
|
|
|
+ if (selectedIds.length === 0) {
|
|
|
+ showToast('请至少选择一个 License', 'warning');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (maxDevices < 1) {
|
|
|
+ showToast('最大设备数必须大于0', 'warning');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const confirmed = await showConfirmDialog(
|
|
|
+ `确定要将选中的 ${selectedIds.length} 个 License 的最大设备数修改为 ${maxDevices} 吗?`,
|
|
|
+ '确认批量修改',
|
|
|
+ '确认',
|
|
|
+ 'primary'
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!confirmed) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 使用批量更新接口
|
|
|
+ const response = await apiRequest(`${API_BASE}/licenses/batch/max-devices`, {
|
|
|
+ method: 'PUT',
|
|
|
+ body: JSON.stringify({
|
|
|
+ ids: selectedIds,
|
|
|
+ max_devices: maxDevices
|
|
|
+ })
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response) return;
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.code === 0) {
|
|
|
+ showToast(result.msg, 'success');
|
|
|
+ closeBatchUpdateModal();
|
|
|
+ // 清除所有选中状态
|
|
|
+ checkboxes.forEach(cb => cb.checked = false);
|
|
|
+ updateSelectedCount();
|
|
|
+ loadStatistics(); // 重新加载统计信息
|
|
|
+ loadLicenses(currentPage); // 重新加载列表
|
|
|
+ } else {
|
|
|
+ showToast('批量修改失败: ' + result.msg, 'error');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ showToast('请求失败: ' + error.message, 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 点击 Modal 外部关闭
|
|
|
window.onclick = function(event) {
|
|
|
const licenseModal = document.getElementById('licenseModal');
|
|
|
const batchModal = document.getElementById('batchModal');
|
|
|
+ const batchUpdateModal = document.getElementById('batchUpdateModal');
|
|
|
const deviceListModal = document.getElementById('deviceListModal');
|
|
|
const confirmDialog = document.getElementById('confirmDialog');
|
|
|
|
|
|
@@ -1446,6 +1854,9 @@
|
|
|
if (event.target === batchModal) {
|
|
|
closeBatchModal();
|
|
|
}
|
|
|
+ if (event.target === batchUpdateModal) {
|
|
|
+ closeBatchUpdateModal();
|
|
|
+ }
|
|
|
if (event.target === deviceListModal) {
|
|
|
closeDeviceListModal();
|
|
|
}
|