From 657d9d3e377359548c10e32cc1e70be0cf13d1dd Mon Sep 17 00:00:00 2001 From: hxuanyu <2252193204@qq.com> Date: Wed, 14 Jan 2026 16:29:46 +0800 Subject: [PATCH] =?UTF-8?q?=E7=95=8C=E9=9D=A2=E4=BC=98=E5=8C=96=E8=B0=83?= =?UTF-8?q?=E6=95=B4=EF=BC=8C=E5=8F=8A=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/swagger.json | 217 +++++-- doc/config_specification.md | 94 +++ package-lock.json | 46 +- package.json | 1 + src/components/ui/AdminNavBar.vue | 117 ++++ src/components/ui/NavBar.vue | 71 +++ src/components/ui/input-otp/InputOTP.vue | 28 + src/components/ui/input-otp/InputOTPGroup.vue | 22 + .../ui/input-otp/InputOTPSeparator.vue | 21 + src/components/ui/input-otp/InputOTPSlot.vue | 32 + src/components/ui/input-otp/index.ts | 4 + src/composables/usePublicConfig.ts | 142 +++++ src/layouts/AdminLayout.vue | 19 + src/lib/api.ts | 97 +++ src/main.ts | 11 +- src/router/index.ts | 7 + src/views/HomePage.vue | 3 +- src/views/PickupPage.vue | 16 +- src/views/UploadPage.vue | 36 +- src/views/admin/AdminDashboard.vue | 106 +--- src/views/admin/BatchManagement.vue | 97 +-- src/views/admin/ConfigManagement.vue | 566 ++++++++++++++++++ src/views/admin/TokenManagement.vue | 81 +-- 23 files changed, 1528 insertions(+), 306 deletions(-) create mode 100644 doc/config_specification.md create mode 100644 src/components/ui/AdminNavBar.vue create mode 100644 src/components/ui/NavBar.vue create mode 100644 src/components/ui/input-otp/InputOTP.vue create mode 100644 src/components/ui/input-otp/InputOTPGroup.vue create mode 100644 src/components/ui/input-otp/InputOTPSeparator.vue create mode 100644 src/components/ui/input-otp/InputOTPSlot.vue create mode 100644 src/components/ui/input-otp/index.ts create mode 100644 src/composables/usePublicConfig.ts create mode 100644 src/layouts/AdminLayout.vue create mode 100644 src/views/admin/ConfigManagement.vue diff --git a/api/swagger.json b/api/swagger.json index ad4ec81..2f87ab4 100644 --- a/api/swagger.json +++ b/api/swagger.json @@ -24,7 +24,7 @@ "AdminAuth": [] } ], - "description": "获取系统中所有 API Token 的详详信息(不包含哈希)", + "description": "获取系统中所有 API Token 的详细信息(不包含哈希)", "produces": [ "application/json" ], @@ -431,15 +431,26 @@ "application/json" ], "tags": [ - "管理员", - "配置" + "Admin" ], "summary": "获取完整配置", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/config.Config" + "allOf": [ + { + "$ref": "#/definitions/model.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/config.Config" + } + } + } + ] } } } @@ -458,8 +469,7 @@ "application/json" ], "tags": [ - "管理员", - "配置" + "Admin" ], "summary": "更新配置", "parameters": [ @@ -477,7 +487,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/model.Response" + "allOf": [ + { + "$ref": "#/definitions/model.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/config.Config" + } + } + } + ] } }, "400": { @@ -549,7 +571,12 @@ }, "/api/batches": { "post": { - "description": "上传一个或多个文件并创建一个提取批次", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "上传一个或多个文件并创建一个提取批次。如果配置了 require_token,则必须提供带 upload scope 的 API Token。", "consumes": [ "multipart/form-data" ], @@ -629,7 +656,12 @@ }, "/api/batches/text": { "post": { - "description": "中转一段长文本内容并创建一个提取批次", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "中转一段长文本内容并创建一个提取批次。如果配置了 require_token,则必须提供带 upload scope 的 API Token。", "consumes": [ "application/json" ], @@ -687,7 +719,12 @@ }, "/api/batches/{pickup_code}": { "get": { - "description": "根据取件码获取文件批次详详情和文件列表", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "根据取件码获取文件批次详细信息和文件列表。可选提供带 pickup scope 的 API Token。", "produces": [ "application/json" ], @@ -734,7 +771,12 @@ }, "/api/batches/{pickup_code}/download": { "get": { - "description": "根据取件码将批次内的所有文件打包为 ZIP 格式一次性下载", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "根据取件码将批次内的所有文件打包为 ZIP 格式一次性下载。可选提供带 pickup scope 的 API Token。", "produces": [ "application/zip" ], @@ -774,7 +816,7 @@ "application/json" ], "tags": [ - "公共" + "Public" ], "summary": "获取公共配置", "responses": { @@ -801,7 +843,12 @@ }, "/api/files/{file_id}/download": { "get": { - "description": "根据文件 ID 下载单个文件", + "security": [ + { + "APITokenAuth": [] + } + ], + "description": "根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。", "produces": [ "application/octet-stream" ], @@ -935,13 +982,16 @@ "config.APITokenConfig": { "type": "object", "properties": { - "allowAdminAPI": { + "allow_admin_api": { + "description": "是否允许 API Token 访问管理接口", "type": "boolean" }, "enabled": { + "description": "是否启用 API Token", "type": "boolean" }, - "maxTokens": { + "max_tokens": { + "description": "最大 Token 数量", "type": "integer" } } @@ -949,23 +999,53 @@ "config.Config": { "type": "object", "properties": { - "apitoken": { - "$ref": "#/definitions/config.APITokenConfig" + "api_token": { + "description": "API Token 设置", + "allOf": [ + { + "$ref": "#/definitions/config.APITokenConfig" + } + ] }, "database": { - "$ref": "#/definitions/config.DatabaseConfig" + "description": "数据库设置", + "allOf": [ + { + "$ref": "#/definitions/config.DatabaseConfig" + } + ] }, "security": { - "$ref": "#/definitions/config.SecurityConfig" + "description": "安全设置", + "allOf": [ + { + "$ref": "#/definitions/config.SecurityConfig" + } + ] }, "site": { - "$ref": "#/definitions/config.SiteConfig" + "description": "站点设置", + "allOf": [ + { + "$ref": "#/definitions/config.SiteConfig" + } + ] }, "storage": { - "$ref": "#/definitions/config.StorageConfig" + "description": "存储设置", + "allOf": [ + { + "$ref": "#/definitions/config.StorageConfig" + } + ] }, "upload": { - "$ref": "#/definitions/config.UploadConfig" + "description": "上传设置", + "allOf": [ + { + "$ref": "#/definitions/config.UploadConfig" + } + ] } } }, @@ -973,6 +1053,7 @@ "type": "object", "properties": { "path": { + "description": "数据库文件路径", "type": "string" } } @@ -980,16 +1061,20 @@ "config.SecurityConfig": { "type": "object", "properties": { - "adminPasswordHash": { + "admin_password_hash": { + "description": "管理员密码哈希 (bcrypt)", "type": "string" }, - "jwtsecret": { + "jwt_secret": { + "description": "JWT 签名密钥", "type": "string" }, - "pickupCodeLength": { + "pickup_code_length": { + "description": "取件码长度", "type": "integer" }, - "pickupFailLimit": { + "pickup_fail_limit": { + "description": "取件失败尝试限制", "type": "integer" } } @@ -998,9 +1083,11 @@ "type": "object", "properties": { "description": { + "description": "站点描述", "type": "string" }, "name": { + "description": "站点名称", "type": "string" } } @@ -1012,6 +1099,7 @@ "type": "object", "properties": { "path": { + "description": "本地存储路径", "type": "string" } } @@ -1019,42 +1107,53 @@ "s3": { "type": "object", "properties": { - "accessKey": { + "access_key": { + "description": "S3 Access Key", "type": "string" }, "bucket": { + "description": "S3 Bucket", "type": "string" }, "endpoint": { + "description": "S3 端点", "type": "string" }, "region": { + "description": "S3 区域", "type": "string" }, - "secretKey": { + "secret_key": { + "description": "S3 Secret Key", "type": "string" }, - "useSSL": { + "use_ssl": { + "description": "是否使用 SSL", "type": "boolean" } } }, "type": { + "description": "存储类型: local, webdav, s3", "type": "string" }, - "webDAV": { + "webdav": { "type": "object", "properties": { "password": { + "description": "WebDAV 密码", "type": "string" }, "root": { + "description": "WebDAV 根目录", "type": "string" }, "url": { + "description": "WebDAV 地址", "type": "string" }, "username": { + "description": "WebDAV 用户名", "type": "string" } } @@ -1064,14 +1163,21 @@ "config.UploadConfig": { "type": "object", "properties": { - "maxBatchFiles": { + "max_batch_files": { + "description": "每个批次最大文件数", "type": "integer" }, - "maxFileSizeMB": { + "max_file_size_mb": { + "description": "单个文件最大大小 (MB)", "type": "integer" }, - "maxRetentionDays": { + "max_retention_days": { + "description": "最大保留天数", "type": "integer" + }, + "require_token": { + "description": "是否强制要求上传 Token", + "type": "boolean" } } }, @@ -1223,25 +1329,50 @@ } } }, + "public.PublicAPITokenConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, "public.PublicConfig": { "type": "object", "properties": { "api_token": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - } - } + "$ref": "#/definitions/public.PublicAPITokenConfig" + }, + "security": { + "$ref": "#/definitions/public.PublicSecurityConfig" }, "site": { "$ref": "#/definitions/config.SiteConfig" }, + "storage": { + "$ref": "#/definitions/public.PublicStorageConfig" + }, "upload": { "$ref": "#/definitions/config.UploadConfig" } } }, + "public.PublicSecurityConfig": { + "type": "object", + "properties": { + "pickup_code_length": { + "type": "integer" + } + } + }, + "public.PublicStorageConfig": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + } + }, "public.UploadResponse": { "type": "object", "properties": { @@ -1286,8 +1417,14 @@ } }, "securityDefinitions": { + "APITokenAuth": { + "description": "Type \"Bearer \u003cAPI-Token\u003e\" to authenticate. Required scope depends on the endpoint.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, "AdminAuth": { - "description": "Type \"Bearer \u003cyour-jwt-token\u003e\" to authenticate.", + "description": "Type \"Bearer \u003cJWT-Token\u003e\" or \"Bearer \u003cAPI-Token\u003e\" to authenticate. API Token must have 'admin' scope.", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/doc/config_specification.md b/doc/config_specification.md new file mode 100644 index 0000000..a977780 --- /dev/null +++ b/doc/config_specification.md @@ -0,0 +1,94 @@ +# FileRelay 配置项详细说明文档 + +本文档整理了 FileRelay 系统 `config.yaml` 配置文件中各字段的含义、类型及示例,供前端配置页面开发参考。 + +## 1. 站点设置 (site) +用于定义前端展示的站点基本信息。 + +| 字段名 | 类型 | 含义 | 示例 | +| :--- | :--- | :--- | :--- | +| `name` | string | 站点名称,显示在网页标题和页头 | `文件暂存柜` | +| `description` | string | 站点描述,显示在首页或元标签中 | `临时文件中转服务` | + +## 2. 安全设置 (security) +涉及系统鉴权、取件保护相关的核心安全配置。 + +| 字段名 | 类型 | 含义 | 示例 | +| :--- | :--- | :--- | :--- | +| `admin_password_hash` | string | 管理员密码的 bcrypt 哈希值 | `$2a$10$...` | +| `pickup_code_length` | int | 自动生成的取件码长度(已包含在公共配置接口中) | `6` | +| `pickup_fail_limit` | int | 单个 IP 对单个取件码尝试失败的最大次数,超过后将被临时封禁 | `5` | +| `jwt_secret` | string | 用于签发管理端 JWT Token 的密钥,建议设置为复杂随机字符串 | `file-relay-secret` | + +## 3. 上传设置 (upload) +控制文件上传的限制和策略。 + +| 字段名 | 类型 | 含义 | 示例 | +| :--- | :--- | :--- | :--- | +| `max_file_size_mb` | int64 | 单个文件的最大允许大小(单位:MB) | `100` | +| `max_batch_files` | int | 一个取件批次中允许包含的最大文件数量 | `20` | +| `max_retention_days` | int | 文件在服务器上的最长保留天数(针对 time 类型的过期策略) | `30` | +| `require_token` | bool | 是否强制要求提供 API Token 才能进行上传操作 | `false` | + +## 4. 存储设置 (storage) +定义文件的实际物理存储方式。系统支持多种存储后端。 + +| 字段名 | 类型 | 含义 | 示例 | +| :--- | :--- | :--- | :--- | +| `type` | string | 当前激活的存储类型。可选值:`local`, `webdav`, `s3` | `local` | + +### 4.1 本地存储 (local) +当 `type` 为 `local` 时生效。 + +| 字段名 | 类型 | 含义 | 示例 | +| :--- | :--- | :--- | :--- | +| `path` | string | 文件存储在服务器本地的相对或绝对路径 | `storage_data` | + +### 4.2 WebDAV 存储 (webdav) +当 `type` 为 `webdav` 时生效。 + +| 字段名 | 类型 | 含义 | 示例 | +| :--- | :--- | :--- | :--- | +| `url` | string | WebDAV 服务器的 API 地址 | `https://dav.example.com` | +| `username` | string | WebDAV 登录用户名 | `user` | +| `password` | string | WebDAV 登录密码 | `pass` | +| `root` | string | WebDAV 上的基础存储根目录 | `/file-relay` | + +### 4.3 S3 存储 (s3) +当 `type` 为 `s3` 时生效。 + +| 字段名 | 类型 | 含义 | 示例 | +| :--- | :--- | :--- | :--- | +| `endpoint` | string | S3 服务端点 | `s3.amazonaws.com` | +| `region` | string | S3 区域 | `us-east-1` | +| `access_key` | string | S3 Access Key | `your-access-key` | +| `secret_key` | string | S3 Secret Key | `your-secret-key` | +| `bucket` | string | S3 存储桶名称 | `file-relay-bucket` | +| `use_ssl` | bool | 是否强制使用 SSL (HTTPS) 连接 | `false` | + +## 5. API Token 设置 (api_token) +控制系统对外开放的 API Token 管理功能。 + +| 字段名 | 类型 | 含义 | 示例 | +| :--- | :--- | :--- | :--- | +| `enabled` | bool | 是否启用 API Token 功能模块 | `true` | +| `allow_admin_api` | bool | 是否允许具备 `admin` 权限的 API Token 访问管理接口 | `true` | +| `max_tokens` | int | 系统允许创建的 API Token 最大总数限制 | `20` | + +## 6. 数据库设置 (database) +系统元数据存储配置。 + +| 字段名 | 类型 | 含义 | 示例 | +| :--- | :--- | :--- | :--- | +| `path` | string | SQLite 数据库文件的路径 | `file_relay.db` | +84: +85:--- +86: +87:## 附录:公共配置接口 (/api/config) +88: +89:为了方便前端展示和交互约束,系统提供了 `/api/config` 接口,该接口不需要鉴权,返回以下非敏感字段: +90: +91:- **site**: 完整内容(`name`, `description`) +92:- **upload**: 完整内容(`max_file_size_mb`, `max_batch_files`, `max_retention_days`, `require_token`) +93:- **api_token**: 仅包含 `enabled` 开关 +94:- **pickup_code_length**: 核心字段,用于前端生成固定位数的取件码输入框。 diff --git a/package-lock.json b/package-lock.json index 208cef0..e1522ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "vue-vite-shadcn-template", - "version": "0.0.0", + "name": "file-relay-ui", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "vue-vite-shadcn-template", - "version": "0.0.0", + "name": "file-relay-ui", + "version": "1.0.0", "dependencies": { "@tailwindcss/vite": "^4.1.18", "@tanstack/vue-table": "^8.21.3", @@ -19,6 +19,7 @@ "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", "vue": "^3.5.24", + "vue-input-otp": "^0.3.2", "vue-router": "^4.6.4", "vue-sonner": "^2.0.9" }, @@ -2625,6 +2626,43 @@ } } }, + "node_modules/vue-input-otp": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/vue-input-otp/-/vue-input-otp-0.3.2.tgz", + "integrity": "sha512-QMl1842WB6uNAsK4+mZXIskb00TOfahH3AQt8rpRecbtQnOp+oHSUbL/Z3wekfy6pAl+hyN3e1rCUSkCMzbDLQ==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "^12.8.2", + "reka-ui": "^2.6.1" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-input-otp/node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vue-input-otp/node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/vue-router": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", diff --git a/package.json b/package.json index 72ab0f7..68b0bb0 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", "vue": "^3.5.24", + "vue-input-otp": "^0.3.2", "vue-router": "^4.6.4", "vue-sonner": "^2.0.9" }, diff --git a/src/components/ui/AdminNavBar.vue b/src/components/ui/AdminNavBar.vue new file mode 100644 index 0000000..f8ab985 --- /dev/null +++ b/src/components/ui/AdminNavBar.vue @@ -0,0 +1,117 @@ + + + \ No newline at end of file diff --git a/src/components/ui/NavBar.vue b/src/components/ui/NavBar.vue new file mode 100644 index 0000000..7109881 --- /dev/null +++ b/src/components/ui/NavBar.vue @@ -0,0 +1,71 @@ + + + \ No newline at end of file diff --git a/src/components/ui/input-otp/InputOTP.vue b/src/components/ui/input-otp/InputOTP.vue new file mode 100644 index 0000000..f34b734 --- /dev/null +++ b/src/components/ui/input-otp/InputOTP.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/components/ui/input-otp/InputOTPGroup.vue b/src/components/ui/input-otp/InputOTPGroup.vue new file mode 100644 index 0000000..1dea2ab --- /dev/null +++ b/src/components/ui/input-otp/InputOTPGroup.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/ui/input-otp/InputOTPSeparator.vue b/src/components/ui/input-otp/InputOTPSeparator.vue new file mode 100644 index 0000000..7675e25 --- /dev/null +++ b/src/components/ui/input-otp/InputOTPSeparator.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/components/ui/input-otp/InputOTPSlot.vue b/src/components/ui/input-otp/InputOTPSlot.vue new file mode 100644 index 0000000..a165799 --- /dev/null +++ b/src/components/ui/input-otp/InputOTPSlot.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/components/ui/input-otp/index.ts b/src/components/ui/input-otp/index.ts new file mode 100644 index 0000000..d97448a --- /dev/null +++ b/src/components/ui/input-otp/index.ts @@ -0,0 +1,4 @@ +export { default as InputOTP } from "./InputOTP.vue" +export { default as InputOTPGroup } from "./InputOTPGroup.vue" +export { default as InputOTPSeparator } from "./InputOTPSeparator.vue" +export { default as InputOTPSlot } from "./InputOTPSlot.vue" diff --git a/src/composables/usePublicConfig.ts b/src/composables/usePublicConfig.ts new file mode 100644 index 0000000..62bf758 --- /dev/null +++ b/src/composables/usePublicConfig.ts @@ -0,0 +1,142 @@ +import { ref, onMounted } from 'vue' +import { publicApi, type PublicConfig } from '@/lib/api' + +// 默认配置 +const defaultConfig: PublicConfig = { + site: { + name: '文件中转站', + description: '安全、便捷的文件暂存服务' + }, + upload: { + max_file_size_mb: 100, + max_batch_files: 10, + max_retention_days: 30, + require_token: false + }, + security: { + pickup_code_length: 6 + }, + storage: { + type: 'local' + }, + api_token: { + enabled: false + } +} + +// 全局配置状态 +const config = ref(defaultConfig) +const loading = ref(false) + +export function usePublicConfig() { + // 加载公共配置 + const loadConfig = async () => { + try { + loading.value = true + const response = await publicApi.getPublicConfig() + config.value = response.data.data + } catch (error) { + console.error('加载公共配置失败:', error) + // 使用默认配置 + config.value = defaultConfig + } finally { + loading.value = false + } + } + + // 获取上传配置 + const getUploadLimits = () => { + return { + maxFileSizeMB: config.value.upload.max_file_size_mb, + maxFileSize: config.value.upload.max_file_size_mb * 1024 * 1024, // 字节 + maxBatchFiles: config.value.upload.max_batch_files, + maxRetentionDays: config.value.upload.max_retention_days + } + } + + // 生成失效时间选项 + const getExpireOptions = () => { + const maxDays = config.value.upload.max_retention_days + const options = [ + { label: '1小时', value: 1/24 }, + { label: '6小时', value: 6/24 }, + { label: '1天', value: 1 }, + { label: '3天', value: 3 }, + { label: '7天', value: 7 } + ] + + // 只显示不超过最大保存天数的选项 + const validOptions = options.filter(option => option.value <= maxDays) + + // 如果最大天数大于7,添加更多选项 + if (maxDays >= 14) { + validOptions.push({ label: '14天', value: 14 }) + } + if (maxDays >= 30) { + validOptions.push({ label: '30天', value: 30 }) + } + if (maxDays >= 60) { + validOptions.push({ label: '60天', value: 60 }) + } + if (maxDays >= 90) { + validOptions.push({ label: '90天', value: 90 }) + } + + // 如果允许,添加永久选项 + validOptions.push({ label: '永久', value: 0 }) + + return validOptions + } + + // 生成下载次数选项 + const getDownloadOptions = () => { + return [ + { label: '1次', value: 1 }, + { label: '3次', value: 3 }, + { label: '5次', value: 5 }, + { label: '10次', value: 10 }, + { label: '20次', value: 20 }, + { label: '50次', value: 50 }, + { label: '不限制', value: 0 } + ] + } + + // 验证文件大小 + const validateFileSize = (file: File): boolean => { + const limits = getUploadLimits() + return file.size <= limits.maxFileSize + } + + // 验证文件数量 + const validateFileCount = (files: FileList | File[]): boolean => { + const limits = getUploadLimits() + return files.length <= limits.maxBatchFiles + } + + // 格式化文件大小限制文本 + const getFileSizeLimit = (): string => { + const sizeMB = config.value.upload.max_file_size_mb + if (sizeMB >= 1024) { + return `${(sizeMB / 1024).toFixed(1)}GB` + } + return `${sizeMB}MB` + } + + return { + config, + loading, + loadConfig, + getUploadLimits, + getExpireOptions, + getDownloadOptions, + validateFileSize, + validateFileCount, + getFileSizeLimit + } +} + +// 初始化配置(应用启动时调用) +export async function initPublicConfig() { + const { loadConfig } = usePublicConfig() + await loadConfig() +} \ No newline at end of file diff --git a/src/layouts/AdminLayout.vue b/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..655735a --- /dev/null +++ b/src/layouts/AdminLayout.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts index 2f4a3a7..37dcf7f 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -99,8 +99,87 @@ interface APIToken { revoked: boolean } +// 配置相关类型定义 +interface SiteConfig { + name: string + description: string +} + +interface UploadConfig { + max_file_size_mb: number + max_batch_files: number + max_retention_days: number + require_token: boolean +} + +interface StorageConfig { + type: string + local?: { + path: string + } + s3?: { + endpoint: string + region: string + access_key: string + secret_key: string + bucket: string + use_ssl: boolean + } + webdav?: { + url: string + username: string + password: string + root: string + } +} + +interface SecurityConfig { + pickup_code_length: number + pickup_fail_limit: number + admin_password_hash: string + jwt_secret: string +} + +interface DatabaseConfig { + path: string +} + +interface APITokenConfig { + enabled: boolean + max_tokens: number + allow_admin_api: boolean +} + +interface SystemConfig { + site: SiteConfig + upload: UploadConfig + storage: StorageConfig + security: SecurityConfig + database: DatabaseConfig + api_token: APITokenConfig +} + +interface PublicConfig { + site: SiteConfig + upload: UploadConfig + security: { + pickup_code_length: number + } + storage: { + type: string + } + api_token: { + enabled: boolean + } +} + // 公共 API export const publicApi = { + // 获取公共配置 + getPublicConfig: (): Promise>> => { + return api.get('/api/config') + }, + // 上传文件 uploadFiles: (formData: FormData, config?: any): Promise>> => { return api.post('/api/batches', formData, { @@ -203,6 +282,16 @@ export const adminApi = { deleteToken: (tokenId: number): Promise>> => { return api.delete(`/admin/api-tokens/${tokenId}`) }, + + // 获取系统配置 + getConfig: (): Promise> => { + return api.get('/admin/config') + }, + + // 更新系统配置 + updateConfig: (config: SystemConfig): Promise>> => { + return api.put('/admin/config', config) + }, } // 工具函数 @@ -303,4 +392,12 @@ export type { PickupResponse, BatchListResponse, APIToken, + SystemConfig, + PublicConfig, + SiteConfig, + UploadConfig, + StorageConfig, + SecurityConfig, + DatabaseConfig, + APITokenConfig, } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 7b19912..bb90bec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,5 +2,14 @@ import { createApp } from 'vue' import './style.css' import App from './App.vue' import router from './router' +import { initPublicConfig } from '@/composables/usePublicConfig' -createApp(App).use(router).mount('#app') +async function initApp() { + // 初始化公共配置 + await initPublicConfig() + + // 创建应用 + createApp(App).use(router).mount('#app') +} + +initApp() diff --git a/src/router/index.ts b/src/router/index.ts index a5314dc..4e55649 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -10,6 +10,7 @@ const AdminLogin = () => import('@/views/admin/AdminLogin.vue') const AdminDashboard = () => import('@/views/admin/AdminDashboard.vue') const BatchManagement = () => import('@/views/admin/BatchManagement.vue') const TokenManagement = () => import('@/views/admin/TokenManagement.vue') +const ConfigManagement = () => import('@/views/admin/ConfigManagement.vue') // 检查管理员权限 function requireAuth(_to: any, _from: any, next: any) { @@ -66,6 +67,12 @@ const routes = [ name: 'TokenManagement', component: TokenManagement, beforeEnter: requireAuth + }, + { + path: '/admin/config', + name: 'ConfigManagement', + component: ConfigManagement, + beforeEnter: requireAuth } ] diff --git a/src/views/HomePage.vue b/src/views/HomePage.vue index 8e861fd..b6aa705 100644 --- a/src/views/HomePage.vue +++ b/src/views/HomePage.vue @@ -1,5 +1,6 @@