From 3b7870aa9ff62bc8f38615f6615db77f86a7409c Mon Sep 17 00:00:00 2001 From: murphyyi Date: Sun, 28 Sep 2025 09:21:02 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(admin):=20=E4=BC=98=E5=8C=96=20swagger?= =?UTF-8?q?=20=E9=A2=84=E8=A7=88=20iframe=20=E9=AB=98=E5=BA=A6=E8=87=AA?= =?UTF-8?q?=E9=80=82=E5=BA=94=E4=B8=8E=E5=93=8D=E5=BA=94=E5=BC=8F=E4=BD=93?= =?UTF-8?q?=E9=AA=8C\n\n-=20CSS:=20=E6=98=8E=E7=A1=AE=E8=AE=BE=E7=BD=AE=20?= =?UTF-8?q?.swagger-embed-frame=20=E5=8F=8A=20iframe=20=E9=AB=98=E5=BA=A6?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=BC=BA=E5=93=8D=E5=BA=94=E5=BC=8F=E6=96=AD?= =?UTF-8?q?=E7=82=B9\n-=20JS:=20=E6=96=B0=E5=A2=9E=20adjustSwaggerIframeHe?= =?UTF-8?q?ight=20=E5=8A=A8=E6=80=81=E9=AB=98=E5=BA=A6=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=EF=BC=8C=E7=AA=97=E5=8F=A3=E5=B0=BA=E5=AF=B8=E5=8F=98=E5=8C=96?= =?UTF-8?q?=E6=97=B6=E8=87=AA=E5=8A=A8=E8=B0=83=E6=95=B4\n-=20=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E7=BA=B5=E5=90=91=E9=AB=98=E5=BA=A6=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=EF=BC=8C=E6=8F=90=E5=8D=87=E5=A4=9A=E7=AB=AF?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- themes/2025/admin/css/admin-modern.css | 143 ++++++++++++++ themes/2025/admin/js/main.js | 253 +++++++++++++++++++++++++ 2 files changed, 396 insertions(+) diff --git a/themes/2025/admin/css/admin-modern.css b/themes/2025/admin/css/admin-modern.css index 9ded24b..6d717c5 100644 --- a/themes/2025/admin/css/admin-modern.css +++ b/themes/2025/admin/css/admin-modern.css @@ -1972,10 +1972,153 @@ body.admin-modern-body { grid-column: span 2; } +.swagger-embed { + background: var(--admin-surface); + border: 1px solid var(--admin-border); + border-radius: 20px; + padding: 26px; + display: flex; + flex-direction: column; + gap: 22px; + box-shadow: var(--admin-shadow-card); +} + +.swagger-embed-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; + flex-wrap: wrap; +} + +.swagger-embed-title h3 { + margin: 0; + font-size: 20px; + font-weight: 600; + color: var(--admin-heading); + display: flex; + align-items: center; + gap: 10px; +} + +.swagger-embed-title p { + margin: 8px 0 0; + color: var(--admin-subtle); +} + +.swagger-embed-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.swagger-embed-frame { + position: relative; + border: 1px solid rgba(148, 163, 184, 0.22); + border-radius: 18px; + overflow: hidden; + height: 800px; + min-height: 620px; + background: var(--admin-placeholder-bg); +} + +.swagger-embed-frame iframe { + width: 100%; + height: 800px; + border: none; + background: #fff; +} + +.swagger-embed-placeholder { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + padding: 32px; + text-align: center; + background: linear-gradient(145deg, rgba(129, 140, 248, 0.16), rgba(59, 130, 246, 0.16)); + color: var(--admin-muted); + transition: opacity 0.3s ease, visibility 0.3s ease; +} + +.swagger-embed-placeholder .placeholder-icon { + font-size: 42px; + color: var(--admin-accent); +} + +.swagger-embed-placeholder.is-hidden { + opacity: 0; + visibility: hidden; + pointer-events: none; +} + +.swagger-embed-placeholder.has-error { + background: linear-gradient(145deg, rgba(248, 113, 113, 0.18), rgba(244, 63, 94, 0.18)); + color: var(--admin-danger); +} + +.swagger-embed-placeholder.has-error .placeholder-icon { + color: var(--admin-danger-strong, var(--admin-danger)); +} + +.swagger-embed-placeholder.has-error h4 { + color: var(--admin-danger); +} + +.swagger-embed-footer { + font-size: 14px; + color: var(--admin-muted); + background: var(--admin-surface-subtle); + border: 1px dashed rgba(99, 102, 241, 0.25); + border-radius: 16px; + padding: 16px 18px; + line-height: 1.6; + display: flex; + align-items: flex-start; + gap: 12px; +} + +.swagger-embed-footer i { + color: var(--admin-accent); + margin-top: 4px; +} + @media (max-width: 768px) { .admin-modern-body .form-grid .span-2 { grid-column: span 1; } + .swagger-embed-actions { + width: 100%; + justify-content: flex-start; + } + .swagger-embed-frame { + height: 600px; + min-height: 480px; + } + .swagger-embed-frame iframe { + height: 600px; + } +} + +@media (min-width: 769px) and (max-width: 1200px) { + .swagger-embed-frame { + height: 700px; + } + .swagger-embed-frame iframe { + height: 700px; + } +} + +@media (min-width: 1201px) { + .swagger-embed-frame { + height: 850px; + } + .swagger-embed-frame iframe { + height: 850px; + } } .admin-modern-body .storage-actions { diff --git a/themes/2025/admin/js/main.js b/themes/2025/admin/js/main.js index d773214..6be2eb1 100644 --- a/themes/2025/admin/js/main.js +++ b/themes/2025/admin/js/main.js @@ -134,6 +134,12 @@ let authToken = localStorage.getItem('user_token'); // 使用统一的user_token let currentStorageType = 'local'; let storageData = {}; +const SWAGGER_MAX_MONITOR_ATTEMPTS = 12; +const swaggerMonitorState = { + timer: null, + attempts: 0, +}; + const ADMIN_THEME_KEY = 'filecodebox_admin_theme'; const THEMES = { LIGHT: 'light', @@ -726,6 +732,9 @@ function loadTabData(tabName) { initTransferLogsTab(); } break; + case 'swagger': + initializeSwaggerEmbed(); + break; case 'mcp': // 由 mcp-simple.js 处理 if (typeof loadMCPConfig === 'function') { @@ -749,6 +758,237 @@ function loadTabData(tabName) { } } +function ensureSwaggerEmbedIframe() { + const iframe = document.getElementById('swagger-preview'); + if (!iframe) { + return null; + } + + if (!iframe.dataset.bound) { + iframe.addEventListener('load', () => handleSwaggerIframeLoaded(iframe)); + iframe.dataset.bound = '1'; + } + + return iframe; +} + +function setSwaggerIframeSource(iframe, forceReload = false) { + if (!iframe) { + return; + } + + clearSwaggerMonitorTimer(); + setSwaggerPlaceholderState('loading', null, iframe); + + const baseUrl = iframe.dataset.src || '/swagger/index.html'; + const nextUrl = forceReload + ? `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}ts=${Date.now()}` + : baseUrl; + + iframe.src = nextUrl; + + // 动态调整 iframe 高度 + adjustSwaggerIframeHeight(iframe); +} + +function adjustSwaggerIframeHeight(iframe) { + if (!iframe) return; + + const container = iframe.closest('.swagger-embed-frame'); + if (!container) return; + + // 根据屏幕尺寸动态设置高度 + const screenHeight = window.innerHeight; + const adminHeaderHeight = 80; // 管理后台顶部导航高度 + const adminTabsHeight = 60; // 标签页高度 + const adminPadding = 120; // 额外的内边距和间距 + + let optimalHeight; + + if (screenHeight <= 768) { + // 移动端 + optimalHeight = Math.max(480, screenHeight - adminHeaderHeight - adminTabsHeight - adminPadding); + } else if (screenHeight <= 1080) { + // 中等屏幕 + optimalHeight = Math.max(600, screenHeight - adminHeaderHeight - adminTabsHeight - adminPadding); + } else { + // 大屏幕 + optimalHeight = Math.max(700, Math.min(850, screenHeight - adminHeaderHeight - adminTabsHeight - adminPadding)); + } + + // 设置容器和 iframe 的高度 + container.style.height = `${optimalHeight}px`; + iframe.style.height = `${optimalHeight}px`; +} + +function initializeSwaggerEmbed() { + const iframe = ensureSwaggerEmbedIframe(); + if (!iframe) { + return; + } + + const currentSrc = iframe.getAttribute('src'); + const placeholder = document.getElementById('swagger-preview-placeholder'); + const hasError = placeholder ? placeholder.classList.contains('has-error') : false; + if (iframe.dataset.loaded === '1' && currentSrc && currentSrc !== 'about:blank' && !hasError) { + return; + } + + setSwaggerIframeSource(iframe, false); +} + +function reloadSwaggerPreview() { + const iframe = ensureSwaggerEmbedIframe(); + if (!iframe) { + return; + } + setSwaggerIframeSource(iframe, true); +} + +function openSwaggerInNewWindow() { + window.open('/swagger/index.html', '_blank', 'noopener,noreferrer'); +} + +function clearSwaggerMonitorTimer() { + if (swaggerMonitorState.timer) { + clearTimeout(swaggerMonitorState.timer); + swaggerMonitorState.timer = null; + } +} + +function scheduleSwaggerContentCheck(iframe, delay = 600) { + clearSwaggerMonitorTimer(); + swaggerMonitorState.timer = setTimeout(() => checkSwaggerIframeContent(iframe), delay); +} + +function handleSwaggerIframeLoaded(iframe) { + if (!iframe) { + return; + } + swaggerMonitorState.attempts = 0; + scheduleSwaggerContentCheck(iframe, 450); +} + +function checkSwaggerIframeContent(iframe) { + if (!iframe) { + return; + } + + let doc = null; + try { + doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document) || null; + } catch (error) { + doc = null; + } + + if (!doc) { + if (swaggerMonitorState.attempts >= SWAGGER_MAX_MONITOR_ATTEMPTS) { + setSwaggerPlaceholderState('error', '无法加载 Swagger UI 文档,请刷新或在新窗口中打开。', iframe); + clearSwaggerMonitorTimer(); + return; + } + swaggerMonitorState.attempts += 1; + scheduleSwaggerContentCheck(iframe); + return; + } + + const swaggerRoot = doc.querySelector('.swagger-ui'); + if (!swaggerRoot) { + if (swaggerMonitorState.attempts >= SWAGGER_MAX_MONITOR_ATTEMPTS) { + setSwaggerPlaceholderState('error', 'Swagger UI 未能正确渲染,请刷新或在新窗口中打开。', iframe); + clearSwaggerMonitorTimer(); + return; + } + swaggerMonitorState.attempts += 1; + scheduleSwaggerContentCheck(iframe); + return; + } + + const errorNode = swaggerRoot.querySelector('.errors-wrapper'); + if (errorNode && errorNode.textContent.trim()) { + setSwaggerPlaceholderState('error', errorNode.textContent.trim(), iframe); + clearSwaggerMonitorTimer(); + return; + } + + const hasOperations = swaggerRoot.querySelector('.opblock') || swaggerRoot.querySelector('.opblock-tag-section'); + if (hasOperations) { + setSwaggerPlaceholderState('hidden', null, iframe); + clearSwaggerMonitorTimer(); + return; + } + + if (swaggerMonitorState.attempts >= SWAGGER_MAX_MONITOR_ATTEMPTS) { + setSwaggerPlaceholderState('error', '未能在限定时间内加载任何接口,请刷新或在新窗口中打开。', iframe); + clearSwaggerMonitorTimer(); + return; + } + + swaggerMonitorState.attempts += 1; + scheduleSwaggerContentCheck(iframe); +} + +function setSwaggerPlaceholderState(state, message, iframe) { + const placeholder = document.getElementById('swagger-preview-placeholder'); + const iconNode = placeholder ? placeholder.querySelector('.placeholder-icon i') : null; + const titleNode = placeholder ? placeholder.querySelector('h4') : null; + const descNode = placeholder ? placeholder.querySelector('p') : null; + + if (state === 'loading') { + if (placeholder) { + placeholder.classList.remove('is-hidden', 'has-error'); + } + if (iconNode) { + iconNode.className = 'fas fa-spinner fa-spin'; + } + if (titleNode) { + titleNode.textContent = '正在加载 Swagger UI...'; + } + if (descNode) { + descNode.textContent = '如果长时间未出现内容,请使用右上角按钮在新窗口中打开。'; + } + if (iframe) { + iframe.dataset.loaded = 'loading'; + } + return; + } + + if (state === 'hidden') { + if (placeholder) { + placeholder.classList.add('is-hidden'); + placeholder.classList.remove('has-error'); + } + if (iconNode) { + iconNode.className = 'fas fa-window-restore'; + } + swaggerMonitorState.attempts = 0; + if (iframe) { + iframe.dataset.loaded = '1'; + } + return; + } + + if (state === 'error') { + if (placeholder) { + placeholder.classList.remove('is-hidden'); + placeholder.classList.add('has-error'); + } + if (iconNode) { + iconNode.className = 'fas fa-triangle-exclamation'; + } + if (titleNode) { + titleNode.textContent = 'Swagger UI 加载失败'; + } + if (descNode) { + descNode.textContent = message || '请刷新预览或在新窗口中打开查看完整文档。'; + } + swaggerMonitorState.attempts = 0; + if (iframe) { + iframe.dataset.loaded = '0'; + } + } +} + // ========== 工具函数 ========== /** @@ -945,6 +1185,18 @@ document.addEventListener('DOMContentLoaded', () => { if (activeBtn) { updateHeadlineFromNav(activeBtn); } + + // 监听窗口尺寸变化,动态调整 Swagger iframe 高度 + let resizeTimeout; + window.addEventListener('resize', () => { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(() => { + const swaggerIframe = document.getElementById('swagger-preview'); + if (swaggerIframe && swaggerIframe.src && swaggerIframe.src !== 'about:blank') { + adjustSwaggerIframeHeight(swaggerIframe); + } + }, 150); // 防抖处理,避免频繁调整 + }); }); // 将关键函数和变量暴露到全局作用域 @@ -960,3 +1212,4 @@ window.redirectToUserLogin = redirectToUserLogin; window.showLoginPrompt = showLoginPrompt; window.toggleTheme = toggleTheme; window.authToken = authToken; +window.adjustSwaggerIframeHeight = adjustSwaggerIframeHeight; From ba6364de079689ee0bccbac7330be7caaa38ba1b Mon Sep 17 00:00:00 2001 From: murphyyi Date: Sun, 28 Sep 2025 09:21:26 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E6=8F=90=E4=BA=A4=E9=81=97?= =?UTF-8?q?=E6=BC=8F=E7=9A=84=20API=20=E6=96=87=E6=A1=A3=E3=80=81=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E3=80=81handler=20=E5=8F=8A=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=8F=98=E6=9B=B4\n\n-=20docs:=20swagger=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E4=B8=8E=20API=20=E8=AF=B4=E6=98=8E=E6=9B=B4?= =?UTF-8?q?=E6=96=B0\n-=20=E6=96=B0=E5=A2=9E/=E8=B0=83=E6=95=B4=20API=20?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E4=B8=8E=E4=B8=AD=E9=97=B4=E4=BB=B6\n-=20han?= =?UTF-8?q?dler=20=E9=80=BB=E8=BE=91=E4=BC=98=E5=8C=96\n-=20=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=20dashboard=20=E7=9B=B8=E5=85=B3=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=B8=8E=E6=A0=B7=E5=BC=8F=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/API-README.md | 87 ++++++ docs/swagger-enhanced.yaml | 392 +++++++++++++++++++++++++ docs/swagger.yaml | 434 ++++++++++++++++++++++++++++ internal/handlers/chunk.go | 101 +++++++ internal/handlers/share.go | 156 +++++++++- internal/middleware/api_key_auth.go | 70 +++++ internal/routes/api.go | 45 +++ internal/routes/setup.go | 4 + themes/2025/admin/index.html | 40 +++ themes/2025/css/dashboard.css | 23 ++ themes/2025/dashboard.html | 1 + themes/2025/js/dashboard.js | 5 + 12 files changed, 1347 insertions(+), 11 deletions(-) create mode 100644 internal/middleware/api_key_auth.go create mode 100644 internal/routes/api.go diff --git a/docs/API-README.md b/docs/API-README.md index d43afb8..773d438 100644 --- a/docs/API-README.md +++ b/docs/API-README.md @@ -39,6 +39,93 @@ http://localhost:8080/swagger/doc.json go install github.com/swaggo/swag/cmd/swag@latest # 生成/更新 Swagger 文档 + +## 🧰 API 模式(/api/v1) + +API 模式面向 CLI 工具与自动化脚本,仅开放文件上传/下载及分片管理等核心能力。所有请求必须携带有效的 API Key,系统会拒绝使用普通用户 Token 的请求。 + +### ✅ 支持的接口 + +| 方法 | 路径 | 描述 | +|------|------|------| +| `POST` | `/api/v1/share/text` | 分享文本内容 | +| `POST` | `/api/v1/share/file` | 上传并分享文件 | +| `GET` | `/api/v1/share/{code}` | 查询分享详情 | +| `GET` | `/api/v1/share/{code}/download` | 下载分享内容 | +| `POST` | `/api/v1/chunks/upload/init` | 初始化分片上传 | +| `POST` | `/api/v1/chunks/upload/chunk/{upload_id}/{chunk_index}` | 上传单个分片 | +| `POST` | `/api/v1/chunks/upload/complete/{upload_id}` | 合并分片并生成分享 | +| `GET` | `/api/v1/chunks/upload/status/{upload_id}` | 查询上传进度 | +| `POST` | `/api/v1/chunks/upload/verify/{upload_id}/{chunk_index}` | 校验分片是否存在 | +| `DELETE` | `/api/v1/chunks/upload/cancel/{upload_id}` | 取消分片上传 | + +> 📌 **提示**:API Key 仅可访问 `/api/v1/...` 路由,不具备用户中心(/user/*)权限。 + +### 🔑 请求示例 + +所有示例均假设你已经通过 `/user/api-keys` 生成密钥,并使用 `X-API-Key` 头发送: + +```bash +# 分享文本 +curl -X POST "http://localhost:8080/api/v1/share/text" \ + -H "X-API-Key: " \ + -F "text=Hello API Mode" \ + -F "expire_value=1" \ + -F "expire_style=day" + +# 上传文件 +curl -X POST "http://localhost:8080/api/v1/share/file" \ + -H "X-API-Key: " \ + -F "file=@README.md" \ + -F "expire_value=7" \ + -F "expire_style=day" + +# 根据分享码下载 +curl -L -H "X-API-Key: " \ + "http://localhost:8080/api/v1/share/{code}/download" -o downloaded.bin +``` + +### 📦 分片上传脚本示例 + +```bash +# 1. 初始化上传 +UPLOAD_INFO=$(curl -s -X POST "http://localhost:8080/api/v1/chunks/upload/init" \ + -H "X-API-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "file_name": "large.zip", + "file_size": 10485760, + "chunk_size": 1048576, + "file_hash": "example-hash" + }') +UPLOAD_ID=$(echo "$UPLOAD_INFO" | jq -r '.detail.upload_id') + +# 2. 上传分片(以第 0 块为例) +curl -X POST "http://localhost:8080/api/v1/chunks/upload/chunk/$UPLOAD_ID/0" \ + -H "X-API-Key: " \ + -F "chunk=@part-0.bin" + +# 3. 合并分片 +curl -X POST "http://localhost:8080/api/v1/chunks/upload/complete/$UPLOAD_ID" \ + -H "X-API-Key: " \ + -H "Content-Type: application/json" \ + -d '{ + "expire_value": 7, + "expire_style": "day", + "require_auth": false + }' + +# 4. 查询进度(可选) +curl -H "X-API-Key: " \ + "http://localhost:8080/api/v1/chunks/upload/status/$UPLOAD_ID" + +# 5. 取消上传(可选) +curl -X DELETE -H "X-API-Key: " \ + "http://localhost:8080/api/v1/chunks/upload/cancel/$UPLOAD_ID" +``` + +> 🧪 **建议**:使用 `jq` 或自编脚本解析响应,提取 `detail.code`、`detail.share_url` 等字段,便于自动化处理。 + swag init ``` diff --git a/docs/swagger-enhanced.yaml b/docs/swagger-enhanced.yaml index 284d740..e5700f0 100644 --- a/docs/swagger-enhanced.yaml +++ b/docs/swagger-enhanced.yaml @@ -26,6 +26,11 @@ securityDefinitions: description: "在请求头中携带 Bearer Token,例如:Authorization: Bearer " BasicAuth: type: "basic" + ApiKeyAuth: + type: "apiKey" + name: "X-API-Key" + in: "header" + description: "在请求头中携带 API Key,例如:X-API-Key: 或 Authorization: ApiKey " tags: - name: "系统" @@ -44,6 +49,8 @@ tags: description: "Model Context Protocol 接口" - name: "初始化" description: "系统初始化与安装向导接口" + - name: "API" + description: "API Key 模式下的轻量接口" paths: # 系统接口 @@ -364,6 +371,391 @@ paths: schema: $ref: "#/definitions/ErrorResponse" + # API 模式接口 + /api/v1/share/text: + post: + tags: ["API"] + summary: "分享文本(API 模式)" + description: "通过 API Key 分享文本内容" + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - name: "text" + in: "formData" + type: "string" + required: true + description: "文本内容" + - name: "expire_value" + in: "formData" + type: "integer" + default: 1 + description: "过期值" + - name: "expire_style" + in: "formData" + type: "string" + enum: ["minute", "hour", "day", "week", "month", "year", "forever"] + default: "day" + description: "过期样式" + - name: "require_auth" + in: "formData" + type: "boolean" + default: false + description: "是否需要认证" + security: + - ApiKeyAuth: [] + responses: + 200: + description: "分享成功" + schema: + $ref: "#/definitions/ShareResponse" + 400: + description: "请求参数错误" + schema: + $ref: "#/definitions/ErrorResponse" + 401: + description: "API Key 无效" + schema: + $ref: "#/definitions/ErrorResponse" + 500: + description: "服务器内部错误" + schema: + $ref: "#/definitions/ErrorResponse" + + /api/v1/share/file: + post: + tags: ["API"] + summary: "分享文件(API 模式)" + description: "通过 API Key 上传并分享文件" + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - name: "file" + in: "formData" + type: "file" + required: true + description: "要分享的文件" + - name: "expire_value" + in: "formData" + type: "integer" + default: 1 + description: "过期值" + - name: "expire_style" + in: "formData" + type: "string" + enum: ["minute", "hour", "day", "week", "month", "year", "forever"] + default: "day" + description: "过期样式" + - name: "require_auth" + in: "formData" + type: "boolean" + default: false + description: "是否需要认证" + security: + - ApiKeyAuth: [] + responses: + 200: + description: "分享成功" + schema: + $ref: "#/definitions/ShareResponse" + 400: + description: "请求参数错误" + schema: + $ref: "#/definitions/ErrorResponse" + 401: + description: "API Key 无效" + schema: + $ref: "#/definitions/ErrorResponse" + 500: + description: "服务器内部错误" + schema: + $ref: "#/definitions/ErrorResponse" + + /api/v1/share/{code}: + get: + tags: ["API"] + summary: "获取分享信息(API 模式)" + description: "根据分享代码返回文件或文本信息" + produces: + - "application/json" + parameters: + - name: "code" + in: "path" + type: "string" + required: true + description: "分享代码" + security: + - ApiKeyAuth: [] + responses: + 200: + description: "分享信息" + schema: + $ref: "#/definitions/ShareInfo" + 400: + description: "请求参数错误" + schema: + $ref: "#/definitions/ErrorResponse" + 401: + description: "API Key 无效" + schema: + $ref: "#/definitions/ErrorResponse" + 404: + description: "分享代码不存在" + schema: + $ref: "#/definitions/ErrorResponse" + + /api/v1/share/{code}/download: + get: + tags: ["API"] + summary: "下载分享内容(API 模式)" + description: "根据分享代码下载文件或获取文本内容" + produces: + - "application/octet-stream" + - "application/json" + parameters: + - name: "code" + in: "path" + type: "string" + required: true + description: "分享代码" + security: + - ApiKeyAuth: [] + responses: + 200: + description: "文件或文本内容" + 400: + description: "请求参数错误" + schema: + $ref: "#/definitions/ErrorResponse" + 401: + description: "API Key 无效" + schema: + $ref: "#/definitions/ErrorResponse" + 404: + description: "分享代码不存在" + schema: + $ref: "#/definitions/ErrorResponse" + + /api/v1/chunks/upload/init: + post: + tags: ["API"] + summary: "初始化分片上传(API 模式)" + description: "初始化分片上传并返回上传ID" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "request" + in: "body" + required: true + description: "上传初始化参数" + schema: + $ref: "#/definitions/ChunkInitRequest" + security: + - ApiKeyAuth: [] + responses: + 200: + description: "初始化成功" + schema: + $ref: "#/definitions/ChunkInitResponse" + 400: + description: "请求参数错误" + schema: + $ref: "#/definitions/ErrorResponse" + 401: + description: "API Key 无效" + schema: + $ref: "#/definitions/ErrorResponse" + 500: + description: "服务器内部错误" + schema: + $ref: "#/definitions/ErrorResponse" + + /api/v1/chunks/upload/chunk/{upload_id}/{chunk_index}: + post: + tags: ["API"] + summary: "上传文件分片(API 模式)" + description: "上传指定索引的文件分片" + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - name: "upload_id" + in: "path" + type: "string" + required: true + description: "上传ID" + - name: "chunk_index" + in: "path" + type: "integer" + required: true + description: "分片索引" + - name: "chunk" + in: "formData" + type: "file" + required: true + description: "分片文件" + security: + - ApiKeyAuth: [] + responses: + 200: + description: "上传成功" + schema: + $ref: "#/definitions/ChunkUploadResponse" + 400: + description: "请求参数错误" + schema: + $ref: "#/definitions/ErrorResponse" + 401: + description: "API Key 无效" + schema: + $ref: "#/definitions/ErrorResponse" + 500: + description: "服务器内部错误" + schema: + $ref: "#/definitions/ErrorResponse" + + /api/v1/chunks/upload/complete/{upload_id}: + post: + tags: ["API"] + summary: "完成分片上传(API 模式)" + description: "合并所有分片并生成分享代码" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "upload_id" + in: "path" + type: "string" + required: true + description: "上传ID" + - name: "request" + in: "body" + required: true + description: "完成上传参数" + schema: + $ref: "#/definitions/ChunkCompleteRequest" + security: + - ApiKeyAuth: [] + responses: + 200: + description: "上传完成" + schema: + $ref: "#/definitions/ShareResponse" + 400: + description: "请求参数错误" + schema: + $ref: "#/definitions/ErrorResponse" + 401: + description: "API Key 无效" + schema: + $ref: "#/definitions/ErrorResponse" + 500: + description: "服务器内部错误" + schema: + $ref: "#/definitions/ErrorResponse" + + /api/v1/chunks/upload/status/{upload_id}: + get: + tags: ["API"] + summary: "获取上传状态(API 模式)" + description: "获取分片上传的进度和状态" + produces: + - "application/json" + parameters: + - name: "upload_id" + in: "path" + type: "string" + required: true + description: "上传ID" + security: + - ApiKeyAuth: [] + responses: + 200: + description: "上传状态" + schema: + $ref: "#/definitions/ChunkStatusResponse" + 401: + description: "API Key 无效" + schema: + $ref: "#/definitions/ErrorResponse" + 404: + description: "上传ID不存在" + schema: + $ref: "#/definitions/ErrorResponse" + + /api/v1/chunks/upload/verify/{upload_id}/{chunk_index}: + post: + tags: ["API"] + summary: "校验分片(API 模式)" + description: "用于断点续传场景,校验指定分片是否已存在" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "upload_id" + in: "path" + type: "string" + required: true + description: "上传ID" + - name: "chunk_index" + in: "path" + type: "integer" + required: true + description: "分片索引" + - name: "request" + in: "body" + required: true + description: "分片校验参数" + schema: + $ref: "#/definitions/ChunkVerifyRequest" + security: + - ApiKeyAuth: [] + responses: + 200: + description: "分片校验结果" + schema: + $ref: "#/definitions/ChunkVerifyResponse" + 400: + description: "请求参数错误" + schema: + $ref: "#/definitions/ErrorResponse" + 401: + description: "API Key 无效" + schema: + $ref: "#/definitions/ErrorResponse" + + /api/v1/chunks/upload/cancel/{upload_id}: + delete: + tags: ["API"] + summary: "取消分片上传(API 模式)" + description: "取消分片上传并清理相关文件" + produces: + - "application/json" + parameters: + - name: "upload_id" + in: "path" + type: "string" + required: true + description: "上传ID" + security: + - ApiKeyAuth: [] + responses: + 200: + description: "取消成功" + schema: + $ref: "#/definitions/SuccessResponse" + 401: + description: "API Key 无效" + schema: + $ref: "#/definitions/ErrorResponse" # 分片上传接口 /chunk/upload/init/: post: diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6c2d95a..b046894 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -238,6 +238,316 @@ paths: /share/file/: post: consumes: + /api/v1/share/{code}: + get: + description: 根据分享代码返回文件或文本信息 + parameters: + - description: 分享代码 + in: path + name: code + required: true + type: string + produces: + - application/json + responses: + "200": + description: 分享信息 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "401": + description: API Key 无效 + schema: + additionalProperties: true + type: object + "404": + description: 分享代码不存在 + schema: + additionalProperties: true + type: object + security: + - ApiKeyAuth: [] + summary: 获取分享信息(API 模式) + tags: + - API + /api/v1/share/{code}/download: + get: + description: 根据分享代码下载文件或获取文本内容 + parameters: + - description: 分享代码 + in: path + name: code + required: true + type: string + produces: + - application/octet-stream + - application/json + responses: + "200": + description: 文件或文本内容 + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "401": + description: API Key 无效 + schema: + additionalProperties: true + type: object + "404": + description: 分享代码不存在 + schema: + additionalProperties: true + type: object + security: + - ApiKeyAuth: [] + summary: 下载分享内容(API 模式) + tags: + - API + /api/v1/chunks/upload/init: + post: + consumes: + - application/json + description: 初始化分片上传并返回上传ID + parameters: + - description: 上传初始化参数 + in: body + name: request + required: true + schema: + type: object + produces: + - application/json + responses: + "200": + description: 初始化成功 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "401": + description: API Key 无效 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + security: + - ApiKeyAuth: [] + summary: 初始化分片上传(API 模式) + tags: + - API + /api/v1/chunks/upload/chunk/{upload_id}/{chunk_index}: + post: + consumes: + - multipart/form-data + description: 上传指定索引的文件分片 + parameters: + - description: 上传ID + in: path + name: upload_id + required: true + type: string + - description: 分片索引 + in: path + name: chunk_index + required: true + type: integer + - description: 分片文件 + in: formData + name: chunk + required: true + type: file + produces: + - application/json + responses: + "200": + description: 上传成功 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "401": + description: API Key 无效 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + security: + - ApiKeyAuth: [] + summary: 上传文件分片(API 模式) + tags: + - API + /api/v1/chunks/upload/complete/{upload_id}: + post: + consumes: + - application/json + description: 完成所有分片上传并生成分享代码 + parameters: + - description: 上传ID + in: path + name: upload_id + required: true + type: string + - description: 完成上传参数 + in: body + name: request + required: true + schema: + type: object + produces: + - application/json + responses: + "200": + description: 上传完成,返回分享代码 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "401": + description: API Key 无效 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + security: + - ApiKeyAuth: [] + summary: 完成分片上传(API 模式) + tags: + - API + /api/v1/chunks/upload/status/{upload_id}: + get: + description: 查询指定上传的进度与状态 + parameters: + - description: 上传ID + in: path + name: upload_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 上传状态 + schema: + additionalProperties: true + type: object + "401": + description: API Key 无效 + schema: + additionalProperties: true + type: object + "404": + description: 上传ID不存在 + schema: + additionalProperties: true + type: object + security: + - ApiKeyAuth: [] + summary: 获取上传状态(API 模式) + tags: + - API + /api/v1/chunks/upload/verify/{upload_id}/{chunk_index}: + post: + consumes: + - application/json + description: 校验指定分片是否已存在 + parameters: + - description: 上传ID + in: path + name: upload_id + required: true + type: string + - description: 分片索引 + in: path + name: chunk_index + required: true + type: integer + - description: 校验参数 + in: body + name: request + required: true + schema: + type: object + produces: + - application/json + responses: + "200": + description: 校验结果 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "401": + description: API Key 无效 + schema: + additionalProperties: true + type: object + security: + - ApiKeyAuth: [] + summary: 校验分片(API 模式) + tags: + - API + /api/v1/chunks/upload/cancel/{upload_id}: + delete: + description: 取消进行中的分片上传并清理资源 + parameters: + - description: 上传ID + in: path + name: upload_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: 取消成功 + schema: + additionalProperties: true + type: object + "401": + description: API Key 无效 + schema: + additionalProperties: true + type: object + security: + - ApiKeyAuth: [] + summary: 取消分片上传(API 模式) + tags: + - API - multipart/form-data description: 上传并分享文件,生成分享代码 parameters: @@ -414,6 +724,130 @@ paths: summary: 分享文本内容 tags: - 分享 + /api/v1/share/file: + post: + consumes: + - multipart/form-data + description: 通过 API Key 上传并分享文件 + parameters: + - description: 要分享的文件 + in: formData + name: file + required: true + type: file + - default: 1 + description: 过期值 + in: formData + name: expire_value + type: integer + - default: day + description: 过期样式 + enum: + - minute + - hour + - day + - week + - month + - year + - forever + in: formData + name: expire_style + type: string + - default: false + description: 是否需要认证 + in: formData + name: require_auth + type: boolean + produces: + - application/json + responses: + "200": + description: 分享成功,返回分享代码和文件信息 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "401": + description: API Key 无效 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + security: + - ApiKeyAuth: [] + summary: 分享文件(API 模式) + tags: + - API + /api/v1/share/text: + post: + consumes: + - multipart/form-data + description: 通过 API Key 分享文本内容 + parameters: + - description: 文本内容 + in: formData + name: text + required: true + type: string + - default: 1 + description: 过期值 + in: formData + name: expire_value + type: integer + - default: day + description: 过期样式 + enum: + - minute + - hour + - day + - week + - month + - year + - forever + in: formData + name: expire_style + type: string + - default: false + description: 是否需要认证 + in: formData + name: require_auth + type: boolean + produces: + - application/json + responses: + "200": + description: 分享成功,返回分享代码 + schema: + additionalProperties: true + type: object + "400": + description: 请求参数错误 + schema: + additionalProperties: true + type: object + "401": + description: API Key 无效 + schema: + additionalProperties: true + type: object + "500": + description: 服务器内部错误 + schema: + additionalProperties: true + type: object + security: + - ApiKeyAuth: [] + summary: 分享文本(API 模式) + tags: + - API securityDefinitions: ApiKeyAuth: in: header diff --git a/internal/handlers/chunk.go b/internal/handlers/chunk.go index 6b8b465..0e973b8 100644 --- a/internal/handlers/chunk.go +++ b/internal/handlers/chunk.go @@ -51,6 +51,23 @@ func (h *ChunkHandler) InitChunkUpload(c *gin.Context) { common.SuccessResponse(c, response) } +// InitChunkUploadAPI 初始化分片上传(API 模式) +// @Summary 初始化分片上传(API 模式) +// @Description 使用 API Key 初始化分片上传,返回上传ID +// @Tags API +// @Accept json +// @Produce json +// @Param request body object true "上传初始化参数" +// @Success 200 {object} map[string]interface{} "初始化成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "API Key 校验失败" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/chunks/upload/init [post] +// @Security ApiKeyAuth +func (h *ChunkHandler) InitChunkUploadAPI(c *gin.Context) { + h.InitChunkUpload(c) +} + // UploadChunk 上传分片 // @Summary 上传文件分片 // @Description 上传指定索引的文件分片 @@ -90,6 +107,25 @@ func (h *ChunkHandler) UploadChunk(c *gin.Context) { common.SuccessResponse(c, response) } +// UploadChunkAPI 上传文件分片(API 模式) +// @Summary 上传文件分片(API 模式) +// @Description 上传指定索引的文件分片 +// @Tags API +// @Accept multipart/form-data +// @Produce json +// @Param upload_id path string true "上传ID" +// @Param chunk_index path int true "分片索引" +// @Param chunk formData file true "分片文件" +// @Success 200 {object} map[string]interface{} "上传成功" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "API Key 校验失败" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/chunks/upload/chunk/{upload_id}/{chunk_index} [post] +// @Security ApiKeyAuth +func (h *ChunkHandler) UploadChunkAPI(c *gin.Context) { + h.UploadChunk(c) +} + // CompleteUpload 完成上传 // @Summary 完成分片上传 // @Description 完成所有分片上传,合并文件并生成分享代码 @@ -123,6 +159,24 @@ func (h *ChunkHandler) CompleteUpload(c *gin.Context) { common.SuccessResponse(c, response) } +// CompleteUploadAPI 完成分片上传(API 模式) +// @Summary 完成分片上传(API 模式) +// @Description 合并所有分片并生成分享代码 +// @Tags API +// @Accept json +// @Produce json +// @Param upload_id path string true "上传ID" +// @Param request body object true "完成上传参数" +// @Success 200 {object} map[string]interface{} "上传完成,返回分享代码" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "API Key 校验失败" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/chunks/upload/complete/{upload_id} [post] +// @Security ApiKeyAuth +func (h *ChunkHandler) CompleteUploadAPI(c *gin.Context) { + h.CompleteUpload(c) +} + // GetUploadStatus 获取上传状态(断点续传支持) func (h *ChunkHandler) GetUploadStatus(c *gin.Context) { uploadID := c.Param("upload_id") @@ -137,6 +191,21 @@ func (h *ChunkHandler) GetUploadStatus(c *gin.Context) { common.SuccessResponse(c, status) } +// GetUploadStatusAPI 查询上传状态(API 模式) +// @Summary 查询上传状态(API 模式) +// @Description 查询分片上传的进度和状态 +// @Tags API +// @Produce json +// @Param upload_id path string true "上传ID" +// @Success 200 {object} map[string]interface{} "上传状态" +// @Failure 401 {object} map[string]interface{} "API Key 校验失败" +// @Failure 404 {object} map[string]interface{} "上传ID不存在" +// @Router /api/v1/chunks/upload/status/{upload_id} [get] +// @Security ApiKeyAuth +func (h *ChunkHandler) GetUploadStatusAPI(c *gin.Context) { + h.GetUploadStatus(c) +} + // VerifyChunk 验证分片完整性 func (h *ChunkHandler) VerifyChunk(c *gin.Context) { uploadID := c.Param("upload_id") @@ -165,6 +234,24 @@ func (h *ChunkHandler) VerifyChunk(c *gin.Context) { }) } +// VerifyChunkAPI 校验分片(API 模式) +// @Summary 校验分片(API 模式) +// @Description 校验指定分片是否已上传 +// @Tags API +// @Accept json +// @Produce json +// @Param upload_id path string true "上传ID" +// @Param chunk_index path int true "分片索引" +// @Param request body object true "分片校验参数" +// @Success 200 {object} map[string]interface{} "校验结果" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "API Key 校验失败" +// @Router /api/v1/chunks/upload/verify/{upload_id}/{chunk_index} [post] +// @Security ApiKeyAuth +func (h *ChunkHandler) VerifyChunkAPI(c *gin.Context) { + h.VerifyChunk(c) +} + // CancelUpload 取消上传 func (h *ChunkHandler) CancelUpload(c *gin.Context) { uploadID := c.Param("upload_id") @@ -177,3 +264,17 @@ func (h *ChunkHandler) CancelUpload(c *gin.Context) { common.SuccessWithMessage(c, "上传已取消", nil) } + +// CancelUploadAPI 取消分片上传(API 模式) +// @Summary 取消分片上传(API 模式) +// @Description 取消上传流程并清理已有分片 +// @Tags API +// @Produce json +// @Param upload_id path string true "上传ID" +// @Success 200 {object} map[string]interface{} "取消成功" +// @Failure 401 {object} map[string]interface{} "API Key 校验失败" +// @Router /api/v1/chunks/upload/cancel/{upload_id} [delete] +// @Security ApiKeyAuth +func (h *ChunkHandler) CancelUploadAPI(c *gin.Context) { + h.CancelUpload(c) +} diff --git a/internal/handlers/share.go b/internal/handlers/share.go index 2c33583..decd8bf 100644 --- a/internal/handlers/share.go +++ b/internal/handlers/share.go @@ -82,6 +82,26 @@ func (h *ShareHandler) ShareText(c *gin.Context) { common.SuccessWithMessage(c, "分享成功", response) } +// ShareTextAPI 面向 API Key 用户的文本分享入口 +// @Summary 分享文本(API 模式) +// @Description 通过 API Key 分享文本内容 +// @Tags API +// @Accept multipart/form-data +// @Produce json +// @Param text formData string true "文本内容" +// @Param expire_value formData int false "过期值" default(1) +// @Param expire_style formData string false "过期样式" default(day) Enums(minute, hour, day, week, month, year, forever) +// @Param require_auth formData boolean false "是否需要认证" default(false) +// @Success 200 {object} map[string]interface{} "分享成功,返回分享代码" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "API Key 校验失败" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/share/text [post] +// @Security ApiKeyAuth +func (h *ShareHandler) ShareTextAPI(c *gin.Context) { + h.ShareText(c) +} + // ShareFile 分享文件 // @Summary 分享文件 // @Description 上传并分享文件,生成分享代码 @@ -147,6 +167,26 @@ func (h *ShareHandler) ShareFile(c *gin.Context) { common.SuccessResponse(c, response) } +// ShareFileAPI 面向 API Key 用户的文件分享入口 +// @Summary 分享文件(API 模式) +// @Description 通过 API Key 上传并分享文件 +// @Tags API +// @Accept multipart/form-data +// @Produce json +// @Param file formData file true "要分享的文件" +// @Param expire_value formData int false "过期值" default(1) +// @Param expire_style formData string false "过期样式" default(day) Enums(minute, hour, day, week, month, year, forever) +// @Param require_auth formData boolean false "是否需要认证" default(false) +// @Success 200 {object} map[string]interface{} "分享成功,返回分享代码和文件信息" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "API Key 校验失败" +// @Failure 500 {object} map[string]interface{} "服务器内部错误" +// @Router /api/v1/share/file [post] +// @Security ApiKeyAuth +func (h *ShareHandler) ShareFileAPI(c *gin.Context) { + h.ShareFile(c) +} + // GetFile 获取文件信息 // @Summary 获取分享文件信息 // @Description 根据分享代码获取文件或文本的详细信息 @@ -219,6 +259,47 @@ func (h *ShareHandler) GetFile(c *gin.Context) { common.SuccessResponse(c, response) } +// GetFileAPI 通过 REST 模式查询分享信息(API 模式) +// @Summary 查询分享详情(API 模式) +// @Description 根据分享代码返回分享的文件或文本信息 +// @Tags API +// @Produce json +// @Param code path string true "分享代码" +// @Success 200 {object} map[string]interface{} "分享详情" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "API Key 校验失败" +// @Failure 404 {object} map[string]interface{} "分享不存在" +// @Router /api/v1/share/{code} [get] +// @Security ApiKeyAuth +func (h *ShareHandler) GetFileAPI(c *gin.Context) { + code := c.Param("code") + if code == "" { + common.BadRequestResponse(c, "文件代码不能为空") + return + } + + fileCode, _, ok := h.fetchFileForRequest(c, code) + if !ok { + return + } + + response := web.FileInfoResponse{ + Code: fileCode.Code, + Name: getDisplayFileName(fileCode), + Size: fileCode.Size, + UploadType: fileCode.UploadType, + RequireAuth: fileCode.RequireAuth, + } + + if fileCode.Text != "" { + response.Text = fileCode.Text + } else { + response.Text = "/share/download?code=" + fileCode.Code + } + + common.SuccessResponse(c, response) +} + // DownloadFile 下载文件 // @Summary 下载分享文件 // @Description 根据分享代码下载文件或获取文本内容 @@ -239,7 +320,54 @@ func (h *ShareHandler) DownloadFile(c *gin.Context) { return } - // 获取用户ID(如果已登录) + fileCode, userID, ok := h.fetchFileForRequest(c, code) + if !ok { + return + } + + if h.tryReturnText(c, fileCode, userID) { + return + } + + if !h.streamFileResponse(c, fileCode, userID) { + return + } +} + +// DownloadFileAPI REST 风格下载接口(API 模式) +// @Summary 下载分享内容(API 模式) +// @Description 根据分享代码下载文件或获取文本内容 +// @Tags API +// @Produce application/octet-stream +// @Produce application/json +// @Param code path string true "分享代码" +// @Success 200 {file} binary "文件内容" +// @Success 200 {object} map[string]interface{} "文本内容" +// @Failure 400 {object} map[string]interface{} "请求参数错误" +// @Failure 401 {object} map[string]interface{} "API Key 校验失败" +// @Failure 404 {object} map[string]interface{} "分享不存在" +// @Router /api/v1/share/{code}/download [get] +// @Security ApiKeyAuth +func (h *ShareHandler) DownloadFileAPI(c *gin.Context) { + code := c.Param("code") + if code == "" { + common.BadRequestResponse(c, "文件代码不能为空") + return + } + + fileCode, userID, ok := h.fetchFileForRequest(c, code) + if !ok { + return + } + + if h.tryReturnText(c, fileCode, userID) { + return + } + + _ = h.streamFileResponse(c, fileCode, userID) +} + +func (h *ShareHandler) fetchFileForRequest(c *gin.Context, code string) (*models.FileCode, *uint, bool) { var userID *uint if uid, exists := c.Get("user_id"); exists { id := uid.(uint) @@ -249,36 +377,42 @@ func (h *ShareHandler) DownloadFile(c *gin.Context) { fileCode, err := h.service.GetFileByCodeWithAuth(code, userID) if err != nil { common.NotFoundResponse(c, err.Error()) - return + return nil, nil, false } - // 更新使用次数 if err := h.service.UpdateFileUsage(fileCode.Code); err != nil { - // 记录错误但不阻止下载 logrus.WithError(err).Error("更新文件使用次数失败") } - if fileCode.Text != "" { - common.SuccessResponse(c, fileCode.Text) - h.service.RecordDownloadLog(fileCode, userID, c.ClientIP(), 0) - return + return fileCode, userID, true +} + +func (h *ShareHandler) tryReturnText(c *gin.Context, fileCode *models.FileCode, userID *uint) bool { + if fileCode.Text == "" { + return false } - // 使用存储服务下载文件 + common.SuccessResponse(c, fileCode.Text) + h.service.RecordDownloadLog(fileCode, userID, c.ClientIP(), 0) + return true +} + +func (h *ShareHandler) streamFileResponse(c *gin.Context, fileCode *models.FileCode, userID *uint) bool { storageServiceInterface := h.service.GetStorageService() storageService, ok := storageServiceInterface.(*storage.ConcreteStorageService) if !ok { common.InternalServerErrorResponse(c, "存储服务类型错误") - return + return false } start := time.Now() if err := storageService.GetFileResponse(c, fileCode); err != nil { common.NotFoundResponse(c, "文件下载失败: "+err.Error()) - return + return false } h.service.RecordDownloadLog(fileCode, userID, c.ClientIP(), time.Since(start)) + return true } // getDisplayFileName 获取用于显示的文件名 diff --git a/internal/middleware/api_key_auth.go b/internal/middleware/api_key_auth.go new file mode 100644 index 0000000..527e973 --- /dev/null +++ b/internal/middleware/api_key_auth.go @@ -0,0 +1,70 @@ +package middleware + +import ( + "strings" + + "github.com/gin-gonic/gin" + "github.com/zy84338719/filecodebox/internal/common" + "github.com/zy84338719/filecodebox/internal/services" +) + +// APIKeyAuthenticator 抽象出 API Key 验证能力,避免直接依赖具体服务类型 +type APIKeyAuthenticator interface { + AuthenticateAPIKey(string) (*services.APIKeyAuthResult, error) +} + +// APIKeyAuthOnly 强制 API Key 认证中间件 +// 未携带或携带无效密钥的请求将被直接拒绝 +func APIKeyAuthOnly(authenticator APIKeyAuthenticator) gin.HandlerFunc { + return func(c *gin.Context) { + if authenticator == nil { + common.InternalServerErrorResponse(c, "API key authenticator 未配置") + c.Abort() + return + } + + key := extractAPIKeyFromRequest(c) + if key == "" { + common.UnauthorizedResponse(c, "缺少 API Key") + c.Abort() + return + } + + result, err := authenticator.AuthenticateAPIKey(key) + if err != nil || result == nil { + common.UnauthorizedResponse(c, "API Key 无效或已过期") + c.Abort() + return + } + + c.Set("user_id", result.UserID) + c.Set("username", result.Username) + c.Set("role", result.Role) + c.Set("api_key_id", result.KeyID) + c.Set("auth_via_api_key", true) + c.Set("is_anonymous", false) + + c.Next() + } +} + +// APIKeyAuth 为向后兼容的别名 +func APIKeyAuth(authenticator APIKeyAuthenticator) gin.HandlerFunc { + return APIKeyAuthOnly(authenticator) +} + +func extractAPIKeyFromRequest(c *gin.Context) string { + authHeader := c.GetHeader("Authorization") + if authHeader != "" { + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) == 2 && strings.EqualFold(parts[0], "apikey") { + return strings.TrimSpace(parts[1]) + } + } + + if key := c.GetHeader("X-API-Key"); key != "" { + return strings.TrimSpace(key) + } + + return "" +} diff --git a/internal/routes/api.go b/internal/routes/api.go new file mode 100644 index 0000000..c17ed0a --- /dev/null +++ b/internal/routes/api.go @@ -0,0 +1,45 @@ +package routes + +import ( + "github.com/zy84338719/filecodebox/internal/config" + "github.com/zy84338719/filecodebox/internal/handlers" + "github.com/zy84338719/filecodebox/internal/middleware" + "github.com/zy84338719/filecodebox/internal/services" + + "github.com/gin-gonic/gin" +) + +// SetupAPIRoutes 注册面向 API Key 客户端的精简接口 +func SetupAPIRoutes( + router *gin.Engine, + shareHandler *handlers.ShareHandler, + chunkHandler *handlers.ChunkHandler, + cfg *config.ConfigManager, + userService *services.UserService, +) { + if router == nil || shareHandler == nil || cfg == nil || userService == nil { + return + } + + apiGroup := router.Group("/api/v1") + apiGroup.Use(middleware.ShareAuth(cfg)) + apiGroup.Use(middleware.APIKeyAuthOnly(userService)) + + { + shareGroup := apiGroup.Group("/share") + shareGroup.POST("/text", shareHandler.ShareTextAPI) + shareGroup.POST("/file", shareHandler.ShareFileAPI) + shareGroup.GET("/:code", shareHandler.GetFileAPI) + shareGroup.GET("/:code/download", shareHandler.DownloadFileAPI) + } + + if chunkHandler != nil { + chunkGroup := apiGroup.Group("/chunks") + chunkGroup.POST("/upload/init", chunkHandler.InitChunkUploadAPI) + chunkGroup.POST("/upload/chunk/:upload_id/:chunk_index", chunkHandler.UploadChunkAPI) + chunkGroup.POST("/upload/complete/:upload_id", chunkHandler.CompleteUploadAPI) + chunkGroup.GET("/upload/status/:upload_id", chunkHandler.GetUploadStatusAPI) + chunkGroup.POST("/upload/verify/:upload_id/:chunk_index", chunkHandler.VerifyChunkAPI) + chunkGroup.DELETE("/upload/cancel/:upload_id", chunkHandler.CancelUploadAPI) + } +} diff --git a/internal/routes/setup.go b/internal/routes/setup.go index a9977f9..ffc131b 100644 --- a/internal/routes/setup.go +++ b/internal/routes/setup.go @@ -289,6 +289,7 @@ func RegisterDynamicRoutes( // Use API-only user routes here to avoid duplicate page route registration SetupUserAPIRoutes(router, userHandler, manager, userService) SetupChunkRoutes(router, chunkHandler, manager, userService) + SetupAPIRoutes(router, shareHandler, chunkHandler, manager, userService) SetupAdminRoutes(router, adminHandler, storageHandler, manager, userService) // System init routes are no longer needed after DB init } @@ -336,6 +337,9 @@ func SetupAllRoutes( // 设置分片上传路由 SetupChunkRoutes(router, chunkHandler, manager, userService) + // 设置 API Key 客户端路由 + SetupAPIRoutes(router, shareHandler, chunkHandler, manager, userService) + // 设置管理员路由 SetupAdminRoutes(router, adminHandler, storageHandler, manager, userService) } diff --git a/themes/2025/admin/index.html b/themes/2025/admin/index.html index feceba2..dfa0979 100644 --- a/themes/2025/admin/index.html +++ b/themes/2025/admin/index.html @@ -61,6 +61,10 @@ 传输日志 + + + + +
+ +
+
+ +
+

正在加载 Swagger UI...

+

如果长时间未出现内容,请使用右上角按钮在新窗口中打开。

+
+
+ + + +
diff --git a/themes/2025/css/dashboard.css b/themes/2025/css/dashboard.css index 563af8a..836eeab 100644 --- a/themes/2025/css/dashboard.css +++ b/themes/2025/css/dashboard.css @@ -765,6 +765,29 @@ body { font-weight: 600; } +.api-key-scope-note { + flex-basis: 100%; + display: block; + color: #2563eb; + font-size: 13px; + line-height: 1.6; + margin-top: 4px; +} + +.api-key-scope-note code { + background: rgba(37, 99, 235, 0.08); + color: #1d4ed8; + padding: 2px 6px; + border-radius: 6px; + font-size: 12px; +} + +.api-key-scope-note a { + color: #1d4ed8; + text-decoration: underline; + font-weight: 500; +} + .api-key-result { background: #f8fafc; border: 1px solid #e2e8f0; diff --git a/themes/2025/dashboard.html b/themes/2025/dashboard.html index f93595a..d90e20e 100644 --- a/themes/2025/dashboard.html +++ b/themes/2025/dashboard.html @@ -176,6 +176,7 @@
每个账户最多可保留 5 个有效密钥。 +
diff --git a/themes/2025/js/dashboard.js b/themes/2025/js/dashboard.js index 34b6a5c..ac0be35 100644 --- a/themes/2025/js/dashboard.js +++ b/themes/2025/js/dashboard.js @@ -669,6 +669,7 @@ const Dashboard = { const refreshBtn = document.getElementById('api-key-refresh-btn'); const closeResultBtn = document.getElementById('api-key-result-close'); const copyResultBtn = document.getElementById('api-key-result-copy'); + const scopeNote = document.getElementById('api-key-scope-note'); if (expireTypeSelect) { expireTypeSelect.addEventListener('change', () => { @@ -677,6 +678,10 @@ const Dashboard = { this.toggleAPIKeyCustomFields(expireTypeSelect.value === 'custom', customFields); } + if (scopeNote) { + scopeNote.innerHTML = '🌐 生成的 API 密钥仅用于调用 /api/v1 路由(例如 /api/v1/share/text/api/v1/chunk/init),请在请求头中携带 X-API-Key。更多示例见 Swagger 文档。'; + } + if (refreshBtn) { refreshBtn.addEventListener('click', () => { this.loadAPIKeys(true); From 1d5dcf6c416bd18c4252f8269fc45140a286e3ba Mon Sep 17 00:00:00 2001 From: murphyyi Date: Sun, 28 Sep 2025 17:38:11 +0800 Subject: [PATCH 3/3] refactor: refresh dashboard ui layout --- themes/2025/css/auth.css | 168 ++++- themes/2025/css/base.css | 76 ++- themes/2025/css/components.css | 169 +++-- themes/2025/css/dashboard.css | 595 ++++++++---------- themes/2025/css/layout.css | 106 +++- themes/2025/css/responsive.css | 50 +- themes/2025/dashboard.html | 297 +++++---- themes/2025/forgot-password.html | 1013 ++++++++++++------------------ themes/2025/index.html | 182 +++--- themes/2025/js/dashboard.js | 31 +- 10 files changed, 1344 insertions(+), 1343 deletions(-) diff --git a/themes/2025/css/auth.css b/themes/2025/css/auth.css index c3fcbf7..70cddd8 100644 --- a/themes/2025/css/auth.css +++ b/themes/2025/css/auth.css @@ -101,15 +101,15 @@ } .auth-subtitle { - color: #666; - font-size: 0.9em; + color: var(--color-text-secondary); + font-size: 0.95em; margin-bottom: var(--spacing-sm); } .auth-title { font-size: 1.4em; font-weight: 600; - color: #333; + color: var(--color-text-primary); margin-bottom: var(--spacing-xs); } @@ -122,13 +122,13 @@ display: block; margin-bottom: var(--spacing-xs); font-weight: 600; - color: #333; + color: var(--color-text-primary); } .auth-form .form-control { width: 100%; padding: var(--spacing-sm) var(--spacing-md); - border: 2px solid #e0e0e0; + border: 2px solid var(--color-border); border-radius: var(--radius-sm); font-size: 16px; transition: all 0.3s ease; @@ -146,7 +146,7 @@ .auth-form select { width: 100%; padding: var(--spacing-sm) var(--spacing-md); - border: 2px solid #e0e0e0; + border: 2px solid var(--color-border); border-radius: var(--radius-sm); font-size: 16px; transition: all 0.3s ease; @@ -178,14 +178,14 @@ .auth-form .form-control.error, .auth-form input.error { - border-color: #e53e3e; - background-color: rgba(229, 62, 62, 0.05); + border-color: var(--color-danger); + background-color: rgba(239, 68, 68, 0.08); } .auth-form .form-control.success, .auth-form input.success { - border-color: #38a169; - background-color: rgba(56, 161, 105, 0.05); + border-color: var(--color-success); + background-color: rgba(34, 197, 94, 0.08); } /* 按钮样式 */ @@ -217,6 +217,24 @@ box-shadow: none; } +.auth-btn--outline { + background: transparent; + color: var(--primary-color); + border: 2px solid var(--primary-color); + box-shadow: none; +} + +.auth-btn--outline:hover:not(:disabled) { + background: rgba(102, 126, 234, 0.12); + color: var(--primary-color); +} + +.auth-btn--outline:disabled { + color: rgba(148, 163, 184, 0.9); + border-color: rgba(148, 163, 184, 0.6); + background: transparent; +} + .auth-btn.loading { color: transparent; } @@ -249,15 +267,15 @@ } .auth-message.error { - background: #fef2f2; - color: #dc2626; - border: 1px solid #fecaca; + background: rgba(239, 68, 68, 0.08); + color: var(--color-danger); + border: 1px solid rgba(239, 68, 68, 0.24); } .auth-message.success { - background: #f0fdf4; - color: #16a34a; - border: 1px solid #bbf7d0; + background: rgba(34, 197, 94, 0.08); + color: var(--color-success); + border: 1px solid rgba(34, 197, 94, 0.24); } /* 表单底部 */ @@ -288,7 +306,7 @@ } .auth-link-section p { - color: #666; + color: var(--color-text-secondary); margin-bottom: var(--spacing-sm); font-size: 0.9em; } @@ -349,11 +367,11 @@ } .field-message.error { - color: #e53e3e; + color: var(--color-danger); } .field-message.success { - color: #38a169; + color: var(--color-success); } /* 复选框样式 */ @@ -374,7 +392,7 @@ .checkbox-group label { margin-bottom: 0; font-size: 13px; - color: #666; + color: var(--color-text-secondary); line-height: 1.4; } @@ -399,7 +417,7 @@ transform: translateY(-50%); background: none; border: none; - color: #666; + color: var(--color-text-secondary); cursor: pointer; padding: var(--spacing-xs); border-radius: var(--radius-xs); @@ -408,7 +426,113 @@ .password-toggle-btn:hover { background: #f5f5f5; - color: #333; + color: var(--color-text-primary); +} + +.auth-stepper { + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacing-xs); + margin-bottom: var(--spacing-lg); +} + +.auth-stepper__item { + width: 36px; + height: 36px; + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + color: var(--color-text-secondary); + background: rgba(148, 163, 184, 0.16); + transition: all var(--transition-base); +} + +.auth-stepper__item.active { + background: var(--primary-gradient); + color: #fff; + box-shadow: var(--shadow-sm); +} + +.auth-stepper__item.completed { + background: linear-gradient(135deg, var(--color-success), #15803d); + color: #fff; +} + +.auth-stepper__line { + width: 45px; + height: 2px; + background: rgba(148, 163, 184, 0.24); + border-radius: var(--radius-full); + transition: background var(--transition-base); +} + +.auth-stepper__line.completed { + background: var(--color-success); +} + +.auth-step-title { + text-align: center; + font-size: 1.1rem; + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: var(--spacing-sm); +} + +.auth-step-description { + text-align: center; + color: var(--color-text-secondary); + font-size: 0.95rem; + margin-bottom: var(--spacing-lg); + line-height: 1.6; +} + +.auth-message-inline { + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--radius-sm); + margin-top: var(--spacing-sm); + font-size: 0.9rem; + text-align: center; +} + +.auth-message-inline.success { + background: rgba(34, 197, 94, 0.12); + color: var(--color-success); + border: 1px solid rgba(34, 197, 94, 0.2); +} + +.auth-message-inline.error { + background: rgba(239, 68, 68, 0.12); + color: var(--color-danger); + border: 1px solid rgba(239, 68, 68, 0.2); +} + +.auth-message-inline.info { + background: rgba(14, 165, 233, 0.12); + color: var(--color-info); + border: 1px solid rgba(14, 165, 233, 0.2); +} + +.auth-section { + display: none; +} + +.auth-section.active { + display: block; +} + +.hidden-content { + display: none !important; +} + +.step { + display: none; +} + +.step.active { + display: block; } /* 响应式设计 */ diff --git a/themes/2025/css/base.css b/themes/2025/css/base.css index a778930..46a2baa 100644 --- a/themes/2025/css/base.css +++ b/themes/2025/css/base.css @@ -11,21 +11,40 @@ --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); --primary-color: #667eea; --secondary-color: #764ba2; - + + /* 品牌辅助色彩 */ + --color-success: #22c55e; + --color-warning: #f97316; + --color-danger: #ef4444; + --color-info: #0ea5e9; + + /* 中性色与文字体系 */ + --color-bg-app: #f5f7fb; + --color-bg-soft: #eef2ff; + --color-surface: #ffffff; + --color-surface-subtle: rgba(255, 255, 255, 0.78); + --color-border: rgba(148, 163, 184, 0.28); + --color-divider: rgba(148, 163, 184, 0.14); + --color-text-primary: #0f172a; + --color-text-secondary: #475569; + --color-text-muted: #94a3b8; + /* 统一的圆角系统 - 同心圆设计 */ --radius-xs: 6px; /* 小元素 */ --radius-sm: 12px; /* 按钮、输入框 */ --radius-md: 18px; /* 卡片、容器 */ --radius-lg: 24px; /* 主要容器 */ --radius-xl: 30px; /* 页面容器 */ - + --radius-full: 999px; /* 圆角胶囊 */ + /* 统一的阴影系统 */ - --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.06); - --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.12); - --shadow-xl: 0 16px 64px rgba(0, 0, 0, 0.16); - + --shadow-sm: 0 2px 8px rgba(15, 23, 42, 0.06); + --shadow-md: 0 6px 18px rgba(15, 23, 42, 0.08); + --shadow-lg: 0 12px 32px rgba(15, 23, 42, 0.1); + --shadow-xl: 0 20px 48px rgba(15, 23, 42, 0.12); + /* 统一的间距系统 */ + --spacing-2xs: 4px; --spacing-xs: 8px; --spacing-sm: 12px; --spacing-md: 16px; @@ -33,10 +52,22 @@ --spacing-xl: 32px; --spacing-2xl: 40px; --spacing-3xl: 48px; + + /* 字号体系 */ + --font-size-xs: 12px; + --font-size-sm: 14px; + --font-size-md: 16px; + --font-size-lg: 18px; + --font-size-xl: 24px; + + /* 其他通用变量 */ + --transition-base: 0.25s ease; + --container-max-width: 1200px; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + color: var(--color-text-primary); background: var(--primary-gradient); min-height: 100vh; display: flex; @@ -45,6 +76,37 @@ body { background-size: cover; background-position: center; background-attachment: fixed; + line-height: 1.6; +} + +body.theme-dashboard { + background: var(--color-bg-app); + display: block; + padding: var(--spacing-2xl) var(--spacing-xl); + background-size: auto; + background-position: top center; + background-attachment: scroll; +} + +body.landing-page { + display: block; + padding: var(--spacing-3xl) var(--spacing-xl); +} + +body.landing-page .container { + margin: 0 auto; +} + +@media (max-width: 992px) { + body.landing-page { + padding: var(--spacing-2xl) var(--spacing-lg); + } +} + +@media (max-width: 600px) { + body.landing-page { + padding: var(--spacing-xl) var(--spacing-md); + } } /* 动态背景样式 - 由JavaScript动态设置 */ diff --git a/themes/2025/css/components.css b/themes/2025/css/components.css index 9c45fda..d092671 100644 --- a/themes/2025/css/components.css +++ b/themes/2025/css/components.css @@ -3,13 +3,27 @@ /* 用户链接区域 */ .user-links { position: absolute; - top: 15px; - right: 20px; + top: var(--spacing-lg); + right: var(--spacing-lg); display: flex; - gap: 10px; + gap: var(--spacing-sm); align-items: center; } +.user-links__group { + display: flex; + gap: var(--spacing-sm); + align-items: center; +} + +#guest-links { + display: flex; +} + +#user-logged-in { + display: none; +} + .user-link { padding: 6px 12px; border-radius: 20px; @@ -20,8 +34,8 @@ } .user-link.login { - color: #667eea; - border: 1px solid #667eea; + color: var(--primary-color); + border: 1px solid var(--primary-color); background: transparent; } @@ -31,7 +45,7 @@ } .user-link.register { - background: linear-gradient(135deg, #667eea, #764ba2); + background: var(--primary-gradient); color: white; border: none; } @@ -45,14 +59,18 @@ .user-info { display: flex; align-items: center; - gap: 10px; + gap: var(--spacing-sm); +} + +.user-info--clickable { + cursor: pointer; } .user-avatar { width: 32px; height: 32px; - border-radius: 50%; - background: linear-gradient(135deg, #667eea, #764ba2); + border-radius: var(--radius-full); + background: var(--primary-gradient); display: flex; align-items: center; justify-content: center; @@ -62,7 +80,7 @@ } .user-name { - color: #333; + color: var(--color-text-primary); font-weight: 600; font-size: 0.9em; } @@ -78,13 +96,14 @@ position: absolute; right: 0; top: 100%; - background-color: white; + background-color: var(--color-surface); min-width: 150px; - box-shadow: 0 8px 16px rgba(0,0,0,0.1); - border-radius: 8px; + box-shadow: var(--shadow-md); + border-radius: var(--radius-sm); overflow: hidden; z-index: 1000; margin-top: 5px; + border: 1px solid var(--color-border); } .dropdown-content.show { @@ -92,7 +111,7 @@ } .dropdown-item { - color: #333; + color: var(--color-text-primary); padding: 12px 16px; text-decoration: none; display: block; @@ -101,30 +120,30 @@ } .dropdown-item:hover { - background-color: #f8f9fa; + background-color: rgba(102, 126, 234, 0.08); } .dropdown-item.logout { - color: #dc3545; - border-top: 1px solid #e9ecef; + color: var(--color-danger); + border-top: 1px solid var(--color-divider); } .dropdown-item.logout:hover { - background-color: #f8d7da; + background-color: rgba(239, 68, 68, 0.12); } /* 头部区域 */ .header { text-align: center; - margin-bottom: 30px; + margin-bottom: var(--spacing-xl); } .logo { display: flex; align-items: center; justify-content: center; - gap: 15px; - margin-bottom: 10px; + gap: var(--spacing-md); + margin-bottom: var(--spacing-sm); } .logo-icon { @@ -133,52 +152,61 @@ } .logo-text { - font-size: 2.5em; + font-size: clamp(2rem, 4vw, 2.75rem); font-weight: bold; - background: linear-gradient(135deg, #667eea, #764ba2); + background: var(--primary-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .subtitle { - color: #666; - font-size: 1.1em; - margin-top: 5px; + color: var(--color-text-secondary); + font-size: 1rem; + font-weight: 500; + max-width: 80%; + margin: 0 auto; +} + +.landing-intro { + text-align: center; + color: var(--color-text-secondary); + font-size: 0.95rem; + line-height: 1.7; } /* 标签页 */ .tabs { display: flex; - margin-bottom: 30px; - border-radius: 25px; - background: #f8f9fa; - padding: 5px; + border-radius: var(--radius-full); + background: var(--color-bg-soft); + padding: 6px; + gap: var(--spacing-xs); } .tab { flex: 1; - padding: 12px 20px; + padding: var(--spacing-sm) var(--spacing-lg); text-align: center; cursor: pointer; - border-radius: 20px; - transition: all 0.3s ease; + border-radius: var(--radius-full); + transition: all var(--transition-base); font-weight: 600; - color: #666; + color: var(--color-text-secondary); background: transparent; border: none; - font-size: 14px; + font-size: var(--font-size-sm); } .tab.active { - background: linear-gradient(135deg, #667eea, #764ba2); + background: var(--primary-gradient); color: white; - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); + box-shadow: 0 8px 24px rgba(102, 126, 234, 0.28); } .tab:hover:not(.active) { - background: rgba(102, 126, 234, 0.1); - color: #667eea; + background: rgba(102, 126, 234, 0.12); + color: var(--primary-color); } /* 标签页内容 */ @@ -192,68 +220,73 @@ /* 表单组 */ .form-group { - margin-bottom: 20px; + margin-bottom: var(--spacing-md); +} + +.section-stack .form-group { + margin-bottom: 0; } .form-group label { display: block; margin-bottom: 8px; font-weight: 600; - color: #333; - font-size: 14px; + color: var(--color-text-primary); + font-size: var(--font-size-sm); } /* 上传区域 */ .upload-area { - border: 2px dashed #ddd; - border-radius: 10px; - padding: 40px 20px; + border: 2px dashed var(--color-border); + border-radius: var(--radius-lg); + padding: var(--spacing-2xl) var(--spacing-xl); text-align: center; - margin-bottom: 20px; - transition: all 0.3s ease; - background: rgba(255, 255, 255, 0.5); + margin-bottom: var(--spacing-lg); + transition: all var(--transition-base); + background: rgba(255, 255, 255, 0.6); } .upload-area:hover { - border-color: #667eea; - background: rgba(102, 126, 234, 0.05); + border-color: var(--primary-color); + background: rgba(102, 126, 234, 0.08); } .upload-area.dragover { - border-color: #667eea; - background: rgba(102, 126, 234, 0.1); + border-color: var(--primary-color); + background: rgba(102, 126, 234, 0.15); } .upload-icon { - font-size: 48px; - margin-bottom: 15px; - color: #666; + font-size: 3rem; + margin-bottom: var(--spacing-sm); + color: var(--color-text-secondary); } .upload-text { - color: #666; - font-size: 16px; - margin-bottom: 20px; + color: var(--color-text-primary); + font-size: var(--font-size-md); + font-weight: 600; + margin-bottom: var(--spacing-md); } /* 上传选项按钮 */ .upload-options { display: flex; - gap: 15px; + gap: var(--spacing-md); justify-content: center; - margin-top: 20px; + margin-top: var(--spacing-lg); } .upload-option-btn { padding: 10px 20px; - background: linear-gradient(135deg, #667eea, #764ba2); + background: var(--primary-gradient); color: white; border: none; - border-radius: 6px; + border-radius: var(--radius-sm); cursor: pointer; - font-size: 14px; + font-size: var(--font-size-sm); font-weight: 600; - transition: all 0.3s ease; + transition: all var(--transition-base); text-decoration: none; display: inline-block; } @@ -269,10 +302,10 @@ /* 页脚 */ .footer { - margin-top: 30px; + margin-top: var(--spacing-lg); text-align: center; - color: #888; - font-size: 0.9em; + color: var(--color-text-muted); + font-size: 0.85rem; line-height: 1.6; } @@ -281,7 +314,7 @@ } .footer a { - color: #667eea; + color: var(--primary-color); text-decoration: none; } diff --git a/themes/2025/css/dashboard.css b/themes/2025/css/dashboard.css index 836eeab..bf53073 100644 --- a/themes/2025/css/dashboard.css +++ b/themes/2025/css/dashboard.css @@ -1,79 +1,77 @@ +.dashboard-shell .section-stack { + gap: var(--spacing-xl); +} + +#upload-form.section-stack, +#profile-form.section-stack, +#password-form.section-stack, +.api-key-form.section-stack { + gap: var(--spacing-lg); +} /* 仪表板专用样式 - 用户中心页面特有的样式 */ -/* 重置body样式为全屏居中布局 */ -body { - background: #f5f5f5; +/* 重置 body 样式为主题化布局 */ +body.theme-dashboard { + background: var(--color-bg-app); min-height: 100vh; - display: flex; - align-items: flex-start; - justify-content: center; - padding: var(--spacing-lg) 0; + padding: clamp(var(--spacing-xl), 5vw, var(--spacing-3xl)) var(--spacing-xl); } -/* 容器布局 - 居中设计 */ -.container { - max-width: 1200px; - width: 95%; +/* 页面主体布局 */ +.dashboard-shell { + width: min(100%, var(--container-max-width)); margin: 0 auto; - padding: 0; - background: transparent; - box-shadow: none; - border-radius: 0; + display: flex; + flex-direction: column; + gap: var(--spacing-2xl); } /* 头部样式 */ -.header { - background: white; - padding: var(--spacing-xl); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-md); - margin-bottom: var(--spacing-lg); - display: flex; - justify-content: space-between; - margin-bottom: 20px; - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--spacing-md); - flex-wrap: wrap; +.dashboard-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: var(--spacing-xl); } -.logo { +.dashboard-brand { display: flex; align-items: center; - gap: var(--spacing-sm); - font-size: 1.5em; - font-weight: bold; - color: var(--primary-color); + gap: var(--spacing-md); } -.logo-icon { - width: 32px; - height: 32px; +.dashboard-brand .logo-icon { + width: 36px; + height: 36px; object-fit: contain; } -/* 用户信息区域重写(仪表板特有) */ -.header .user-info { +.dashboard-brand-text { + display: flex; + flex-direction: column; + gap: 4px; +} + +.dashboard-brand-text .brand-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-color); +} + +.dashboard-brand-text .brand-subtitle { + color: var(--color-text-secondary); + font-size: var(--font-size-sm); +} + +.dashboard-user { display: flex; align-items: center; gap: var(--spacing-md); + justify-content: flex-end; + color: var(--color-text-primary); } - .section-actions { - display: flex; - align-items: center; - gap: var(--spacing-sm); - } - - .section-desc { - margin-top: -10px; - margin-bottom: var(--spacing-lg); - color: #6b7280; - font-size: 14px; - line-height: 1.6; - } -.header .user-avatar { +.dashboard-user .user-avatar { width: 40px; height: 40px; border-radius: var(--radius-full); @@ -82,119 +80,84 @@ body { display: flex; align-items: center; justify-content: center; - font-weight: bold; + font-weight: 600; font-size: 16px; } -.header .user-name { - font-weight: 500; - color: #333; -} - -/* 管理员和退出按钮 */ -.admin-btn { - background: var(--primary-gradient); - color: white; - border: none; - padding: var(--spacing-xs) var(--spacing-md); - border-radius: var(--radius-xs); - cursor: pointer; - text-decoration: none; - font-size: 14px; - margin-right: var(--spacing-sm); - transition: all 0.3s ease; +.dashboard-user .user-name { + font-weight: 600; } -.admin-btn:hover { - opacity: 0.9; - transform: translateY(-1px); +.dashboard-actions { + display: flex; + align-items: center; + gap: var(--spacing-sm); + flex-wrap: wrap; } -.logout-btn { - background: #dc3545; - color: white; - border: none; - padding: var(--spacing-xs) var(--spacing-md); - border-radius: var(--radius-xs); - cursor: pointer; - text-decoration: none; - font-size: 14px; - transition: all 0.3s ease; +.dashboard-actions .btn { + min-width: 108px; } -.logout-btn:hover { - opacity: 0.9; - transform: translateY(-1px); +.dashboard-actions .admin-btn { + display: none; } -/* 标签页容器 */ -.nav-tabs { - background: white; - border-radius: var(--radius-lg); - box-shadow: var(--shadow-md); +.section-desc { + margin-top: -4px; margin-bottom: var(--spacing-lg); - overflow: hidden; + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + line-height: 1.6; } -/* 标签按钮 */ -.tabs { - display: flex; - border-bottom: 1px solid #eee; +/* 标签导航与内容 */ +.dashboard-tabs { + padding: var(--spacing-xs); + border-radius: var(--radius-full); + border: 1px solid var(--color-border); + background: var(--color-surface); + box-shadow: var(--shadow-md); } -.tab { - flex: 1; - padding: var(--spacing-md) var(--spacing-lg); - background: none; - border: none; - cursor: pointer; - transition: all 0.3s ease; - color: #666; - font-size: 16px; +.dashboard-tabs .tabs { + width: 100%; + justify-content: center; + gap: var(--spacing-xs); + flex-wrap: wrap; } -.tab:hover { - background: #f8f9fa; +.tab-panels { + display: flex; + flex-direction: column; + gap: var(--spacing-2xl); } -.tab.active { - background: var(--primary-color); - color: white; +.dashboard-shell .tab-content.section-stack { + gap: var(--spacing-xl); + padding: var(--spacing-xl); } -/* 标签内容 */ -.tab-content { +.dashboard-shell .tab-content.section-stack:not(.active) { display: none; - padding: var(--spacing-lg); -} - -.tab-content.active { - display: block; } -/* 内容区域 */ -.content-section { - margin-bottom: 30px; -} - -.section-header { - margin-bottom: 20px; +.dashboard-shell .tab-content.section-stack.active { + display: flex; + flex-direction: column; } -.section-title { - font-size: 1.2em; - font-weight: bold; - color: #333; - padding-bottom: 10px; - border-bottom: 2px solid #667eea; - flex: 1 1 auto; +.dashboard-shell .tab-content[hidden] { + display: none !important; } -.section-content { - background: white; - padding: 25px; - border-radius: 10px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +/* 内容区域 */ +.dashboard-shell .content-section { + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + background: var(--color-surface-subtle); + box-shadow: none; + padding: var(--spacing-xl); } /* 统计卡片网格 */ @@ -202,20 +165,21 @@ body { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--spacing-lg); - margin-bottom: var(--spacing-2xl); } .stat-card { - background: white; + background: var(--color-surface); padding: var(--spacing-xl); border-radius: var(--radius-lg); box-shadow: var(--shadow-md); text-align: center; - transition: transform 0.3s ease; + transition: transform var(--transition-base), box-shadow var(--transition-base); + border: 1px solid var(--color-border); } .stat-card:hover { transform: translateY(-5px); + box-shadow: var(--shadow-lg); } .stat-icon { @@ -231,13 +195,13 @@ body { } .stat-label { - color: #666; - font-size: 14px; + color: var(--color-text-secondary); + font-size: var(--font-size-sm); } /* 进度条 */ .progress-bar { - background: #f0f0f0; + background: rgba(148, 163, 184, 0.16); border-radius: var(--radius-full); overflow: hidden; height: 20px; @@ -253,30 +217,31 @@ body { /* 上传区域 */ .upload-area { - border: 2px dashed #ddd; + border: 2px dashed var(--color-border); border-radius: var(--radius-lg); padding: var(--spacing-3xl); text-align: center; cursor: pointer; - transition: all 0.3s ease; + transition: all var(--transition-base); margin-bottom: var(--spacing-lg); + background: rgba(255, 255, 255, 0.7); } .upload-area:hover, .upload-area.dragover { border-color: var(--primary-color); - background: #f8f9fa; + background: rgba(102, 126, 234, 0.1); } .upload-text { - font-size: 16px; - color: #666; + font-size: var(--font-size-md); + color: var(--color-text-secondary); margin-bottom: var(--spacing-sm); } .upload-hint { - font-size: 14px; - color: #999; + font-size: var(--font-size-sm); + color: var(--color-text-muted); } /* 表单样式 */ @@ -287,8 +252,8 @@ body { .form-group label { display: block; margin-bottom: var(--spacing-xs); - font-weight: 500; - color: #333; + font-weight: 600; + color: var(--color-text-primary); } .form-group input, @@ -296,10 +261,10 @@ body { .form-group textarea { width: 100%; padding: var(--spacing-sm); - border: 1px solid #ddd; + border: 1px solid var(--color-border); border-radius: var(--radius-xs); - font-size: 16px; - transition: border-color 0.3s ease; + font-size: var(--font-size-md); + transition: border-color var(--transition-base), box-shadow var(--transition-base); } .form-group input:focus, @@ -310,8 +275,8 @@ body { } .form-group input[readonly] { - background: #f8f9fa; - color: #666; + background: rgba(148, 163, 184, 0.08); + color: var(--color-text-secondary); } .form-row { @@ -336,36 +301,40 @@ body { /* 按钮样式扩展 */ .btn-sm { - padding: 6px 12px; - font-size: 14px; + padding: var(--spacing-xs) var(--spacing-sm); + font-size: var(--font-size-xs); } .btn-info { - background: #17a2b8; + background: linear-gradient(135deg, rgba(14, 165, 233, 0.92), rgba(59, 130, 246, 0.92)); + color: #fff; } .btn-success { - background: #28a745; + background: linear-gradient(135deg, rgba(34, 197, 94, 0.95), rgba(16, 185, 129, 0.95)); + color: #fff; } .btn-danger { - background: #dc3545; + background: linear-gradient(135deg, rgba(239, 68, 68, 0.95), rgba(190, 18, 60, 0.95)); + color: #fff; } .btn-secondary { - background: #6c757d; + background: rgba(148, 163, 184, 0.28); + color: var(--color-text-primary); } .btn-secondary:hover { - box-shadow: 0 4px 15px rgba(108, 117, 125, 0.4); + box-shadow: 0 4px 15px rgba(148, 163, 184, 0.36); } /* 进度条 */ .progress { width: 100%; height: 20px; - background: #f0f0f0; - border-radius: 10px; + background: rgba(148, 163, 184, 0.18); + border-radius: var(--radius-full); overflow: hidden; margin: 10px 0; display: none; @@ -373,20 +342,21 @@ body { .progress .progress-fill { height: 100%; - background: linear-gradient(90deg, #667eea, #764ba2); - border-radius: 10px; - transition: width 0.3s ease; + background: var(--primary-gradient); + border-radius: var(--radius-full); + transition: width var(--transition-base); width: 0%; } /* 文件列表 */ .file-list { - background: white; + background: var(--color-surface); border-radius: var(--radius-xl); overflow: hidden; box-shadow: var(--shadow-lg); margin-top: var(--spacing-lg); animation: slideInUp 0.3s ease; + border: 1px solid var(--color-border); } @keyframes slideInUp { @@ -403,12 +373,13 @@ body { .file-table { width: 100%; border-collapse: collapse; - font-size: 14px; - background: white; + font-size: var(--font-size-sm); + background: var(--color-surface); border-radius: var(--radius-lg); overflow: hidden; - box-shadow: var(--shadow-lg); + box-shadow: var(--shadow-md); margin-top: var(--spacing-lg); + border: 1px solid var(--color-border); } .file-table th { @@ -435,8 +406,9 @@ body { .file-table td { padding: var(--spacing-md) var(--spacing-md); text-align: left; - border-bottom: 1px solid #f1f3f4; + border-bottom: 1px solid var(--color-divider); vertical-align: middle; + color: var(--color-text-secondary); } .file-table tr { @@ -444,7 +416,7 @@ body { } .file-table tbody tr:hover { - background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%); + background: rgba(102, 126, 234, 0.08); transform: translateY(-1px); box-shadow: var(--shadow-sm); } @@ -474,11 +446,11 @@ body { /* 文件名样式 */ .file-name { font-weight: 600; - color: #2c3e50; + color: var(--color-text-primary); display: flex; align-items: center; gap: 8px; - font-size: 14px; + font-size: var(--font-size-sm); } .file-name .file-icon { @@ -489,7 +461,7 @@ body { .file-upload-type { font-size: 11px; - color: #6c757d; + color: var(--color-text-muted); font-weight: 400; margin-top: 2px; display: block; @@ -512,33 +484,33 @@ body { /* 文件类型标签 */ .file-type { - background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%); - color: #7b1fa2; + background: linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%); + color: #6d28d9; padding: var(--spacing-xs) var(--spacing-xs); border-radius: var(--radius-sm); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; - border: 1px solid #ce93d8; + border: 1px solid rgba(109, 40, 217, 0.24); } /* 文件大小 */ .file-size { - color: #6c757d; + color: var(--color-text-secondary); font-weight: 500; font-size: 13px; } /* 过期时间 */ .file-expire { - color: #6c757d; + color: var(--color-text-secondary); font-size: 13px; } /* 下载次数 */ .file-downloads { - color: #28a745; + color: var(--color-success); font-weight: 600; font-size: 14px; } @@ -567,37 +539,34 @@ body { } .file-actions .btn-info { - background: linear-gradient(135deg, #17a2b8 0%, #138496 100%); + background: linear-gradient(135deg, rgba(14, 165, 233, 0.9), rgba(79, 70, 229, 0.95)); color: white; box-shadow: var(--shadow-sm); } .file-actions .btn-info:hover { - background: linear-gradient(135deg, #138496 0%, #117a8b 100%); transform: translateY(-1px); box-shadow: var(--shadow-md); } .file-actions .btn-success { - background: linear-gradient(135deg, #28a745 0%, #20c997 100%); + background: linear-gradient(135deg, rgba(34, 197, 94, 0.95), rgba(16, 185, 129, 0.95)); color: white; box-shadow: var(--shadow-sm); } .file-actions .btn-success:hover { - background: linear-gradient(135deg, #20c997 0%, #1e7e34 100%); transform: translateY(-1px); box-shadow: var(--shadow-md); } .file-actions .btn-danger { - background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); + background: linear-gradient(135deg, rgba(239, 68, 68, 0.95), rgba(190, 18, 60, 0.95)); color: white; box-shadow: var(--shadow-sm); } .file-actions .btn-danger:hover { - background: linear-gradient(135deg, #c82333 0%, #bd2130 100%); transform: translateY(-1px); box-shadow: var(--shadow-md); } @@ -610,23 +579,24 @@ body { gap: var(--spacing-xs); margin-top: var(--spacing-xl); padding: var(--spacing-lg); - background: white; - border-radius: var(--radius-lg); + background: var(--color-surface); + border-radius: var(--radius-xl); box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border); flex-wrap: wrap; } .page-btn { padding: var(--spacing-sm) var(--spacing-md); - border: 2px solid #e9ecef; - background: white; - color: #495057; + border: 1px solid var(--color-border); + background: var(--color-surface); + color: var(--color-text-secondary); border-radius: var(--radius-sm); cursor: pointer; transition: all 0.2s ease; text-decoration: none; font-weight: 500; - font-size: 14px; + font-size: var(--font-size-sm); min-width: 44px; text-align: center; display: flex; @@ -635,8 +605,8 @@ body { } .page-btn:hover:not(:disabled) { - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); - border-color: #ced4da; + background: rgba(102, 126, 234, 0.1); + border-color: var(--primary-color); transform: translateY(-1px); box-shadow: var(--shadow-sm); } @@ -658,14 +628,14 @@ body { .page-ellipsis { padding: var(--spacing-sm) var(--spacing-xs); - color: #6c757d; + color: var(--color-text-muted); font-weight: 500; } .page-info { margin-left: var(--spacing-md); - color: #6c757d; - font-size: 14px; + color: var(--color-text-muted); + font-size: var(--font-size-sm); white-space: nowrap; } @@ -680,10 +650,11 @@ body { .empty-state { text-align: center; padding: var(--spacing-3xl) var(--spacing-lg); - color: #6c757d; - background: white; + color: var(--color-text-secondary); + background: var(--color-surface); border-radius: var(--radius-xl); box-shadow: var(--shadow-lg); + border: 1px solid var(--color-border); margin-top: var(--spacing-lg); } @@ -695,9 +666,9 @@ body { } .empty-state p { - font-size: 16px; + font-size: var(--font-size-md); margin-bottom: var(--spacing-lg); - color: #6c757d; + color: var(--color-text-secondary); } .empty-state .btn { @@ -721,9 +692,9 @@ body { /* API 密钥管理 */ .api-key-form { - background: white; - border: 1px solid #e5e7eb; - border-radius: var(--radius-lg); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); padding: var(--spacing-xl); box-shadow: var(--shadow-sm); margin-bottom: var(--spacing-xl); @@ -732,8 +703,8 @@ body { .api-key-form .form-group small { display: block; margin-top: var(--spacing-xs); - color: #9ca3af; - font-size: 13px; + color: var(--color-text-muted); + font-size: var(--font-size-xs); } .api-key-form-actions { @@ -749,17 +720,17 @@ body { } .api-key-limit { - color: #6b7280; - font-size: 14px; + color: var(--color-text-secondary); + font-size: var(--font-size-sm); display: inline-flex; align-items: center; gap: var(--spacing-xs); - transition: color 0.3s ease, background 0.3s ease; + transition: color var(--transition-base), background var(--transition-base); } .api-key-limit.warning { - color: #b45309; - background: #fef3c7; + color: var(--color-warning); + background: rgba(249, 115, 22, 0.12); padding: 4px 10px; border-radius: var(--radius-sm); font-weight: 600; @@ -768,30 +739,30 @@ body { .api-key-scope-note { flex-basis: 100%; display: block; - color: #2563eb; - font-size: 13px; + color: var(--primary-color); + font-size: var(--font-size-xs); line-height: 1.6; margin-top: 4px; } .api-key-scope-note code { - background: rgba(37, 99, 235, 0.08); - color: #1d4ed8; + background: rgba(102, 126, 234, 0.12); + color: var(--primary-color); padding: 2px 6px; border-radius: 6px; font-size: 12px; } .api-key-scope-note a { - color: #1d4ed8; + color: var(--primary-color); text-decoration: underline; font-weight: 500; } .api-key-result { - background: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: var(--radius-lg); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); padding: var(--spacing-xl); margin-bottom: var(--spacing-xl); box-shadow: var(--shadow-sm); @@ -807,12 +778,12 @@ body { .api-key-result-title { font-weight: 600; - color: #111827; - font-size: 16px; + color: var(--color-text-primary); + font-size: var(--font-size-md); } .api-key-result-text { - color: #4b5563; + color: var(--color-text-secondary); margin-bottom: var(--spacing-sm); line-height: 1.6; } @@ -822,16 +793,16 @@ body { align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); - background: rgba(17, 24, 39, 0.04); + background: rgba(102, 126, 234, 0.08); border-radius: var(--radius-sm); - border: 1px dashed #94a3b8; + border: 1px dashed var(--color-border); flex-wrap: wrap; } .api-key-secret code { font-family: 'SF Mono', 'Cascadia Code', monospace; - font-size: 15px; - color: #1f2937; + font-size: var(--font-size-sm); + color: var(--color-text-primary); flex: 1 1 240px; word-break: break-all; } @@ -842,8 +813,8 @@ body { .api-key-result-meta { margin-top: var(--spacing-sm); - font-size: 13px; - color: #6b7280; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); line-height: 1.6; } @@ -852,38 +823,40 @@ body { overflow-x: auto; } + .api-keys-table { width: 100%; min-width: 720px; border-collapse: collapse; - background: white; + background: var(--color-surface); border-radius: var(--radius-lg); overflow: hidden; - box-shadow: var(--shadow-lg); + box-shadow: var(--shadow-md); + border: 1px solid var(--color-border); } .api-keys-table thead { - background: #f9fafb; + background: rgba(102, 126, 234, 0.08); } .api-keys-table th, .api-keys-table td { padding: var(--spacing-md); text-align: left; - border-bottom: 1px solid #e5e7eb; - font-size: 14px; - color: #374151; + border-bottom: 1px solid var(--color-divider); + font-size: var(--font-size-sm); + color: var(--color-text-secondary); } .api-keys-table th { font-weight: 600; text-transform: none; - color: #1f2937; - font-size: 13px; + color: var(--color-text-primary); + font-size: var(--font-size-xs); } .api-keys-table tbody tr:hover { - background: #f9fafb; + background: rgba(102, 126, 234, 0.08); } .api-keys-table tbody tr:last-child td { @@ -904,7 +877,7 @@ body { } .api-key-status.active { - background: rgba(34, 197, 94, 0.15); + background: rgba(34, 197, 94, 0.18); color: #15803d; } @@ -928,40 +901,55 @@ body { /* 响应式设计 */ @media (max-width: 768px) { - .container { - padding: var(--spacing-sm); + body.theme-dashboard { + padding: var(--spacing-xl) var(--spacing-md); } - - .header { - flex-direction: column; - gap: var(--spacing-md); + + .dashboard-header { + grid-template-columns: 1fr; text-align: center; + row-gap: var(--spacing-lg); + } + + .dashboard-brand { + justify-content: center; } - - .tabs { + + .dashboard-user { + justify-content: center; + } + + .dashboard-actions { + justify-content: center; + width: 100%; + } + + .dashboard-actions .btn { + flex: 1 1 auto; + min-width: 0; + } + + .dashboard-tabs .tabs { flex-direction: column; + gap: var(--spacing-xs); } - + .stats-grid { grid-template-columns: 1fr; } - + .form-row { flex-direction: column; } - + .file-table { font-size: 14px; } - + .file-actions { flex-direction: column; gap: var(--spacing-xs); - } - - .user-actions { - flex-direction: column; - gap: var(--spacing-sm); + align-items: stretch; } .api-key-form { @@ -991,91 +979,4 @@ body { .api-key-secret code { flex: 1 1 auto; } -} - -/* ===== Migrated overrides from dashboard.html inline styles ===== */ -.container { - max-width: 1100px; - margin: 20px auto; - padding: 20px; -} - -.header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 20px; -} - -.logo { - display: flex; - align-items: center; - gap: 10px; - font-weight: 600; -} - -.logo-icon { width: 36px; height: 36px; } - -.user-info { - display: flex; - align-items: center; - gap: 10px; -} - -.user-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - background: #e9ecef; - display: inline-block; -} - -.nav-tabs { margin-top: 20px; } - -.tabs { display: flex; gap: 8px; margin-bottom: 16px; justify-content: flex-start; } - -.tab { - padding: 8px 14px; - border-radius: 6px; - border: 1px solid #e6e6e6; - background: #fff; - cursor: pointer; -} - -.tab.active { background: #007bff; color: #fff; border-color: #007bff; } - -.tab-content { display: none; } -.tab-content.active { display: block; } - -.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 20px; } - -.content-section { - background: #fff; - padding: 16px; - border-radius: 8px; - border: 1px solid #eee; - margin-bottom: 20px; -} - -.section-header { display: flex; align-items: center; justify-content: space-between; } - -.form-row { display: flex; gap: 16px; flex-wrap: wrap; } - -.form-group { display: flex; flex-direction: column; gap: 6px; min-width: 200px; flex: 1; } - -.upload-area { border: 2px dashed #ccc; border-radius: 8px; padding: 24px; text-align: center; cursor: pointer; } - -.btn { padding: 10px 16px; border-radius: 6px; background: #007bff; color: #fff; border: none; cursor: pointer; } -.btn-secondary { background: #6c757d; } - -.progress { height: 8px; background: #f1f1f1; border-radius: 4px; overflow: hidden; margin-top: 8px; } -.progress-fill { height: 100%; background: #007bff; width: 0%; } - -@media (max-width: 600px) { - .form-row { flex-direction: column; } - .tabs { flex-wrap: wrap; } - .user-name { display: none; } - .container { padding: 12px; } -} - -/* End migrated overrides */ \ No newline at end of file +} \ No newline at end of file diff --git a/themes/2025/css/layout.css b/themes/2025/css/layout.css index 382afaf..d5670b1 100644 --- a/themes/2025/css/layout.css +++ b/themes/2025/css/layout.css @@ -1,8 +1,72 @@ /* 布局样式 - 容器、网格、间距等布局相关 */ +/* 页面壳层与容器 */ +.page-shell { + width: min(100%, var(--container-max-width)); + margin: 0 auto; +} + +.surface-card { + background: var(--color-surface); + border-radius: var(--radius-lg); + padding: var(--spacing-xl); + box-shadow: var(--shadow-md); + border: 1px solid var(--color-border); +} + +.surface-card--soft { + background: var(--color-surface-subtle); + box-shadow: none; + border: 1px solid var(--color-divider); +} + +.section-stack { + display: flex; + flex-direction: column; + gap: var(--spacing-2xl); +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-md); + margin-bottom: var(--spacing-md); +} + +.section-title { + font-size: 1.25rem; + font-weight: 600; + color: var(--color-text-primary); + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.section-subtitle { + color: var(--color-text-secondary); + font-size: 0.95rem; + font-weight: 500; + margin-bottom: var(--spacing-sm); +} + +.section-actions { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.section-divider { + border: none; + height: 1px; + width: 100%; + background: var(--color-divider); + margin: var(--spacing-2xl) 0; +} + /* 主页面容器 - 居中设计 */ .container { - background: rgba(255, 255, 255, 0.95); + background: var(--color-surface-subtle); border-radius: var(--radius-xl); padding: var(--spacing-2xl); box-shadow: var(--shadow-xl); @@ -13,22 +77,44 @@ margin: var(--spacing-lg) auto; } -/* 仪表板容器 - 全屏居中设计 */ -.dashboard-container { - background: #f5f5f5; - min-height: 100vh; +.landing-container { + max-width: 720px; + padding: var(--spacing-3xl); display: flex; flex-direction: column; - align-items: center; - justify-content: flex-start; - padding: var(--spacing-lg); + gap: var(--spacing-2xl); +} + +.landing-container .tab-content { + background: var(--color-surface); + border-radius: var(--radius-lg); + padding: var(--spacing-xl); + box-shadow: var(--shadow-md); + border: 1px solid var(--color-border); +} + +.landing-container .tab-content.active { + display: block; +} + +.landing-container .result { + margin-top: 0; +} + +.landing-content.section-stack { + gap: var(--spacing-xl); } -.dashboard-container .container { +form.section-stack { + gap: var(--spacing-lg); +} + +/* 仪表板容器 - 全屏居中设计 */ +body.theme-dashboard .container { background: transparent; box-shadow: none; backdrop-filter: none; - max-width: 1200px; + max-width: var(--container-max-width); width: 100%; padding: 0; margin: 0; diff --git a/themes/2025/css/responsive.css b/themes/2025/css/responsive.css index c5c16b1..cb2d26d 100644 --- a/themes/2025/css/responsive.css +++ b/themes/2025/css/responsive.css @@ -2,11 +2,13 @@ /* 移动端适配 */ @media (max-width: 600px) { - .container { - padding: 20px; - margin: 20px; + .container, + .landing-container { + padding: var(--spacing-xl); + margin: var(--spacing-md); max-width: none; - width: calc(100% - 40px); + width: calc(100% - 2 * var(--spacing-md)); + box-shadow: var(--shadow-lg); } .logo-text { @@ -21,13 +23,13 @@ .user-links { position: static; justify-content: center; - margin-bottom: 20px; + margin-bottom: var(--spacing-md); flex-wrap: wrap; } .tabs { flex-direction: column; - gap: 5px; + gap: var(--spacing-xs); } .tab { @@ -35,7 +37,7 @@ } .upload-area { - padding: 30px 15px; + padding: var(--spacing-xl) var(--spacing-md); } .upload-icon { @@ -43,17 +45,17 @@ } .upload-text { - font-size: 14px; + font-size: 0.95rem; } .form-group { - margin-bottom: 15px; + margin-bottom: var(--spacing-sm); } .btn { width: 100%; - padding: 15px; - font-size: 16px; + padding: var(--spacing-md); + font-size: var(--font-size-md); } .notification { @@ -68,17 +70,17 @@ } .modal-content { - margin: 20px; - padding: 20px; - max-width: calc(100% - 40px); + margin: var(--spacing-md); + padding: var(--spacing-lg); + max-width: calc(100% - 2 * var(--spacing-md)); } .dropdown-content { position: fixed; top: auto; - bottom: 20px; - left: 20px; - right: 20px; + bottom: var(--spacing-lg); + left: var(--spacing-lg); + right: var(--spacing-lg); width: auto; min-width: auto; } @@ -86,9 +88,10 @@ /* 平板端适配 */ @media (min-width: 601px) and (max-width: 768px) { - .container { - max-width: 600px; - padding: 35px; + .container, + .landing-container { + max-width: 640px; + padding: var(--spacing-2xl); } .logo-text { @@ -102,9 +105,10 @@ /* 大屏幕适配 */ @media (min-width: 1200px) { - .container { - max-width: 550px; - padding: 50px; + .container, + .landing-container { + max-width: 720px; + padding: var(--spacing-3xl); } .logo-text { diff --git a/themes/2025/dashboard.html b/themes/2025/dashboard.html index d90e20e..aae343d 100644 --- a/themes/2025/dashboard.html +++ b/themes/2025/dashboard.html @@ -15,123 +15,122 @@ - -
- -
-