From ecfc1050d3d02ab7db08f651b57fffe857447f81 Mon Sep 17 00:00:00 2001 From: shaw Date: Sun, 3 Aug 2025 01:09:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E7=AB=AF=E5=93=8D=E5=BA=94=E5=BC=8F=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化所有页面的移动端适配(手机、平板、PC) - 修复AccountsView移动端状态显示和按钮功能问题 - 修复ApiKeysView移动端详情展开显示问题 - 移除ApiKeysView不必要的查看按钮 - 修复Dashboard页面PC版时间筛选按钮布局 - 改进所有组件的响应式设计 - 删除dist目录避免构建文件冲突 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/middleware/auth.js | 10 +- src/routes/admin.js | 125 +++- .../src/assets/styles/components.css | 108 ++++ .../src/components/accounts/AccountForm.vue | 32 +- .../components/apikeys/BatchApiKeyModal.vue | 324 ++++++++++ .../components/apikeys/CreateApiKeyModal.vue | 258 +++++++- .../components/apikeys/EditApiKeyModal.vue | 123 +++- .../components/apikeys/ExpiryEditModal.vue | 402 +++++++++++++ .../src/components/apistats/LimitConfig.vue | 80 +-- .../components/apistats/ModelUsageStats.vue | 58 +- .../src/components/apistats/StatsOverview.vue | 113 ++-- .../components/apistats/TokenDistribution.vue | 43 +- .../src/components/common/StatCard.vue | 10 +- .../src/components/layout/AppHeader.vue | 20 +- .../src/components/layout/MainLayout.vue | 6 +- .../src/components/layout/TabBar.vue | 56 +- web/admin-spa/src/views/AccountsView.vue | 285 ++++++++- web/admin-spa/src/views/ApiKeysView.vue | 491 +++++++++++++-- web/admin-spa/src/views/ApiStatsView.vue | 61 +- web/admin-spa/src/views/DashboardView.vue | 125 ++-- web/admin-spa/src/views/LoginView.vue | 30 +- web/admin-spa/src/views/SettingsView.vue | 152 ++++- web/admin-spa/src/views/TutorialView.vue | 560 +++++++++--------- 23 files changed, 2775 insertions(+), 697 deletions(-) create mode 100644 web/admin-spa/src/components/apikeys/BatchApiKeyModal.vue create mode 100644 web/admin-spa/src/components/apikeys/ExpiryEditModal.vue diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 3d08e8c5..47bd4333 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -56,21 +56,25 @@ const authenticateApiKey = async (req, res, next) => { let clientAllowed = false; let matchedClient = null; + // 获取预定义客户端列表,如果配置不存在则使用默认值 + const predefinedClients = config.clientRestrictions?.predefinedClients || []; + const allowCustomClients = config.clientRestrictions?.allowCustomClients || false; + // 遍历允许的客户端列表 for (const allowedClientId of validation.keyData.allowedClients) { // 在预定义客户端列表中查找 - const predefinedClient = config.clientRestrictions.predefinedClients.find( + const predefinedClient = predefinedClients.find( client => client.id === allowedClientId ); if (predefinedClient) { // 使用预定义的正则表达式匹配 User-Agent - if (predefinedClient.userAgentPattern.test(userAgent)) { + if (predefinedClient.userAgentPattern && predefinedClient.userAgentPattern.test(userAgent)) { clientAllowed = true; matchedClient = predefinedClient.name; break; } - } else if (config.clientRestrictions.allowCustomClients) { + } else if (allowCustomClients) { // 如果允许自定义客户端,这里可以添加自定义客户端的验证逻辑 // 目前暂时跳过自定义客户端 continue; diff --git a/src/routes/admin.js b/src/routes/admin.js index 07412291..bf7e566c 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -291,11 +291,26 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { // 获取支持的客户端列表 router.get('/supported-clients', authenticateAdmin, async (req, res) => { try { - const clients = config.clientRestrictions.predefinedClients.map(client => ({ + // 检查配置是否存在,如果不存在则使用默认值 + const predefinedClients = config.clientRestrictions?.predefinedClients || [ + { + id: 'claude_code', + name: 'ClaudeCode', + description: 'Official Claude Code CLI' + }, + { + id: 'gemini_cli', + name: 'Gemini-CLI', + description: 'Gemini Command Line Interface' + } + ]; + + const clients = predefinedClients.map(client => ({ id: client.id, name: client.name, description: client.description })); + res.json({ success: true, data: clients }); } catch (error) { logger.error('❌ Failed to get supported clients:', error); @@ -439,6 +454,114 @@ router.post('/api-keys', authenticateAdmin, async (req, res) => { } }); +// 批量创建API Keys +router.post('/api-keys/batch', authenticateAdmin, async (req, res) => { + try { + const { + baseName, + count, + description, + tokenLimit, + expiresAt, + claudeAccountId, + claudeConsoleAccountId, + geminiAccountId, + permissions, + concurrencyLimit, + rateLimitWindow, + rateLimitRequests, + enableModelRestriction, + restrictedModels, + enableClientRestriction, + allowedClients, + dailyCostLimit, + tags + } = req.body; + + // 输入验证 + if (!baseName || typeof baseName !== 'string' || baseName.trim().length === 0) { + return res.status(400).json({ error: 'Base name is required and must be a non-empty string' }); + } + + if (!count || !Number.isInteger(count) || count < 2 || count > 500) { + return res.status(400).json({ error: 'Count must be an integer between 2 and 500' }); + } + + if (baseName.length > 90) { + return res.status(400).json({ error: 'Base name must be less than 90 characters to allow for numbering' }); + } + + // 生成批量API Keys + const createdKeys = []; + const errors = []; + + for (let i = 1; i <= count; i++) { + try { + const name = `${baseName}_${i}`; + const newKey = await apiKeyService.generateApiKey({ + name, + description, + tokenLimit, + expiresAt, + claudeAccountId, + claudeConsoleAccountId, + geminiAccountId, + permissions, + concurrencyLimit, + rateLimitWindow, + rateLimitRequests, + enableModelRestriction, + restrictedModels, + enableClientRestriction, + allowedClients, + dailyCostLimit, + tags + }); + + // 保留原始 API Key 供返回 + createdKeys.push({ + ...newKey, + apiKey: newKey.apiKey + }); + } catch (error) { + errors.push({ + index: i, + name: `${baseName}_${i}`, + error: error.message + }); + } + } + + // 如果有部分失败,返回部分成功的结果 + if (errors.length > 0 && createdKeys.length === 0) { + return res.status(400).json({ + success: false, + error: 'Failed to create any API keys', + errors + }); + } + + // 返回创建的keys(包含完整的apiKey) + res.json({ + success: true, + data: createdKeys, + errors: errors.length > 0 ? errors : undefined, + summary: { + requested: count, + created: createdKeys.length, + failed: errors.length + } + }); + } catch (error) { + logger.error('Failed to batch create API keys:', error); + res.status(500).json({ + success: false, + error: 'Failed to batch create API keys', + message: error.message + }); + } +}); + // 更新API Key router.put('/api-keys/:keyId', authenticateAdmin, async (req, res) => { try { diff --git a/web/admin-spa/src/assets/styles/components.css b/web/admin-spa/src/assets/styles/components.css index a2c83973..7f5bd632 100644 --- a/web/admin-spa/src/assets/styles/components.css +++ b/web/admin-spa/src/assets/styles/components.css @@ -297,6 +297,114 @@ transition: transform 0.3s ease-in-out; } +/* 响应式设计 - 移动端优化 */ +@media (max-width: 640px) { + /* 玻璃态容器 */ + .glass, + .glass-strong { + margin: 12px; + border-radius: 16px; + padding: 16px; + } + + /* 统计卡片 */ + .stat-card { + padding: 12px; + border-radius: 12px; + } + + .stat-icon { + width: 40px; + height: 40px; + font-size: 16px; + } + + /* 标签按钮 */ + .tab-btn { + font-size: 12px; + padding: 10px 6px; + } + + /* 模态框 */ + .modal-content { + margin: 8px; + max-width: calc(100vw - 24px); + padding: 16px; + } + + .modal-scroll-content { + max-height: calc(90vh - 100px); + } + + /* 卡片 */ + .card { + border-radius: 12px; + } + + /* 表单元素 */ + .form-input, + .form-select, + .form-textarea { + font-size: 14px; + padding: 8px 12px; + } + + /* 按钮 */ + .btn { + font-size: 14px; + padding: 8px 16px; + } + + /* 表格 */ + .table-container table { + font-size: 12px; + } + + .table-container th, + .table-container td { + padding: 8px 12px; + } + + /* Toast通知 */ + .toast { + min-width: 280px; + max-width: calc(100vw - 40px); + right: 12px; + top: 60px; + } + + /* 加载动画 */ + .loading-spinner { + width: 16px; + height: 16px; + } +} + +@media (max-width: 768px) { + /* 玻璃态容器 */ + .glass, + .glass-strong { + margin: 16px; + border-radius: 20px; + } + + /* 统计卡片 */ + .stat-card { + padding: 16px; + } + + /* 标签按钮 */ + .tab-btn { + font-size: 14px; + padding: 12px 8px; + } + + /* 模态框滚动内容 */ + .modal-scroll-content { + max-height: calc(85vh - 120px); + } +} + .toast.show { transform: translateX(0); } diff --git a/web/admin-spa/src/components/accounts/AccountForm.vue b/web/admin-spa/src/components/accounts/AccountForm.vue index 57123955..cf696d59 100644 --- a/web/admin-spa/src/components/accounts/AccountForm.vue +++ b/web/admin-spa/src/components/accounts/AccountForm.vue @@ -2,50 +2,50 @@