Преглед на файлове

Feature #TASK_QT-34190 init

Carl преди 1 ден
родител
ревизия
8ed771f24b
променени са 5 файла, в които са добавени 515 реда и са изтрити 14 реда
  1. 1 0
      database/init.sql
  2. 90 3
      handlers/license.go
  3. 1 0
      main.go
  4. 1 0
      models/license.go
  5. 422 11
      web/index.html

+ 1 - 0
database/init.sql

@@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS `licenses` (
   `device_activations` text COMMENT '设备首次激活时间(JSON对象)',
   `device_heartbeats` text COMMENT '设备心跳时间(JSON对象,最后验证时间)',
   `max_devices` int NOT NULL DEFAULT '2' COMMENT '最大设备数',
+  `remark` text COMMENT '备注信息',
   `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
   `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
   PRIMARY KEY (`id`),

+ 90 - 3
handlers/license.go

@@ -19,13 +19,15 @@ type CreateLicenseRequest struct {
 	Key          string `json:"key" binding:"required"`  // 激活码(必填)
 	MaxDevices   int    `json:"max_devices"`             // 最大设备数(可选,默认2)
 	BoundDevices string `json:"bound_devices,omitempty"` // 初始绑定设备列表(可选,默认为空数组)
+	Remark       string `json:"remark,omitempty"`        // 备注信息(可选)
 }
 
 // UpdateLicenseRequest 更新 License 请求结构
 type UpdateLicenseRequest struct {
-	Key          string `json:"key,omitempty"`           // 激活码(可选)
-	MaxDevices   *int   `json:"max_devices,omitempty"`   // 最大设备数(可选,使用指针以区分零值)
-	BoundDevices string `json:"bound_devices,omitempty"` // 绑定设备列表(可选)
+	Key          string  `json:"key,omitempty"`           // 激活码(可选)
+	MaxDevices   *int    `json:"max_devices,omitempty"`   // 最大设备数(可选,使用指针以区分零值)
+	BoundDevices string  `json:"bound_devices,omitempty"` // 绑定设备列表(可选)
+	Remark       *string `json:"remark,omitempty"`        // 备注信息(可选,使用指针以区分空字符串和未提供)
 }
 
 // LicenseResponse License 响应结构
@@ -88,6 +90,7 @@ func CreateLicense(db *gorm.DB) gin.HandlerFunc {
 			LicenseKey:   req.Key,
 			MaxDevices:   req.MaxDevices,
 			BoundDevices: req.BoundDevices,
+			Remark:       req.Remark,
 		}
 
 		if err := db.Create(&license).Error; err != nil {
@@ -318,6 +321,11 @@ func UpdateLicense(db *gorm.DB) gin.HandlerFunc {
 			license.BoundDevices = req.BoundDevices
 		}
 
+		// 更新备注(如果提供了)
+		if req.Remark != nil {
+			license.Remark = *req.Remark
+		}
+
 		// 保存更新
 		if err := db.Save(&license).Error; err != nil {
 			c.JSON(http.StatusInternalServerError, LicenseResponse{
@@ -804,3 +812,82 @@ func UnbindDevice(db *gorm.DB) gin.HandlerFunc {
 		})
 	}
 }
+
+// StatisticsResponse 统计信息响应结构
+type StatisticsResponse struct {
+	Code int                    `json:"code"` // 状态码:0 表示成功,非0 表示失败
+	Msg  string                 `json:"msg"`  // 响应消息
+	Data *StatisticsData        `json:"data"` // 统计数据
+}
+
+// StatisticsData 统计数据
+type StatisticsData struct {
+	Total       int64 `json:"total"`        // 总License数
+	Activated   int64 `json:"activated"`    // 已激活数(有绑定设备)
+	Unactivated int64 `json:"unactivated"`  // 未激活数(无绑定设备)
+	TotalDevices int64 `json:"total_devices"` // 总绑定设备数
+}
+
+// GetStatistics 获取统计信息
+// GET /api/licenses/statistics
+func GetStatistics(db *gorm.DB) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		var stats StatisticsData
+
+		// 获取总数
+		if err := db.Model(&models.License{}).Count(&stats.Total).Error; err != nil {
+			c.JSON(http.StatusInternalServerError, StatisticsResponse{
+				Code: 500,
+				Msg:  "查询总数失败: " + err.Error(),
+			})
+			return
+		}
+
+		// 获取已激活数(有绑定设备)
+		if err := db.Model(&models.License{}).
+			Where("JSON_LENGTH(COALESCE(bound_devices, '[]')) > 0").
+			Count(&stats.Activated).Error; err != nil {
+			c.JSON(http.StatusInternalServerError, StatisticsResponse{
+				Code: 500,
+				Msg:  "查询已激活数失败: " + err.Error(),
+			})
+			return
+		}
+
+		// 获取未激活数(无绑定设备)
+		if err := db.Model(&models.License{}).
+			Where("JSON_LENGTH(COALESCE(bound_devices, '[]')) = 0 OR bound_devices IS NULL OR bound_devices = ''").
+			Count(&stats.Unactivated).Error; err != nil {
+			c.JSON(http.StatusInternalServerError, StatisticsResponse{
+				Code: 500,
+				Msg:  "查询未激活数失败: " + err.Error(),
+			})
+			return
+		}
+
+		// 计算总绑定设备数(需要遍历所有License的bound_devices)
+		var licenses []models.License
+		if err := db.Find(&licenses).Error; err != nil {
+			c.JSON(http.StatusInternalServerError, StatisticsResponse{
+				Code: 500,
+				Msg:  "查询License列表失败: " + err.Error(),
+			})
+			return
+		}
+
+		var totalDevices int64
+		for _, license := range licenses {
+			devices, err := license.GetBoundDeviceList()
+			if err == nil {
+				totalDevices += int64(len(devices))
+			}
+		}
+		stats.TotalDevices = totalDevices
+
+		c.JSON(http.StatusOK, StatisticsResponse{
+			Code: 0,
+			Msg:  "success",
+			Data: &stats,
+		})
+	}
+}

+ 1 - 0
main.go

@@ -91,6 +91,7 @@ func main() {
 			// License 管理接口(CRUD)
 			licenses := api.Group("/licenses")
 			{
+				licenses.GET("/statistics", handlers.GetStatistics(database.DB))                // 获取统计信息
 				licenses.POST("", handlers.CreateLicense(database.DB))                          // 创建 License
 				licenses.POST("/batch", handlers.BatchCreateLicense(database.DB))               // 批量创建 License
 				licenses.DELETE("/batch", handlers.BatchDeleteLicense(database.DB))             // 批量删除 License

+ 1 - 0
models/license.go

@@ -13,6 +13,7 @@ type License struct {
 	DeviceActivations string  `gorm:"type:text" json:"device_activations"`  // JSON 对象字符串,记录首次激活时间,例如 '{"uuid-1": "2024-01-01T00:00:00Z", "uuid-2": "2024-01-02T00:00:00Z"}'
 	DeviceHeartbeats  string  `gorm:"type:text" json:"device_heartbeats"`   // JSON 对象字符串,记录心跳时间(最后验证时间),例如 '{"uuid-1": "2024-01-01T12:00:00Z", "uuid-2": "2024-01-02T12:00:00Z"}'
 	MaxDevices      int       `gorm:"default:2" json:"max_devices"`
+	Remark          string    `gorm:"type:text" json:"remark"`               // 备注信息
 	CreatedAt       time.Time `json:"created_at"`
 	UpdatedAt       time.Time `json:"updated_at"`
 }

+ 422 - 11
web/index.html

@@ -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()">&times;</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();
             }