界面优化调整,及完善
This commit is contained in:
197
api/swagger.json
197
api/swagger.json
@@ -24,7 +24,7 @@
|
||||
"AdminAuth": []
|
||||
}
|
||||
],
|
||||
"description": "获取系统中所有 API Token 的详详信息(不包含哈希)",
|
||||
"description": "获取系统中所有 API Token 的详细信息(不包含哈希)",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -431,18 +431,29 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"管理员",
|
||||
"配置"
|
||||
"Admin"
|
||||
],
|
||||
"summary": "获取完整配置",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/model.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/config.Config"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
@@ -458,8 +469,7 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"管理员",
|
||||
"配置"
|
||||
"Admin"
|
||||
],
|
||||
"summary": "更新配置",
|
||||
"parameters": [
|
||||
@@ -477,7 +487,19 @@
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"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,30 +999,61 @@
|
||||
"config.Config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apitoken": {
|
||||
"api_token": {
|
||||
"description": "API Token 设置",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/config.APITokenConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
"database": {
|
||||
"description": "数据库设置",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/config.DatabaseConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
"security": {
|
||||
"description": "安全设置",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/config.SecurityConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
"site": {
|
||||
"description": "站点设置",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/config.SiteConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
"storage": {
|
||||
"description": "存储设置",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/config.StorageConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
"upload": {
|
||||
"description": "上传设置",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/config.UploadConfig"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.DatabaseConfig": {
|
||||
"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,10 +1329,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"public.PublicConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api_token": {
|
||||
"public.PublicAPITokenConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
@@ -1234,14 +1337,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"public.PublicConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api_token": {
|
||||
"$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"
|
||||
|
||||
94
doc/config_specification.md
Normal file
94
doc/config_specification.md
Normal file
@@ -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**: 核心字段,用于前端生成固定位数的取件码输入框。
|
||||
46
package-lock.json
generated
46
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
117
src/components/ui/AdminNavBar.vue
Normal file
117
src/components/ui/AdminNavBar.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<nav class="bg-white/95 backdrop-blur-sm border-b border-gray-200 shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<h1 class="text-xl font-bold text-gray-900">文件中转站管理</h1>
|
||||
</div>
|
||||
<div class="hidden md:ml-6 md:flex md:space-x-8">
|
||||
<router-link
|
||||
to="/admin"
|
||||
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
:class="$route.path === '/admin' ? 'border-indigo-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'"
|
||||
>
|
||||
概览
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/batches"
|
||||
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
:class="$route.path.includes('/admin/batches') ? 'border-indigo-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'"
|
||||
>
|
||||
文件管理
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/tokens"
|
||||
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
:class="$route.path.includes('/admin/tokens') ? 'border-indigo-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'"
|
||||
>
|
||||
API 管理
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/config"
|
||||
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
:class="$route.path.includes('/admin/config') ? 'border-indigo-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'"
|
||||
>
|
||||
系统配置
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- 移动端菜单按钮 -->
|
||||
<Button variant="ghost" @click="showMobileMenu = !showMobileMenu" class="md:hidden" size="sm">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" @click="router.push('/')" size="sm">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-1a1 1 0 011-1h2a1 1 0 011 1v1a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
前往前台
|
||||
</Button>
|
||||
<Button variant="destructive" @click="handleLogout" size="sm">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
||||
</svg>
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端菜单 -->
|
||||
<div class="md:hidden" v-if="showMobileMenu">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
<router-link
|
||||
to="/admin"
|
||||
class="block pl-3 pr-4 py-2 text-base font-medium"
|
||||
:class="$route.path === '/admin' ? 'bg-indigo-50 border-indigo-500 text-indigo-700 border-l-4' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-800'"
|
||||
@click="showMobileMenu = false"
|
||||
>
|
||||
概览
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/batches"
|
||||
class="block pl-3 pr-4 py-2 text-base font-medium"
|
||||
:class="$route.path.includes('/admin/batches') ? 'bg-indigo-50 border-indigo-500 text-indigo-700 border-l-4' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-800'"
|
||||
@click="showMobileMenu = false"
|
||||
>
|
||||
文件管理
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/tokens"
|
||||
class="block pl-3 pr-4 py-2 text-base font-medium"
|
||||
:class="$route.path.includes('/admin/tokens') ? 'bg-indigo-50 border-indigo-500 text-indigo-700 border-l-4' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-800'"
|
||||
@click="showMobileMenu = false"
|
||||
>
|
||||
API 管理
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/config"
|
||||
class="block pl-3 pr-4 py-2 text-base font-medium"
|
||||
:class="$route.path.includes('/admin/config') ? 'bg-indigo-50 border-indigo-500 text-indigo-700 border-l-4' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-800'"
|
||||
@click="showMobileMenu = false"
|
||||
>
|
||||
系统配置
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const router = useRouter()
|
||||
const showMobileMenu = ref(false)
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('admin_token')
|
||||
router.push('/admin/login')
|
||||
}
|
||||
</script>
|
||||
71
src/components/ui/NavBar.vue
Normal file
71
src/components/ui/NavBar.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<nav class="bg-white border-b border-gray-200 sticky top-0 z-10">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<!-- 左侧:站点信息 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<router-link to="/" class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold text-gray-900">{{ publicConfig.config.site?.name || '文件中转站' }}</h1>
|
||||
<p v-if="showDescription" class="text-xs text-gray-500">{{ publicConfig.config.site?.description || '安全、便捷的文件暂存服务' }}</p>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:导航链接 -->
|
||||
<div class="flex items-center space-x-1">
|
||||
<router-link to="/">
|
||||
<Button variant="ghost" size="sm" :class="$route.path === '/' ? 'bg-gray-100' : ''">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5l4-4 4 4"></path>
|
||||
</svg>
|
||||
取件
|
||||
</Button>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/upload">
|
||||
<Button variant="ghost" size="sm" :class="$route.path === '/upload' ? 'bg-gray-100' : ''">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
|
||||
</svg>
|
||||
发送
|
||||
</Button>
|
||||
</router-link>
|
||||
|
||||
<!-- 管理员入口 -->
|
||||
<router-link v-if="!isAdminRoute" to="/admin" class="ml-4">
|
||||
<Button variant="outline" size="sm">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
管理
|
||||
</Button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { usePublicConfig } from '@/composables/usePublicConfig'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
defineProps<{
|
||||
showDescription?: boolean
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const publicConfig = usePublicConfig()
|
||||
|
||||
const isAdminRoute = computed(() => route.path.includes('/admin'))
|
||||
</script>
|
||||
28
src/components/ui/input-otp/InputOTP.vue
Normal file
28
src/components/ui/input-otp/InputOTP.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import type { OTPInputEmits, OTPInputProps } from "vue-input-otp"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { useForwardPropsEmits } from "reka-ui"
|
||||
import { OTPInput } from "vue-input-otp"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<OTPInputProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const emits = defineEmits<OTPInputEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OTPInput
|
||||
v-slot="slotProps"
|
||||
v-bind="forwarded"
|
||||
:container-class="cn('flex items-center gap-2 has-disabled:opacity-50', props.class)"
|
||||
data-slot="input-otp"
|
||||
class="disabled:cursor-not-allowed"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</OTPInput>
|
||||
</template>
|
||||
22
src/components/ui/input-otp/InputOTPGroup.vue
Normal file
22
src/components/ui/input-otp/InputOTPGroup.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="input-otp-group"
|
||||
v-bind="forwarded"
|
||||
:class="cn('flex items-center', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
21
src/components/ui/input-otp/InputOTPSeparator.vue
Normal file
21
src/components/ui/input-otp/InputOTPSeparator.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { MinusIcon } from "lucide-vue-next"
|
||||
import { useForwardProps } from "reka-ui"
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const forwarded = useForwardProps(props)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="input-otp-separator"
|
||||
role="separator"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<MinusIcon />
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
32
src/components/ui/input-otp/InputOTPSlot.vue
Normal file
32
src/components/ui/input-otp/InputOTPSlot.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { useForwardProps } from "reka-ui"
|
||||
import { computed } from "vue"
|
||||
import { useVueOTPContext } from "vue-input-otp"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{ index: number, class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
|
||||
const context = useVueOTPContext()
|
||||
|
||||
const slot = computed(() => context?.value.slots[props.index])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-bind="forwarded"
|
||||
data-slot="input-otp-slot"
|
||||
:data-active="slot?.isActive"
|
||||
:class="cn('data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]', props.class)"
|
||||
>
|
||||
{{ slot?.char }}
|
||||
<div v-if="slot?.hasFakeCaret" class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div class="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
4
src/components/ui/input-otp/index.ts
Normal file
4
src/components/ui/input-otp/index.ts
Normal file
@@ -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"
|
||||
142
src/composables/usePublicConfig.ts
Normal file
142
src/composables/usePublicConfig.ts
Normal file
@@ -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<PublicConfig>(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()
|
||||
}
|
||||
19
src/layouts/AdminLayout.vue
Normal file
19
src/layouts/AdminLayout.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="h-screen flex flex-col bg-gray-50">
|
||||
<!-- 固定导航栏 -->
|
||||
<AdminNavBar />
|
||||
|
||||
<!-- 内容区域 - 独立滚动 -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<div class="px-4 py-6 sm:px-0">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AdminNavBar from '@/components/ui/AdminNavBar.vue'
|
||||
</script>
|
||||
@@ -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<AxiosResponse<ApiResponse<PublicConfig>>> => {
|
||||
return api.get('/api/config')
|
||||
},
|
||||
|
||||
// 上传文件
|
||||
uploadFiles: (formData: FormData, config?: any): Promise<AxiosResponse<ApiResponse<UploadResponse>>> => {
|
||||
return api.post('/api/batches', formData, {
|
||||
@@ -203,6 +282,16 @@ export const adminApi = {
|
||||
deleteToken: (tokenId: number): Promise<AxiosResponse<ApiResponse<void>>> => {
|
||||
return api.delete(`/admin/api-tokens/${tokenId}`)
|
||||
},
|
||||
|
||||
// 获取系统配置
|
||||
getConfig: (): Promise<AxiosResponse<SystemConfig>> => {
|
||||
return api.get('/admin/config')
|
||||
},
|
||||
|
||||
// 更新系统配置
|
||||
updateConfig: (config: SystemConfig): Promise<AxiosResponse<ApiResponse<void>>> => {
|
||||
return api.put('/admin/config', config)
|
||||
},
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
@@ -303,4 +392,12 @@ export type {
|
||||
PickupResponse,
|
||||
BatchListResponse,
|
||||
APIToken,
|
||||
SystemConfig,
|
||||
PublicConfig,
|
||||
SiteConfig,
|
||||
UploadConfig,
|
||||
StorageConfig,
|
||||
SecurityConfig,
|
||||
DatabaseConfig,
|
||||
APITokenConfig,
|
||||
}
|
||||
11
src/main.ts
11
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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<NavBar :showDescription="true" />
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="max-w-2xl mx-auto">
|
||||
@@ -224,7 +225,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
|
||||
import NavBar from '@/components/ui/NavBar.vue'
|
||||
// API 和工具导入
|
||||
import { publicApi, utils, type PickupResponse, type FileItem } from '@/lib/api'
|
||||
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
|
||||
<NavBar />
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 页面标题和导航 -->
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-800">文件取件</h1>
|
||||
<p class="text-gray-600 mt-2">使用取件码获取文件</p>
|
||||
</div>
|
||||
<Button variant="outline" @click="router.push('/')">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-1a1 1 0 011-1h2a1 1 0 011 1v1a1 1 0 001 1m-6 0h6"/>
|
||||
</svg>
|
||||
返回首页
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- 取件码输入区域 -->
|
||||
<Card v-if="!batchData" class="border-0 shadow-xl mb-8">
|
||||
@@ -229,6 +216,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import NavBar from '@/components/ui/NavBar.vue'
|
||||
|
||||
// API 和工具导入
|
||||
import { publicApi, utils, type PickupResponse, type FileItem } from '@/lib/api'
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<NavBar />
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- 页面标题 -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">发送文件</h1>
|
||||
<p class="text-gray-600">上传文件或发送文本内容,获取取件码分享给他人</p>
|
||||
</div>
|
||||
|
||||
<!-- 上传类型切换 -->
|
||||
<div class="w-full">
|
||||
<!-- 自定义Tab组件 -->
|
||||
@@ -83,7 +78,7 @@
|
||||
{{ isDragging ? '释放文件到此处' : '点击选择文件或拖拽到此处' }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-400">
|
||||
支持任意格式文件,单文件最大 100MB
|
||||
支持任意格式文件,单文件最大 {{ publicConfig.getFileSizeLimit() }},最多 {{ publicConfig.config.upload?.max_batch_files || 10 }} 个文件
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,11 +191,9 @@
|
||||
<SelectValue placeholder="选择过期时间" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1天后</SelectItem>
|
||||
<SelectItem value="3">3天后</SelectItem>
|
||||
<SelectItem value="7">7天后</SelectItem>
|
||||
<SelectItem value="15">15天后</SelectItem>
|
||||
<SelectItem value="30">30天后</SelectItem>
|
||||
<SelectItem v-for="option in publicConfig.getExpireOptions().filter(opt => opt.value > 0)" :key="option.value" :value="String(option.value)">
|
||||
{{ option.label }}后
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -215,10 +208,9 @@
|
||||
<SelectValue placeholder="选择次数限制" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1次后删除</SelectItem>
|
||||
<SelectItem value="3">3次后删除</SelectItem>
|
||||
<SelectItem value="5">5次后删除</SelectItem>
|
||||
<SelectItem value="10">10次后删除</SelectItem>
|
||||
<SelectItem v-for="option in publicConfig.getDownloadOptions().filter(opt => opt.value > 0)" :key="option.value" :value="String(option.value)">
|
||||
{{ option.label }}后删除
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -348,11 +340,14 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import NavBar from '@/components/ui/NavBar.vue'
|
||||
|
||||
// API 和工具导入
|
||||
import { publicApi, utils, type UploadResponse } from '@/lib/api'
|
||||
import { usePublicConfig } from '@/composables/usePublicConfig'
|
||||
|
||||
const router = useRouter()
|
||||
const publicConfig = usePublicConfig()
|
||||
|
||||
// 响应式数据
|
||||
const activeTab = ref('file')
|
||||
@@ -416,9 +411,14 @@ const addFiles = (files: File[]) => {
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
const oversizedFiles = newFiles.filter(file => file.size > 100 * 1024 * 1024)
|
||||
const oversizedFiles = newFiles.filter(file => !publicConfig.validateFileSize(file))
|
||||
if (oversizedFiles.length > 0) {
|
||||
toast.warning(`${oversizedFiles.length} 个文件超过 100MB 限制`)
|
||||
toast.warning(`${oversizedFiles.length} 个文件超过 ${publicConfig.getFileSizeLimit()} 限制`)
|
||||
}
|
||||
|
||||
// 检查文件数量
|
||||
if (!publicConfig.validateFileCount(selectedFiles.value)) {
|
||||
toast.warning(`最多只能上传 ${publicConfig.config.upload?.max_batch_files || 10} 个文件`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<h1 class="text-xl font-bold text-gray-900">文件中转站管理</h1>
|
||||
</div>
|
||||
<div class="hidden md:ml-6 md:flex md:space-x-8">
|
||||
<router-link
|
||||
to="/admin"
|
||||
class="border-indigo-500 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
:class="$route.path === '/admin' ? 'border-indigo-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'"
|
||||
>
|
||||
概览
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/batches"
|
||||
class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
:class="$route.path.includes('/admin/batches') ? 'border-indigo-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'"
|
||||
>
|
||||
文件管理
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/tokens"
|
||||
class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
:class="$route.path.includes('/admin/tokens') ? 'border-indigo-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'"
|
||||
>
|
||||
API 管理
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<Button variant="outline" @click="router.push('/')" size="sm">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-1a1 1 0 011-1h2a1 1 0 011 1v1a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
前往前台
|
||||
</Button>
|
||||
<Button variant="destructive" @click="handleLogout" size="sm">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
||||
</svg>
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<div class="px-4 py-6 sm:px-0">
|
||||
<AdminLayout>
|
||||
<!-- 页面标题 -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-3xl font-bold text-gray-900">管理概览</h2>
|
||||
@@ -212,7 +158,7 @@
|
||||
<CardDescription>常用的管理功能</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Button @click="router.push('/admin/batches')" variant="outline" class="h-auto p-4 flex-col items-start">
|
||||
<svg class="w-8 h-8 text-blue-600 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2h4l2 2h4a2 2 0 012 2v2M7 7V3a2 2 0 012-2h6a2 2 0 012 2v4"></path>
|
||||
@@ -233,6 +179,17 @@
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button @click="router.push('/admin/config')" variant="outline" class="h-auto p-4 flex-col items-start">
|
||||
<svg class="w-8 h-8 text-orange-600 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<div class="text-left">
|
||||
<div class="font-medium">系统配置</div>
|
||||
<div class="text-sm text-gray-500">管理系统设置和参数</div>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button @click="refreshData" variant="outline" class="h-auto p-4 flex-col items-start">
|
||||
<svg class="w-8 h-8 text-purple-600 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
@@ -245,12 +202,10 @@
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast 组件 -->
|
||||
<Toaster />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -265,6 +220,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue'
|
||||
|
||||
// API 和工具导入
|
||||
import { adminApi, utils, type FileBatch } from '@/lib/api'
|
||||
@@ -313,12 +269,6 @@ const refreshData = async () => {
|
||||
toast.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('admin_token')
|
||||
toast.success('已退出登录')
|
||||
router.push('/admin/login')
|
||||
}
|
||||
|
||||
const getStatusVariant = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
|
||||
@@ -1,56 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<h1 class="text-xl font-bold text-gray-900">文件批次管理</h1>
|
||||
</div>
|
||||
<div class="hidden md:ml-6 md:flex md:space-x-8">
|
||||
<router-link
|
||||
to="/admin"
|
||||
class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
概览
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/batches"
|
||||
class="border-indigo-500 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
文件管理
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/tokens"
|
||||
class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
API 管理
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<Button variant="outline" @click="router.push('/')" size="sm">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-1a1 1 0 011-1h2a1 1 0 011 1v1a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
前往前台
|
||||
</Button>
|
||||
<Button variant="destructive" @click="handleLogout" size="sm">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
||||
</svg>
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<div class="px-4 py-6 sm:px-0">
|
||||
<AdminLayout>
|
||||
<!-- 页面标题和搜索 -->
|
||||
<div class="mb-8">
|
||||
<div class="md:flex md:items-center md:justify-between">
|
||||
@@ -237,8 +186,6 @@
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批次详情对话框 -->
|
||||
<Dialog :open="showDetailDialog" @update:open="showDetailDialog = $event">
|
||||
@@ -308,12 +255,11 @@
|
||||
|
||||
<!-- Toast 组件 -->
|
||||
<Toaster />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
// 组件导入
|
||||
@@ -327,12 +273,11 @@ import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue'
|
||||
|
||||
// API 和工具导入
|
||||
import { adminApi, utils, type FileBatch } from '@/lib/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(true)
|
||||
const batches = ref<FileBatch[]>([])
|
||||
@@ -450,12 +395,6 @@ const deleteBatch = async (batch: FileBatch) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('admin_token')
|
||||
toast.success('已退出登录')
|
||||
router.push('/admin/login')
|
||||
}
|
||||
|
||||
const getStatusVariant = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
|
||||
566
src/views/admin/ConfigManagement.vue
Normal file
566
src/views/admin/ConfigManagement.vue
Normal file
@@ -0,0 +1,566 @@
|
||||
<template>
|
||||
<AdminLayout>
|
||||
<div class="space-y-8">
|
||||
<!-- 页面标题和操作按钮 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">系统配置管理</h1>
|
||||
<p class="text-muted-foreground mt-2">管理系统的全局配置参数</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<Button variant="outline" @click="resetForm" :disabled="loading" class="min-w-20">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
重置
|
||||
</Button>
|
||||
<Button @click="saveConfig" :disabled="loading" class="min-w-24">
|
||||
<span v-if="loading" class="flex items-center">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
保存中...
|
||||
</span>
|
||||
<span v-else class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
保存配置
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置面板 -->
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
<!-- 站点配置 -->
|
||||
<Card class="shadow-sm border-l-4 border-l-blue-500">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9" />
|
||||
</svg>
|
||||
站点配置
|
||||
</CardTitle>
|
||||
<CardDescription>配置站点基本信息和显示内容</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-5">
|
||||
<div>
|
||||
<Label for="siteName" class="text-sm font-medium">站点名称</Label>
|
||||
<Input
|
||||
id="siteName"
|
||||
v-model="config.site.name"
|
||||
placeholder="输入站点名称,例如:文件暂存柜"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="siteDescription" class="text-sm font-medium">站点描述</Label>
|
||||
<Textarea
|
||||
id="siteDescription"
|
||||
v-model="config.site.description"
|
||||
placeholder="输入站点描述,例如:临时文件中转服务"
|
||||
class="mt-2 resize-none"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 上传配置 -->
|
||||
<Card class="shadow-sm border-l-4 border-l-green-500">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
上传配置
|
||||
</CardTitle>
|
||||
<CardDescription>配置文件上传的限制和规则</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-5">
|
||||
<div>
|
||||
<Label for="maxFileSizeMB" class="text-sm font-medium">单文件大小限制 (MB)</Label>
|
||||
<Input
|
||||
id="maxFileSizeMB"
|
||||
v-model.number="config.upload.max_file_size_mb"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="100"
|
||||
class="mt-2"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-1">允许上传的单个文件最大大小</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="maxBatchFiles" class="text-sm font-medium">批次最大文件数</Label>
|
||||
<Input
|
||||
id="maxBatchFiles"
|
||||
v-model.number="config.upload.max_batch_files"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="20"
|
||||
class="mt-2"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-1">单次批量上传允许的最大文件数量</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="maxRetentionDays" class="text-sm font-medium">最大保存天数</Label>
|
||||
<Input
|
||||
id="maxRetentionDays"
|
||||
v-model.number="config.upload.max_retention_days"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="30"
|
||||
class="mt-2"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-1">文件在系统中的最大保存时间</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||
<div>
|
||||
<Label for="requireToken" class="text-base font-medium">强制要求 Token</Label>
|
||||
<p class="text-sm text-muted-foreground mt-1">上传时必须提供 API Token</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="requireToken"
|
||||
v-model="config.upload.require_token"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 安全配置 -->
|
||||
<Card class="shadow-sm border-l-4 border-l-red-500">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
安全配置
|
||||
</CardTitle>
|
||||
<CardDescription>配置系统安全参数和验证规则</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-5">
|
||||
<div>
|
||||
<Label for="pickupCodeLength" class="text-sm font-medium">取件码长度</Label>
|
||||
<Input
|
||||
id="pickupCodeLength"
|
||||
v-model.number="config.security.pickup_code_length"
|
||||
type="number"
|
||||
min="4"
|
||||
max="20"
|
||||
placeholder="6"
|
||||
class="mt-2"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-1">生成取件码的字符长度(4-20位)</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="pickupFailLimit" class="text-sm font-medium">取件失败限制次数</Label>
|
||||
<Input
|
||||
id="pickupFailLimit"
|
||||
v-model.number="config.security.pickup_fail_limit"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="5"
|
||||
class="mt-2"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-1">连续取件失败后锁定的次数阈值</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="adminPassword" class="text-sm font-medium">管理员密码</Label>
|
||||
<Input
|
||||
id="adminPassword"
|
||||
v-model="adminPassword"
|
||||
type="password"
|
||||
placeholder="留空表示不修改密码"
|
||||
class="mt-2"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
留空表示不修改当前密码,输入新密码将更新管理员登录密码
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- API Token 配置 -->
|
||||
<Card class="shadow-sm border-l-4 border-l-purple-500">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
API Token 配置
|
||||
</CardTitle>
|
||||
<CardDescription>配置 API Token 功能和权限控制</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-6">
|
||||
<div class="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||
<div>
|
||||
<Label for="apiTokenEnabled" class="text-base font-medium">启用 API Token</Label>
|
||||
<p class="text-sm text-muted-foreground mt-1">允许使用 API Token 进行身份验证</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="apiTokenEnabled"
|
||||
v-model="config.api_token.enabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5" :class="{ 'opacity-50': !config.api_token.enabled }">
|
||||
<div>
|
||||
<Label for="maxTokens" class="text-sm font-medium">最大 Token 数量</Label>
|
||||
<Input
|
||||
id="maxTokens"
|
||||
v-model.number="config.api_token.max_tokens"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="20"
|
||||
class="mt-2"
|
||||
:disabled="!config.api_token.enabled"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
系统中最多可以创建的 API Token 数量
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between p-4 rounded-lg bg-muted/50">
|
||||
<div>
|
||||
<Label for="allowAdminAPI" class="text-base font-medium">允许管理员权限</Label>
|
||||
<p class="text-sm text-muted-foreground mt-1">允许 Token 访问管理员接口</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="allowAdminAPI"
|
||||
v-model="config.api_token.allow_admin_api"
|
||||
:disabled="!config.api_token.enabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 存储配置 -->
|
||||
<Card class="xl:col-span-2 shadow-sm border-l-4 border-l-orange-500">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
存储配置
|
||||
</CardTitle>
|
||||
<CardDescription>配置文件存储方式和参数</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-6">
|
||||
<div>
|
||||
<Label class="text-sm font-medium">存储类型</Label>
|
||||
<Select v-model="config.storage.type" class="mt-2">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择存储类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="local">本地存储</SelectItem>
|
||||
<SelectItem value="s3">S3 兼容存储</SelectItem>
|
||||
<SelectItem value="webdav">WebDAV</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- 本地存储配置 -->
|
||||
<div v-if="config.storage.type === 'local'" class="space-y-4 border rounded-lg p-6 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20">
|
||||
<h4 class="font-semibold text-lg flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
本地存储配置
|
||||
</h4>
|
||||
<div>
|
||||
<Label for="localPath" class="text-sm font-medium">存储路径</Label>
|
||||
<Input
|
||||
id="localPath"
|
||||
v-model="config.storage.local!.path"
|
||||
placeholder="storage_data"
|
||||
class="mt-2"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-1">相对或绝对路径,用于保存上传的文件</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- S3 存储配置 -->
|
||||
<div v-if="config.storage.type === 's3'" class="space-y-6 border rounded-lg p-6 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20">
|
||||
<h4 class="font-semibold text-lg flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 12l2 2 4-4" />
|
||||
</svg>
|
||||
S3 存储配置
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label for="s3Endpoint" class="text-sm font-medium">Endpoint</Label>
|
||||
<Input
|
||||
id="s3Endpoint"
|
||||
v-model="config.storage.s3!.endpoint"
|
||||
placeholder="s3.amazonaws.com"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="s3Region" class="text-sm font-medium">Region</Label>
|
||||
<Input
|
||||
id="s3Region"
|
||||
v-model="config.storage.s3!.region"
|
||||
placeholder="us-east-1"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="s3Bucket" class="text-sm font-medium">Bucket</Label>
|
||||
<Input
|
||||
id="s3Bucket"
|
||||
v-model="config.storage.s3!.bucket"
|
||||
placeholder="file-relay-bucket"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="s3AccessKey" class="text-sm font-medium">Access Key</Label>
|
||||
<Input
|
||||
id="s3AccessKey"
|
||||
v-model="config.storage.s3!.access_key"
|
||||
placeholder="your-access-key"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<Label for="s3SecretKey" class="text-sm font-medium">Secret Key</Label>
|
||||
<Input
|
||||
id="s3SecretKey"
|
||||
v-model="config.storage.s3!.secret_key"
|
||||
type="password"
|
||||
placeholder="your-secret-key"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<div class="flex items-center justify-between p-4 rounded-lg bg-white/80 dark:bg-gray-900/50">
|
||||
<div>
|
||||
<Label for="s3UseSSL" class="text-base font-medium">使用 SSL</Label>
|
||||
<p class="text-sm text-muted-foreground mt-1">启用 HTTPS 连接加密</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="s3UseSSL"
|
||||
v-model="config.storage.s3!.use_ssl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebDAV 存储配置 -->
|
||||
<div v-if="config.storage.type === 'webdav'" class="space-y-6 border rounded-lg p-6 bg-gradient-to-r from-purple-50 to-violet-50 dark:from-purple-950/20 dark:to-violet-950/20">
|
||||
<h4 class="font-semibold text-lg flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M13 12a1 1 0 11-2 0 1 1 0 012 0z" />
|
||||
</svg>
|
||||
WebDAV 存储配置
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label for="webdavUrl" class="text-sm font-medium">WebDAV URL</Label>
|
||||
<Input
|
||||
id="webdavUrl"
|
||||
v-model="config.storage.webdav!.url"
|
||||
placeholder="https://dav.example.com"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="webdavRoot" class="text-sm font-medium">根目录</Label>
|
||||
<Input
|
||||
id="webdavRoot"
|
||||
v-model="config.storage.webdav!.root"
|
||||
placeholder="/file-relay"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="webdavUsername" class="text-sm font-medium">用户名</Label>
|
||||
<Input
|
||||
id="webdavUsername"
|
||||
v-model="config.storage.webdav!.username"
|
||||
placeholder="user"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="webdavPassword" class="text-sm font-medium">密码</Label>
|
||||
<Input
|
||||
id="webdavPassword"
|
||||
v-model="config.storage.webdav!.password"
|
||||
type="password"
|
||||
placeholder="pass"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- 数据库配置 -->
|
||||
<Card class="xl:col-span-2 shadow-sm border-l-4 border-l-indigo-500">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||
</svg>
|
||||
数据库配置
|
||||
</CardTitle>
|
||||
<CardDescription>配置 SQLite 数据库存储路径</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
<Label for="databasePath" class="text-sm font-medium">数据库文件路径</Label>
|
||||
<Input
|
||||
id="databasePath"
|
||||
v-model="config.database.path"
|
||||
placeholder="file_relay.db"
|
||||
class="mt-2"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground mt-1">SQLite 数据库文件的存储路径</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Toast 组件 -->
|
||||
<Toaster />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { adminApi, type SystemConfig } from '@/lib/api'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue'
|
||||
|
||||
// 使用 toast
|
||||
const showToast = toast
|
||||
|
||||
const loading = ref(false)
|
||||
const adminPassword = ref('')
|
||||
const originalConfig = ref<SystemConfig>()
|
||||
|
||||
const config = reactive<SystemConfig>({
|
||||
site: {
|
||||
name: '',
|
||||
description: ''
|
||||
},
|
||||
upload: {
|
||||
max_file_size_mb: 100,
|
||||
max_batch_files: 20,
|
||||
max_retention_days: 30,
|
||||
require_token: false
|
||||
},
|
||||
storage: {
|
||||
type: 'local',
|
||||
local: {
|
||||
path: 'storage_data'
|
||||
},
|
||||
s3: {
|
||||
endpoint: 's3.amazonaws.com',
|
||||
region: 'us-east-1',
|
||||
bucket: 'file-relay-bucket',
|
||||
access_key: 'your-access-key',
|
||||
secret_key: 'your-secret-key',
|
||||
use_ssl: false
|
||||
},
|
||||
webdav: {
|
||||
url: 'https://dav.example.com',
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
root: '/file-relay'
|
||||
}
|
||||
},
|
||||
security: {
|
||||
pickup_code_length: 6,
|
||||
pickup_fail_limit: 5,
|
||||
admin_password_hash: '',
|
||||
jwt_secret: ''
|
||||
},
|
||||
database: {
|
||||
path: 'file_relay.db'
|
||||
},
|
||||
api_token: {
|
||||
enabled: true,
|
||||
max_tokens: 20,
|
||||
allow_admin_api: false
|
||||
}
|
||||
})
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await adminApi.getConfig()
|
||||
const configData = response.data
|
||||
|
||||
// 直接赋值,保持 snake_case 格式
|
||||
Object.assign(config, configData)
|
||||
|
||||
// 保存原始配置副本
|
||||
originalConfig.value = JSON.parse(JSON.stringify(config))
|
||||
|
||||
console.log('配置加载成功:', config)
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
showToast.error('加载配置失败,请检查网络连接或重新登录')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
if (originalConfig.value) {
|
||||
Object.assign(config, JSON.parse(JSON.stringify(originalConfig.value)))
|
||||
adminPassword.value = ''
|
||||
showToast.success('表单已重置,配置已恢复到最后一次保存的状态')
|
||||
}
|
||||
}
|
||||
|
||||
const saveConfig = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 构建要提交的配置
|
||||
const configToSave = JSON.parse(JSON.stringify(config))
|
||||
|
||||
// 如果设置了新密码,添加到 security 配置中
|
||||
if (adminPassword.value) {
|
||||
configToSave.security.admin_password = adminPassword.value
|
||||
}
|
||||
|
||||
await adminApi.updateConfig(configToSave)
|
||||
|
||||
// 更新原始配置
|
||||
originalConfig.value = JSON.parse(JSON.stringify(config))
|
||||
adminPassword.value = ''
|
||||
|
||||
showToast.success('配置保存成功')
|
||||
} catch (error: any) {
|
||||
console.error('保存配置失败:', error)
|
||||
showToast.error(error.response?.data?.msg || '保存配置失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
</script>
|
||||
@@ -1,56 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<h1 class="text-xl font-bold text-gray-900">API Token 管理</h1>
|
||||
</div>
|
||||
<div class="hidden md:ml-6 md:flex md:space-x-8">
|
||||
<router-link
|
||||
to="/admin"
|
||||
class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
概览
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/batches"
|
||||
class="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
文件管理
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/admin/tokens"
|
||||
class="border-indigo-500 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
API 管理
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<Button variant="outline" @click="router.push('/')" size="sm">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-1a1 1 0 011-1h2a1 1 0 011 1v1a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
前往前台
|
||||
</Button>
|
||||
<Button variant="destructive" @click="handleLogout" size="sm">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
||||
</svg>
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<div class="px-4 py-6 sm:px-0">
|
||||
<AdminLayout>
|
||||
<!-- 页面标题和操作 -->
|
||||
<div class="mb-8">
|
||||
<div class="md:flex md:items-center md:justify-between">
|
||||
@@ -207,8 +156,6 @@
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建 Token 对话框 -->
|
||||
<Dialog :open="showCreateDialog" @update:open="showCreateDialog = $event">
|
||||
@@ -345,12 +292,11 @@
|
||||
|
||||
<!-- Toast 组件 -->
|
||||
<Toaster />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
// 组件导入
|
||||
@@ -364,12 +310,11 @@ import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue'
|
||||
|
||||
// API 和工具导入
|
||||
import { adminApi, utils, type APIToken } from '@/lib/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(true)
|
||||
const tokens = ref<APIToken[]>([])
|
||||
@@ -527,12 +472,6 @@ const refreshData = () => {
|
||||
toast.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('admin_token')
|
||||
toast.success('已退出登录')
|
||||
router.push('/admin/login')
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
return utils.formatDate(dateString)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user