|
|
@@ -0,0 +1,1228 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <title>License 管理平台</title>
|
|
|
+ <style>
|
|
|
+ * {
|
|
|
+ margin: 0;
|
|
|
+ padding: 0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ body {
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ min-height: 100vh;
|
|
|
+ padding: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .container {
|
|
|
+ max-width: 1200px;
|
|
|
+ margin: 0 auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header {
|
|
|
+ background: white;
|
|
|
+ padding: 30px;
|
|
|
+ border-radius: 10px;
|
|
|
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
|
+ margin-bottom: 20px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .header h1 {
|
|
|
+ color: #333;
|
|
|
+ font-size: 28px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn {
|
|
|
+ padding: 10px 20px;
|
|
|
+ border: none;
|
|
|
+ border-radius: 5px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: all 0.3s;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-primary {
|
|
|
+ background: #667eea;
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-primary:hover {
|
|
|
+ background: #5568d3;
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-danger {
|
|
|
+ background: #ef4444;
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-danger:hover {
|
|
|
+ background: #dc2626;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-secondary {
|
|
|
+ background: #6b7280;
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-secondary:hover {
|
|
|
+ background: #4b5563;
|
|
|
+ }
|
|
|
+
|
|
|
+ .card {
|
|
|
+ background: white;
|
|
|
+ border-radius: 10px;
|
|
|
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
|
+ padding: 30px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-container {
|
|
|
+ overflow-x: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+ }
|
|
|
+
|
|
|
+ th, td {
|
|
|
+ padding: 12px;
|
|
|
+ text-align: left;
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
+ }
|
|
|
+
|
|
|
+ th {
|
|
|
+ background: #f9fafb;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #374151;
|
|
|
+ }
|
|
|
+
|
|
|
+ tr:hover {
|
|
|
+ background: #f9fafb;
|
|
|
+ }
|
|
|
+
|
|
|
+ .badge {
|
|
|
+ display: inline-block;
|
|
|
+ padding: 4px 12px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .badge-success {
|
|
|
+ background: #d1fae5;
|
|
|
+ color: #065f46;
|
|
|
+ }
|
|
|
+
|
|
|
+ .badge-warning {
|
|
|
+ background: #fef3c7;
|
|
|
+ color: #92400e;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal {
|
|
|
+ display: none;
|
|
|
+ position: fixed;
|
|
|
+ z-index: 1000;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ animation: fadeIn 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal.show {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes fadeIn {
|
|
|
+ from { opacity: 0; }
|
|
|
+ to { opacity: 1; }
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-content {
|
|
|
+ background: white;
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 30px;
|
|
|
+ width: 90%;
|
|
|
+ max-width: 500px;
|
|
|
+ animation: slideDown 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes slideDown {
|
|
|
+ from {
|
|
|
+ transform: translateY(-50px);
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ transform: translateY(0);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-header h2 {
|
|
|
+ color: #333;
|
|
|
+ font-size: 24px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .close {
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #999;
|
|
|
+ cursor: pointer;
|
|
|
+ border: none;
|
|
|
+ background: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .close:hover {
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-group {
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-group label {
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ color: #374151;
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-group input {
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px;
|
|
|
+ border: 1px solid #d1d5db;
|
|
|
+ border-radius: 5px;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-group input:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #667eea;
|
|
|
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-top: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ margin-top: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination button {
|
|
|
+ padding: 8px 16px;
|
|
|
+ border: 1px solid #d1d5db;
|
|
|
+ background: white;
|
|
|
+ border-radius: 5px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination button:hover:not(:disabled) {
|
|
|
+ background: #f9fafb;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination button:disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+ cursor: not-allowed;
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination .page-info {
|
|
|
+ color: #6b7280;
|
|
|
+ }
|
|
|
+
|
|
|
+ .loading {
|
|
|
+ text-align: center;
|
|
|
+ padding: 40px;
|
|
|
+ color: #6b7280;
|
|
|
+ }
|
|
|
+
|
|
|
+ .empty-state {
|
|
|
+ text-align: center;
|
|
|
+ padding: 60px 20px;
|
|
|
+ color: #6b7280;
|
|
|
+ }
|
|
|
+
|
|
|
+ .empty-state svg {
|
|
|
+ width: 100px;
|
|
|
+ height: 100px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ opacity: 0.5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .device-list {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7280;
|
|
|
+ max-width: 200px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-sm {
|
|
|
+ padding: 6px 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .license-key-cell {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ max-width: 200px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .license-key-text {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .copy-btn {
|
|
|
+ padding: 6px 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ background: #667eea;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 5px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s;
|
|
|
+ flex-shrink: 0;
|
|
|
+ font-weight: 500;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .copy-btn:hover {
|
|
|
+ background: #5568d3;
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
|
|
|
+ }
|
|
|
+
|
|
|
+ .copy-btn:active {
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Toast 通知样式 */
|
|
|
+ .toast-container {
|
|
|
+ position: fixed;
|
|
|
+ top: 20px;
|
|
|
+ right: 20px;
|
|
|
+ z-index: 10000;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast {
|
|
|
+ background: white;
|
|
|
+ padding: 16px 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ min-width: 300px;
|
|
|
+ max-width: 400px;
|
|
|
+ animation: slideInRight 0.3s ease-out;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes slideInRight {
|
|
|
+ from {
|
|
|
+ transform: translateX(100%);
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ transform: translateX(0);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast.success {
|
|
|
+ border-left: 4px solid #10b981;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast.error {
|
|
|
+ border-left: 4px solid #ef4444;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast.warning {
|
|
|
+ border-left: 4px solid #f59e0b;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast.info {
|
|
|
+ border-left: 4px solid #3b82f6;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast-icon {
|
|
|
+ font-size: 20px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast.success .toast-icon {
|
|
|
+ color: #10b981;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast.error .toast-icon {
|
|
|
+ color: #ef4444;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast.warning .toast-icon {
|
|
|
+ color: #f59e0b;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast.info .toast-icon {
|
|
|
+ color: #3b82f6;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast-message {
|
|
|
+ flex: 1;
|
|
|
+ color: #374151;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast-close {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ font-size: 18px;
|
|
|
+ color: #9ca3af;
|
|
|
+ cursor: pointer;
|
|
|
+ padding: 0;
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toast-close:hover {
|
|
|
+ color: #374151;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 确认对话框样式 */
|
|
|
+ .confirm-dialog {
|
|
|
+ display: none;
|
|
|
+ position: fixed;
|
|
|
+ z-index: 10001;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ animation: fadeIn 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .confirm-dialog.show {
|
|
|
+ display: flex;
|
|
|
+ }
|
|
|
+
|
|
|
+ .confirm-content {
|
|
|
+ background: white;
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 30px;
|
|
|
+ width: 90%;
|
|
|
+ max-width: 400px;
|
|
|
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
|
|
+ animation: slideDown 0.3s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .confirm-icon {
|
|
|
+ font-size: 48px;
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .confirm-title {
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #374151;
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .confirm-message {
|
|
|
+ color: #6b7280;
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 25px;
|
|
|
+ line-height: 1.6;
|
|
|
+ }
|
|
|
+
|
|
|
+ .confirm-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .confirm-actions .btn {
|
|
|
+ min-width: 100px;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <!-- Toast 通知容器 -->
|
|
|
+ <div class="toast-container" id="toast-container"></div>
|
|
|
+
|
|
|
+ <!-- 确认对话框 -->
|
|
|
+ <div id="confirmDialog" class="confirm-dialog">
|
|
|
+ <div class="confirm-content">
|
|
|
+ <div class="confirm-icon">⚠️</div>
|
|
|
+ <div class="confirm-title" id="confirm-title">确认操作</div>
|
|
|
+ <div class="confirm-message" id="confirm-message"></div>
|
|
|
+ <div class="confirm-actions">
|
|
|
+ <button class="btn btn-secondary" onclick="closeConfirmDialog(false)">取消</button>
|
|
|
+ <button class="btn btn-danger" onclick="closeConfirmDialog(true)" id="confirm-ok-btn">确定</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="container">
|
|
|
+ <div class="header">
|
|
|
+ <h1>🔑 License 管理平台</h1>
|
|
|
+ <div style="display: flex; gap: 10px;">
|
|
|
+ <button class="btn btn-primary" onclick="openBatchModal()">📦 批量生成</button>
|
|
|
+ <button class="btn btn-primary" onclick="openCreateModal()">+ 创建 License</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card">
|
|
|
+ <div id="loading" class="loading" style="display: none;">加载中...</div>
|
|
|
+ <div id="empty-state" class="empty-state" style="display: none;">
|
|
|
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
|
+ </svg>
|
|
|
+ <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>
|
|
|
+ <button class="btn btn-danger" id="batch-delete-btn" onclick="batchDeleteLicenses()" style="display: none;">
|
|
|
+ 批量删除
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <table>
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th style="width: 50px;">
|
|
|
+ <input type="checkbox" id="select-all-header" onchange="toggleSelectAll()" style="width: 18px; height: 18px; cursor: pointer;">
|
|
|
+ </th>
|
|
|
+ <th>ID</th>
|
|
|
+ <th>激活码</th>
|
|
|
+ <th>最大设备数</th>
|
|
|
+ <th>已绑定设备</th>
|
|
|
+ <th>设备激活时间</th>
|
|
|
+ <th>绑定设备数</th>
|
|
|
+ <th>创建时间</th>
|
|
|
+ <th>操作</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody id="license-table-body">
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ <div class="pagination" id="pagination"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 创建/编辑 Modal -->
|
|
|
+ <div id="licenseModal" class="modal">
|
|
|
+ <div class="modal-content">
|
|
|
+ <div class="modal-header">
|
|
|
+ <h2 id="modal-title">创建 License</h2>
|
|
|
+ <button class="close" onclick="closeModal()">×</button>
|
|
|
+ </div>
|
|
|
+ <form id="licenseForm" onsubmit="handleSubmit(event)">
|
|
|
+ <input type="hidden" id="license-id">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="license-key">激活码 *</label>
|
|
|
+ <input type="text" id="license-key" required placeholder="例如: VIP-8888">
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="license-max-devices">最大设备数 *</label>
|
|
|
+ <input type="number" id="license-max-devices" required min="1" value="2">
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="license-bound-devices">已绑定设备 (JSON 数组,可选)</label>
|
|
|
+ <input type="text" id="license-bound-devices" placeholder='例如: ["device-1", "device-2"]'>
|
|
|
+ </div>
|
|
|
+ <div class="form-actions">
|
|
|
+ <button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
|
|
|
+ <button type="submit" class="btn btn-primary">保存</button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 批量生成 Modal -->
|
|
|
+ <div id="batchModal" class="modal">
|
|
|
+ <div class="modal-content">
|
|
|
+ <div class="modal-header">
|
|
|
+ <h2>批量生成 License</h2>
|
|
|
+ <button class="close" onclick="closeBatchModal()">×</button>
|
|
|
+ </div>
|
|
|
+ <form id="batchForm" onsubmit="handleBatchSubmit(event)">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="batch-prefix">激活码前缀 *</label>
|
|
|
+ <input type="text" id="batch-prefix" required placeholder="例如: VIP" value="VIP">
|
|
|
+ <small style="color: #6b7280; font-size: 12px; margin-top: 4px; display: block;">
|
|
|
+ 生成的激活码格式:前缀-随机32位字符串(如:VIP-A3B9C2D4E5F6G7H8I9J0K1L2M3N4O5P6)
|
|
|
+ </small>
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="batch-count">生成数量 *</label>
|
|
|
+ <input type="number" id="batch-count" required min="1" max="1000" value="10">
|
|
|
+ <small style="color: #6b7280; font-size: 12px; margin-top: 4px; display: block;">
|
|
|
+ 一次最多可生成 1000 个
|
|
|
+ </small>
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="batch-max-devices">最大设备数 *</label>
|
|
|
+ <input type="number" id="batch-max-devices" required min="1" value="2">
|
|
|
+ </div>
|
|
|
+ <div class="form-actions">
|
|
|
+ <button type="button" class="btn btn-secondary" onclick="closeBatchModal()">取消</button>
|
|
|
+ <button type="submit" class="btn btn-primary">生成</button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ // 使用相对路径,自动适配当前域名
|
|
|
+ const API_BASE = '/api';
|
|
|
+ let currentPage = 1;
|
|
|
+ let pageSize = 10;
|
|
|
+ let total = 0;
|
|
|
+ let editingId = null;
|
|
|
+
|
|
|
+ // Toast 通知函数
|
|
|
+ function showToast(message, type = 'info', duration = 3000) {
|
|
|
+ const container = document.getElementById('toast-container');
|
|
|
+ const toast = document.createElement('div');
|
|
|
+ toast.className = `toast ${type}`;
|
|
|
+
|
|
|
+ const icons = {
|
|
|
+ success: '✅',
|
|
|
+ error: '❌',
|
|
|
+ warning: '⚠️',
|
|
|
+ info: 'ℹ️'
|
|
|
+ };
|
|
|
+
|
|
|
+ toast.innerHTML = `
|
|
|
+ <span class="toast-icon">${icons[type] || icons.info}</span>
|
|
|
+ <span class="toast-message">${message}</span>
|
|
|
+ <button class="toast-close" onclick="this.parentElement.remove()">×</button>
|
|
|
+ `;
|
|
|
+
|
|
|
+ container.appendChild(toast);
|
|
|
+
|
|
|
+ // 自动移除
|
|
|
+ setTimeout(() => {
|
|
|
+ if (toast.parentElement) {
|
|
|
+ toast.style.animation = 'slideInRight 0.3s ease-out reverse';
|
|
|
+ setTimeout(() => {
|
|
|
+ if (toast.parentElement) {
|
|
|
+ toast.remove();
|
|
|
+ }
|
|
|
+ }, 300);
|
|
|
+ }
|
|
|
+ }, duration);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确认对话框函数
|
|
|
+ let confirmCallback = null;
|
|
|
+
|
|
|
+ function showConfirmDialog(message, title = '确认操作', okText = '确定', okType = 'danger') {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ document.getElementById('confirm-title').textContent = title;
|
|
|
+ document.getElementById('confirm-message').textContent = message;
|
|
|
+ const okBtn = document.getElementById('confirm-ok-btn');
|
|
|
+ okBtn.textContent = okText;
|
|
|
+ okBtn.className = `btn btn-${okType}`;
|
|
|
+
|
|
|
+ confirmCallback = resolve;
|
|
|
+ document.getElementById('confirmDialog').classList.add('show');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function closeConfirmDialog(confirmed) {
|
|
|
+ document.getElementById('confirmDialog').classList.remove('show');
|
|
|
+ if (confirmCallback) {
|
|
|
+ confirmCallback(confirmed);
|
|
|
+ confirmCallback = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 复制激活码到剪贴板
|
|
|
+ async function copyLicenseKey(key) {
|
|
|
+ try {
|
|
|
+ await navigator.clipboard.writeText(key);
|
|
|
+ showToast('激活码已复制到剪贴板', 'success', 2000);
|
|
|
+ } catch (err) {
|
|
|
+ // 降级方案:使用传统方法
|
|
|
+ const textArea = document.createElement('textarea');
|
|
|
+ textArea.value = key;
|
|
|
+ textArea.style.position = 'fixed';
|
|
|
+ textArea.style.left = '-999999px';
|
|
|
+ document.body.appendChild(textArea);
|
|
|
+ textArea.select();
|
|
|
+ try {
|
|
|
+ document.execCommand('copy');
|
|
|
+ showToast('激活码已复制到剪贴板', 'success', 2000);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('复制失败,请手动复制', 'error');
|
|
|
+ }
|
|
|
+ document.body.removeChild(textArea);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 全选/取消全选
|
|
|
+ function toggleSelectAll() {
|
|
|
+ const selectAll = document.getElementById('select-all');
|
|
|
+ const selectAllHeader = document.getElementById('select-all-header');
|
|
|
+ const checkboxes = document.querySelectorAll('.license-checkbox');
|
|
|
+ const isChecked = selectAll.checked || selectAllHeader.checked;
|
|
|
+
|
|
|
+ checkboxes.forEach(checkbox => {
|
|
|
+ checkbox.checked = isChecked;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 同步两个全选复选框
|
|
|
+ selectAll.checked = isChecked;
|
|
|
+ selectAllHeader.checked = isChecked;
|
|
|
+
|
|
|
+ updateSelectedCount();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新选中数量
|
|
|
+ function updateSelectedCount() {
|
|
|
+ const checkboxes = document.querySelectorAll('.license-checkbox:checked');
|
|
|
+ const count = checkboxes.length;
|
|
|
+ const selectedCountEl = document.getElementById('selected-count');
|
|
|
+ const batchDeleteBtn = document.getElementById('batch-delete-btn');
|
|
|
+
|
|
|
+ selectedCountEl.textContent = `已选择 ${count} 项`;
|
|
|
+
|
|
|
+ if (count > 0) {
|
|
|
+ batchDeleteBtn.style.display = 'block';
|
|
|
+ } else {
|
|
|
+ batchDeleteBtn.style.display = 'none';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新全选复选框状态
|
|
|
+ const allCheckboxes = document.querySelectorAll('.license-checkbox');
|
|
|
+ const allChecked = allCheckboxes.length > 0 && checkboxes.length === allCheckboxes.length;
|
|
|
+ document.getElementById('select-all').checked = allChecked;
|
|
|
+ document.getElementById('select-all-header').checked = allChecked;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 批量删除 License
|
|
|
+ async function batchDeleteLicenses() {
|
|
|
+ const checkboxes = document.querySelectorAll('.license-checkbox:checked');
|
|
|
+ const selectedIds = Array.from(checkboxes).map(cb => parseInt(cb.value));
|
|
|
+
|
|
|
+ if (selectedIds.length === 0) {
|
|
|
+ showToast('请至少选择一个 License', 'warning');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const confirmed = await showConfirmDialog(
|
|
|
+ `确定要删除选中的 ${selectedIds.length} 个 License 吗?此操作不可恢复!`,
|
|
|
+ '确认批量删除',
|
|
|
+ '删除',
|
|
|
+ 'danger'
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!confirmed) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await apiRequest(`${API_BASE}/licenses/batch`, {
|
|
|
+ method: 'DELETE',
|
|
|
+ body: JSON.stringify({
|
|
|
+ ids: selectedIds
|
|
|
+ })
|
|
|
+ });
|
|
|
+ if (!response) return;
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.code === 0) {
|
|
|
+ showToast(result.msg, 'success');
|
|
|
+ loadLicenses(currentPage);
|
|
|
+ } else {
|
|
|
+ showToast('批量删除失败: ' + result.msg, 'error');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ showToast('请求失败: ' + error.message, 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取认证token
|
|
|
+ function getAuthToken() {
|
|
|
+ return localStorage.getItem('auth_token');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否已登录
|
|
|
+ function checkAuth() {
|
|
|
+ const token = getAuthToken();
|
|
|
+ if (!token) {
|
|
|
+ window.location.href = '/web/login.html';
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取API请求头(包含token)
|
|
|
+ function getAuthHeaders() {
|
|
|
+ const token = getAuthToken();
|
|
|
+ return {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'Authorization': `Bearer ${token}`
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理API错误响应
|
|
|
+ async function handleApiError(response) {
|
|
|
+ if (response.status === 401) {
|
|
|
+ // 未授权,清除token并跳转到登录页
|
|
|
+ localStorage.removeItem('auth_token');
|
|
|
+ showToast('登录已过期,请重新登录', 'error');
|
|
|
+ setTimeout(() => {
|
|
|
+ window.location.href = '/web/login.html';
|
|
|
+ }, 1000);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 统一的API请求函数
|
|
|
+ async function apiRequest(url, options = {}) {
|
|
|
+ const headers = getAuthHeaders();
|
|
|
+ if (options.headers) {
|
|
|
+ Object.assign(headers, options.headers);
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await fetch(url, {
|
|
|
+ ...options,
|
|
|
+ headers: headers
|
|
|
+ });
|
|
|
+
|
|
|
+ if (await handleApiError(response)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 页面加载时检查登录状态
|
|
|
+ window.onload = () => {
|
|
|
+ if (checkAuth()) {
|
|
|
+ loadLicenses();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 加载 License 列表
|
|
|
+ async function loadLicenses(page = 1) {
|
|
|
+ currentPage = page;
|
|
|
+ const loadingEl = document.getElementById('loading');
|
|
|
+ const tableContainer = document.getElementById('table-container');
|
|
|
+ const emptyState = document.getElementById('empty-state');
|
|
|
+
|
|
|
+ loadingEl.style.display = 'block';
|
|
|
+ tableContainer.style.display = 'none';
|
|
|
+ emptyState.style.display = 'none';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await apiRequest(`${API_BASE}/licenses?page=${page}&page_size=${pageSize}`);
|
|
|
+ if (!response) return;
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.code === 0) {
|
|
|
+ total = result.total;
|
|
|
+ const licenses = result.data;
|
|
|
+
|
|
|
+ if (licenses.length === 0) {
|
|
|
+ loadingEl.style.display = 'none';
|
|
|
+ emptyState.style.display = 'block';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ renderTable(licenses);
|
|
|
+ renderPagination();
|
|
|
+ loadingEl.style.display = 'none';
|
|
|
+ tableContainer.style.display = 'block';
|
|
|
+ } else {
|
|
|
+ showToast('加载失败: ' + result.msg, 'error');
|
|
|
+ loadingEl.style.display = 'none';
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ showToast('请求失败: ' + error.message, 'error');
|
|
|
+ loadingEl.style.display = 'none';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染表格
|
|
|
+ function renderTable(licenses) {
|
|
|
+ const tbody = document.getElementById('license-table-body');
|
|
|
+ tbody.innerHTML = licenses.map(license => {
|
|
|
+ let boundDevices = [];
|
|
|
+ try {
|
|
|
+ boundDevices = JSON.parse(license.bound_devices || '[]');
|
|
|
+ } catch (e) {
|
|
|
+ boundDevices = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析设备激活时间
|
|
|
+ let deviceActivations = {};
|
|
|
+ try {
|
|
|
+ const activationsStr = JSON.parse(license.device_activations || '{}');
|
|
|
+ deviceActivations = activationsStr;
|
|
|
+ } catch (e) {
|
|
|
+ deviceActivations = {};
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建设备激活时间显示
|
|
|
+ let activationTimesHtml = '无';
|
|
|
+ if (boundDevices.length > 0) {
|
|
|
+ activationTimesHtml = boundDevices.map(deviceId => {
|
|
|
+ const activationTime = deviceActivations[deviceId];
|
|
|
+ if (activationTime) {
|
|
|
+ const date = new Date(activationTime);
|
|
|
+ return `${deviceId}<br><small style="color: #6b7280;">${date.toLocaleString('zh-CN')}</small>`;
|
|
|
+ }
|
|
|
+ return `${deviceId}<br><small style="color: #9ca3af;">未记录</small>`;
|
|
|
+ }).join('<br><br>');
|
|
|
+ }
|
|
|
+
|
|
|
+ const boundCount = boundDevices.length;
|
|
|
+ const isFull = boundCount >= license.max_devices;
|
|
|
+ const createdDate = new Date(license.created_at).toLocaleString('zh-CN');
|
|
|
+
|
|
|
+ // 限制激活码显示长度为10个字符
|
|
|
+ const displayKey = license.key.length > 10 ? license.key.substring(0, 10) + '...' : license.key;
|
|
|
+
|
|
|
+ return `
|
|
|
+ <tr>
|
|
|
+ <td style="text-align: center;">
|
|
|
+ <input type="checkbox" class="license-checkbox" value="${license.id}" onchange="updateSelectedCount()" style="width: 18px; height: 18px; cursor: pointer;">
|
|
|
+ </td>
|
|
|
+ <td>${license.id}</td>
|
|
|
+ <td>
|
|
|
+ <div class="license-key-cell" title="${license.key}">
|
|
|
+ <span class="license-key-text">${displayKey}</span>
|
|
|
+ <button class="copy-btn" onclick="copyLicenseKey('${license.key}')" title="复制激活码">
|
|
|
+ <span>复制</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ <td>${license.max_devices}</td>
|
|
|
+ <td class="device-list" title="${boundDevices.join(', ') || '无'}">
|
|
|
+ ${boundDevices.length > 0 ? boundDevices.join(', ') : '无'}
|
|
|
+ </td>
|
|
|
+ <td style="font-size: 12px; line-height: 1.6; max-width: 300px;">
|
|
|
+ ${activationTimesHtml}
|
|
|
+ </td>
|
|
|
+ <td>
|
|
|
+ <span class="badge ${isFull ? 'badge-warning' : 'badge-success'}">
|
|
|
+ ${boundCount} / ${license.max_devices}
|
|
|
+ </span>
|
|
|
+ </td>
|
|
|
+ <td>${createdDate}</td>
|
|
|
+ <td>
|
|
|
+ <div class="actions">
|
|
|
+ <button class="btn btn-primary btn-sm" onclick="editLicense(${license.id})">编辑</button>
|
|
|
+ <button class="btn btn-danger btn-sm" onclick="deleteLicense(${license.id}, '${license.key}')">删除</button>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染分页
|
|
|
+ function renderPagination() {
|
|
|
+ const pagination = document.getElementById('pagination');
|
|
|
+ const totalPages = Math.ceil(total / pageSize);
|
|
|
+
|
|
|
+ if (totalPages <= 1) {
|
|
|
+ pagination.innerHTML = '';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ pagination.innerHTML = `
|
|
|
+ <button onclick="loadLicenses(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>
|
|
|
+ 上一页
|
|
|
+ </button>
|
|
|
+ <span class="page-info">第 ${currentPage} / ${totalPages} 页 (共 ${total} 条)</span>
|
|
|
+ <button onclick="loadLicenses(${currentPage + 1})" ${currentPage >= totalPages ? 'disabled' : ''}>
|
|
|
+ 下一页
|
|
|
+ </button>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 打开创建 Modal
|
|
|
+ function openCreateModal() {
|
|
|
+ editingId = null;
|
|
|
+ document.getElementById('modal-title').textContent = '创建 License';
|
|
|
+ document.getElementById('license-id').value = '';
|
|
|
+ document.getElementById('license-key').value = '';
|
|
|
+ document.getElementById('license-max-devices').value = '2';
|
|
|
+ document.getElementById('license-bound-devices').value = '';
|
|
|
+ document.getElementById('license-key').disabled = false;
|
|
|
+ document.getElementById('licenseModal').classList.add('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 编辑 License
|
|
|
+ async function editLicense(id) {
|
|
|
+ try {
|
|
|
+ const response = await apiRequest(`${API_BASE}/licenses/${id}`);
|
|
|
+ if (!response) return;
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.code === 0) {
|
|
|
+ const license = result.data;
|
|
|
+ editingId = id;
|
|
|
+ document.getElementById('modal-title').textContent = '编辑 License';
|
|
|
+ document.getElementById('license-id').value = id;
|
|
|
+ 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-key').disabled = false;
|
|
|
+ document.getElementById('licenseModal').classList.add('show');
|
|
|
+ } else {
|
|
|
+ showToast('加载失败: ' + result.msg, 'error');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ showToast('请求失败: ' + error.message, 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭 Modal
|
|
|
+ function closeModal() {
|
|
|
+ document.getElementById('licenseModal').classList.remove('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提交表单
|
|
|
+ async function handleSubmit(event) {
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ const id = document.getElementById('license-id').value;
|
|
|
+ const key = document.getElementById('license-key').value;
|
|
|
+ const maxDevices = parseInt(document.getElementById('license-max-devices').value);
|
|
|
+ const boundDevices = document.getElementById('license-bound-devices').value || '[]';
|
|
|
+
|
|
|
+ // 验证 boundDevices 是否为有效 JSON
|
|
|
+ try {
|
|
|
+ JSON.parse(boundDevices);
|
|
|
+ } catch (e) {
|
|
|
+ showToast('已绑定设备必须是有效的 JSON 数组格式', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ let response;
|
|
|
+ if (editingId) {
|
|
|
+ // 更新
|
|
|
+ const updateData = {
|
|
|
+ max_devices: maxDevices
|
|
|
+ };
|
|
|
+ if (boundDevices) {
|
|
|
+ updateData.bound_devices = boundDevices;
|
|
|
+ }
|
|
|
+ response = await apiRequest(`${API_BASE}/licenses/${id}`, {
|
|
|
+ method: 'PUT',
|
|
|
+ body: JSON.stringify(updateData)
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 创建
|
|
|
+ response = await apiRequest(`${API_BASE}/licenses`, {
|
|
|
+ method: 'POST',
|
|
|
+ body: JSON.stringify({
|
|
|
+ key: key,
|
|
|
+ max_devices: maxDevices,
|
|
|
+ bound_devices: boundDevices
|
|
|
+ })
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!response) return;
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.code === 0) {
|
|
|
+ showToast(editingId ? '更新成功' : '创建成功', 'success');
|
|
|
+ closeModal();
|
|
|
+ loadLicenses(currentPage);
|
|
|
+ } else {
|
|
|
+ showToast('操作失败: ' + result.msg, 'error');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ showToast('请求失败: ' + error.message, 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 删除 License
|
|
|
+ async function deleteLicense(id, key) {
|
|
|
+ const confirmed = await showConfirmDialog(
|
|
|
+ `确定要删除 License "${key}" 吗?此操作不可恢复!`,
|
|
|
+ '确认删除',
|
|
|
+ '删除',
|
|
|
+ 'danger'
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!confirmed) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await apiRequest(`${API_BASE}/licenses/${id}`, {
|
|
|
+ method: 'DELETE'
|
|
|
+ });
|
|
|
+ if (!response) return;
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.code === 0) {
|
|
|
+ showToast('删除成功', 'success');
|
|
|
+ loadLicenses(currentPage);
|
|
|
+ } else {
|
|
|
+ showToast('删除失败: ' + result.msg, 'error');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ showToast('请求失败: ' + error.message, 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 打开批量生成 Modal
|
|
|
+ function openBatchModal() {
|
|
|
+ document.getElementById('batch-prefix').value = 'VIP';
|
|
|
+ document.getElementById('batch-count').value = '10';
|
|
|
+ document.getElementById('batch-max-devices').value = '2';
|
|
|
+ document.getElementById('batchModal').classList.add('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭批量生成 Modal
|
|
|
+ function closeBatchModal() {
|
|
|
+ document.getElementById('batchModal').classList.remove('show');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 批量生成提交
|
|
|
+ async function handleBatchSubmit(event) {
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ const prefix = document.getElementById('batch-prefix').value.trim();
|
|
|
+ const count = parseInt(document.getElementById('batch-count').value);
|
|
|
+ const maxDevices = parseInt(document.getElementById('batch-max-devices').value);
|
|
|
+
|
|
|
+ if (!prefix) {
|
|
|
+ showToast('请输入激活码前缀', 'warning');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (count <= 0 || count > 1000) {
|
|
|
+ showToast('生成数量必须在 1-1000 之间', 'warning');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const confirmed = await showConfirmDialog(
|
|
|
+ `确定要批量生成 ${count} 个激活码吗?`,
|
|
|
+ '确认批量生成',
|
|
|
+ '生成',
|
|
|
+ 'primary'
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!confirmed) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await apiRequest(`${API_BASE}/licenses/batch`, {
|
|
|
+ method: 'POST',
|
|
|
+ body: JSON.stringify({
|
|
|
+ prefix: prefix,
|
|
|
+ count: count,
|
|
|
+ max_devices: maxDevices
|
|
|
+ })
|
|
|
+ });
|
|
|
+ if (!response) return;
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.code === 0) {
|
|
|
+ showToast(result.msg, 'success', 4000);
|
|
|
+ closeBatchModal();
|
|
|
+ loadLicenses(1); // 重新加载第一页
|
|
|
+ } 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 confirmDialog = document.getElementById('confirmDialog');
|
|
|
+
|
|
|
+ if (event.target === licenseModal) {
|
|
|
+ closeModal();
|
|
|
+ }
|
|
|
+ if (event.target === batchModal) {
|
|
|
+ closeBatchModal();
|
|
|
+ }
|
|
|
+ if (event.target === confirmDialog) {
|
|
|
+ closeConfirmDialog(false);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|
|
|
+
|