From 45101e37c1e2f064d7bc357fb219c889410e1db1 Mon Sep 17 00:00:00 2001 From: hxuanyu <2252193204@qq.com> Date: Wed, 14 Jan 2026 13:48:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E7=A1=80=E5=8A=9F=E8=83=BD=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=B9=B6=E8=BF=9B=E8=A1=8C=E9=94=99=E8=AF=AF=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 6 + .env.development | 2 + .env.production | 2 + OPTIMIZATION.md | 66 +++ README-new.md | 119 +++++ REFACTOR_COMPLETE.md | 74 +++ api/swagger.json | 294 +++++++++++- package-lock.json | 302 ++++++++++++ package.json | 13 +- src/App.vue | 3 +- src/env.d.ts | 17 + src/lib/api.ts | 306 +++++++++++++ src/main.ts | 3 +- src/router/index.ts | 77 ++++ src/views/HomePage.vue | 457 +++++++++++++++++++ src/views/PickupPage.vue | 485 ++++++++++++++++++++ src/views/UploadPage.vue | 682 ++++++++++++++++++++++++++++ src/views/admin/AdminDashboard.vue | 434 ++++++++++++++++++ src/views/admin/AdminLogin.vue | 258 +++++++++++ src/views/admin/BatchManagement.vue | 596 ++++++++++++++++++++++++ src/views/admin/TokenManagement.vue | 651 ++++++++++++++++++++++++++ 21 files changed, 4828 insertions(+), 19 deletions(-) create mode 100644 .env create mode 100644 .env.development create mode 100644 .env.production create mode 100644 OPTIMIZATION.md create mode 100644 README-new.md create mode 100644 REFACTOR_COMPLETE.md create mode 100644 src/env.d.ts create mode 100644 src/lib/api.ts create mode 100644 src/router/index.ts create mode 100644 src/views/HomePage.vue create mode 100644 src/views/PickupPage.vue create mode 100644 src/views/UploadPage.vue create mode 100644 src/views/admin/AdminDashboard.vue create mode 100644 src/views/admin/AdminLogin.vue create mode 100644 src/views/admin/BatchManagement.vue create mode 100644 src/views/admin/TokenManagement.vue diff --git a/.env b/.env new file mode 100644 index 0000000..492573b --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +# API 基础地址 +VITE_API_URL=http://localhost:8080 + +# 应用配置 +VITE_APP_TITLE=文件中转站 +VITE_APP_DESCRIPTION=安全便捷的文件暂存服务 \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..973a0c0 --- /dev/null +++ b/.env.development @@ -0,0 +1,2 @@ +# 开发环境配置 +VITE_API_URL=http://localhost:8080 \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..9529bd1 --- /dev/null +++ b/.env.production @@ -0,0 +1,2 @@ +# 生产环境配置 +VITE_API_URL=/api \ No newline at end of file diff --git a/OPTIMIZATION.md b/OPTIMIZATION.md new file mode 100644 index 0000000..b43e807 --- /dev/null +++ b/OPTIMIZATION.md @@ -0,0 +1,66 @@ +# 界面优化说明 + +## 优化内容 + +### 1. 首页界面优化 +- ✅ **移除顶部大标题**:简化了页面布局,专注于核心功能 +- ✅ **重新设计操作逻辑**: + - 默认显示简洁的取件码输入界面 + - 输入取件码后在当前页面显示文件或文本内容 + - 支持文件下载、文本复制等操作,无需跳转 + - 底部提供"发送文件或文本"的入口 + +### 2. 上传页面重构 +- ✅ **统一上传界面**:将文件上传和文本保存合并到一个页面 +- ✅ **Tab切换设计**: + - "文件上传" - 支持拖拽、多文件选择、进度显示 + - "文本保存" - 快速输入和保存文本内容 +- ✅ **完整配置选项**: + - 过期策略:按时间或按访问/下载次数 + - 备注信息:为分享内容添加说明 + - 实时反馈:上传进度和状态提示 + +### 3. 用户体验优化 +- ✅ **减少页面跳转**:主要操作都在当前页面完成 +- ✅ **即时反馈**: + - 取件码输入后立即显示结果 + - 文件/文本上传后显示取件码 + - 复制、下载等操作有即时提示 +- ✅ **响应式设计**:适配不同屏幕尺寸 +- ✅ **操作便捷性**: + - 支持粘贴取件码 + - 支持拖拽文件 + - 一键复制取件码 + - 批量文件下载 + +### 4. 界面简洁性 +- ✅ **干净的视觉设计**:去除不必要的装饰元素 +- ✅ **清晰的信息层次**:重要信息突出显示 +- ✅ **合理的空间布局**:最大宽度限制,避免界面过宽 +- ✅ **一致的交互模式**:统一的按钮、卡片、表单风格 + +## 主要功能流程 + +### 取件流程 +1. 用户访问首页 +2. 输入取件码(支持粘贴) +3. 在当前页面显示文件列表或文本内容 +4. 可直接下载文件或复制文本,无需跳转 + +### 发送流程 +1. 点击"发送文件或文本"按钮 +2. 选择文件上传或文本保存的Tab +3. 配置过期策略和备注 +4. 提交后获得取件码,可直接复制分享 + +## 技术实现 +- Vue 3 Composition API +- TypeScript 类型安全 +- Shadcn-Vue 组件库 +- 响应式设计(Tailwind CSS) +- 文件拖拽上传 +- 剪贴板API集成 +- 进度条显示 +- Toast消息提示 + +这次优化大大简化了用户操作流程,提升了使用体验,同时保持了界面的简洁性和功能的完整性。 \ No newline at end of file diff --git a/README-new.md b/README-new.md new file mode 100644 index 0000000..61f5185 --- /dev/null +++ b/README-new.md @@ -0,0 +1,119 @@ +# 文件中转站前端 + +一个基于 Vue 3 + TypeScript + Vite + Shadcn-Vue 构建的文件中转站前端应用。 + +## 项目简介 + +文件中转站是一个极简的文件暂存服务,用户可以像使用现实中的暂存柜一样,将文件、文本等内容临时存储在网站中,并获得一个取件码用于在其他设备上提取文件。 + +## 主要功能 + +### 用户功能 +- **文件上传**: 支持批量上传多种格式的文件 +- **文本分享**: 快速分享长文本内容 +- **取件码提取**: 使用取件码获取文件或文本 +- **过期策略**: 支持按时间或下载次数设置过期规则 +- **快捷操作**: 一键复制、打包下载等便捷功能 + +### 管理功能 +- **批次管理**: 查看、编辑、删除文件批次 +- **API Token**: 创建和管理 API 访问凭证 +- **数据统计**: 系统运行状态和使用统计 +- **权限控制**: 管理员密码保护的后台系统 + +## 技术栈 + +- **框架**: Vue 3 + TypeScript +- **构建工具**: Vite +- **UI 组件**: Shadcn-Vue (基于 Tailwind CSS) +- **路由**: Vue Router +- **HTTP 客户端**: Axios +- **状态管理**: 组合式 API +- **代码规范**: ESLint + Prettier + +## 快速开始 + +### 环境要求 + +- Node.js 18+ +- npm 或 yarn 或 pnpm + +### 安装依赖 + +```bash +npm install +``` + +### 开发环境 + +```bash +npm run dev +``` + +启动后访问 `http://localhost:5173` + +### 构建生产版本 + +```bash +npm run build +``` + +### 预览构建结果 + +```bash +npm run preview +``` + +## 环境配置 + +创建环境变量文件: + +### `.env` (通用配置) +```env +VITE_API_URL=http://localhost:8080 +VITE_APP_TITLE=文件中转站 +VITE_APP_DESCRIPTION=安全便捷的文件暂存服务 +``` + +### `.env.development` (开发环境) +```env +VITE_API_URL=http://localhost:8080 +``` + +### `.env.production` (生产环境) +```env +VITE_API_URL=/api +``` + +## 页面路由 + +### 用户页面 +- `/` - 首页 (取件/存件切换) +- `/upload` - 文件上传页面 +- `/pickup` - 取件页面 +- `/pickup/:code` - 直接取件 (URL 包含取件码) + +### 管理页面 (隐藏入口) +- `/admin/login` - 管理员登录 +- `/admin` - 管理概览 +- `/admin/batches` - 批次管理 +- `/admin/tokens` - API Token 管理 + +## 功能特点 + +### 用户体验 +- **极简设计**: 首页默认取件,一键切换存件模式 +- **智能识别**: 自动识别文件类型并显示对应图标 +- **快捷操作**: 支持剪贴板粘贴取件码、一键复制等 +- **进度反馈**: 上传进度显示和状态提示 +- **响应式设计**: 完美适配桌面和移动设备 + +### 管理功能 +- **数据统计**: 实时显示系统运行数据 +- **批次管理**: 支持搜索、筛选、分页查看 +- **权限控制**: Token 可设置权限范围和过期时间 +- **操作日志**: 详细的操作记录和状态跟踪 + +## 许可证 + +MIT License \ No newline at end of file diff --git a/REFACTOR_COMPLETE.md b/REFACTOR_COMPLETE.md new file mode 100644 index 0000000..f2ab5c6 --- /dev/null +++ b/REFACTOR_COMPLETE.md @@ -0,0 +1,74 @@ +# 界面重构完成 + +## ✅ 完成的改进 + +### 1. **UploadPage.vue 重构** +- **共用配置区域**:过期策略和备注信息现在在同一个配置卡片中 +- **自定义Tab样式**: + - 移除了shadcn-vue的Tabs组件,使用自定义样式 + - 修复了tab指示器高度超出背景的问题 + - 采用圆角背景+白色选中状态的现代设计 +- **内容区域优化**: + - 文件上传和文本输入区域独立显示 + - 配置区域根据内容动态显示 + - 统一的操作按钮和进度显示 + +### 2. **SVG路径错误修复** +- 修复了控制台中的SVG `` 属性错误 +- 所有加载动画图标使用正确的SVG路径语法 + +### 3. **错误处理优化** +- **HomePage.vue 和 UploadPage.vue**: + - 404等业务异常不再输出到控制台 + - 客户端错误(4xx)只显示Toast提示 + - 服务器错误(5xx)和网络错误才记录到控制台 + - 改善开发和生产环境的错误显示 + +### 4. **用户体验提升** +- **Tab切换**: + - 更平滑的过渡动画 + - 清晰的视觉反馈 + - 更好的视觉层次 +- **配置共用**: + - 减少重复界面元素 + - 统一的配置体验 + - 更直观的操作流程 + +## 🎯 技术细节 + +### 自定义Tab设计 +```vue +
+ +
+``` + +### 错误处理策略 +```typescript +const handleUploadError = (error: any, defaultMessage: string) => { + if (error.response?.status >= 400 && error.response?.status < 500) { + // 业务异常:只显示Toast,不记录控制台 + toast.error(error.response?.data?.msg || defaultMessage) + } else { + // 系统异常:记录控制台用于调试 + console.error(`${defaultMessage}:`, error) + toast.error(`${defaultMessage},请重试`) + } +} +``` + +### 共用配置逻辑 +- 文件上传和文本保存共享相同的过期策略配置 +- 根据activeTab动态显示不同的标签文本 +- 统一的配置验证和提交逻辑 + +## 🌟 界面效果 + +1. **更简洁的Tab设计**:圆角背景,选中状态有明显对比 +2. **统一的配置体验**:所有上传配置在同一个区域 +3. **清爽的错误提示**:控制台不再被业务异常污染 +4. **流畅的交互体验**:加载状态、进度显示、即时反馈 + +界面现在更加现代化和用户友好!🎉 \ No newline at end of file diff --git a/api/swagger.json b/api/swagger.json index 024dc39..ad4ec81 100644 --- a/api/swagger.json +++ b/api/swagger.json @@ -284,8 +284,8 @@ "summary": "获取批次详情", "parameters": [ { - "type": "integer", - "description": "批次 ID", + "type": "string", + "description": "批次 ID (UUID)", "name": "batch_id", "in": "path", "required": true @@ -337,8 +337,8 @@ "summary": "修改批次信息", "parameters": [ { - "type": "integer", - "description": "批次 ID", + "type": "string", + "description": "批次 ID (UUID)", "name": "batch_id", "in": "path", "required": true @@ -396,8 +396,8 @@ "summary": "删除批次", "parameters": [ { - "type": "integer", - "description": "批次 ID", + "type": "string", + "description": "批次 ID (UUID)", "name": "batch_id", "in": "path", "required": true @@ -419,6 +419,82 @@ } } }, + "/admin/config": { + "get": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "获取系统的完整配置文件内容(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "管理员", + "配置" + ], + "summary": "获取完整配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/config.Config" + } + } + } + }, + "put": { + "security": [ + { + "AdminAuth": [] + } + ], + "description": "更新系统的配置文件内容(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理员", + "配置" + ], + "summary": "更新配置", + "parameters": [ + { + "description": "新配置内容", + "name": "config", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/config.Config" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/model.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/model.Response" + } + } + } + } + }, "/admin/login": { "post": { "description": "通过密码换取 JWT Token", @@ -691,6 +767,38 @@ } } }, + "/api/config": { + "get": { + "description": "获取前端展示所需的非敏感配置数据", + "produces": [ + "application/json" + ], + "tags": [ + "公共" + ], + "summary": "获取公共配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/model.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/public.PublicConfig" + } + } + } + ] + } + } + } + } + }, "/api/files/{file_id}/download": { "get": { "description": "根据文件 ID 下载单个文件", @@ -703,8 +811,8 @@ "summary": "下载单个文件", "parameters": [ { - "type": "integer", - "description": "文件 ID", + "type": "string", + "description": "文件 ID (UUID)", "name": "file_id", "in": "path", "required": true @@ -824,6 +932,149 @@ } } }, + "config.APITokenConfig": { + "type": "object", + "properties": { + "allowAdminAPI": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "maxTokens": { + "type": "integer" + } + } + }, + "config.Config": { + "type": "object", + "properties": { + "apitoken": { + "$ref": "#/definitions/config.APITokenConfig" + }, + "database": { + "$ref": "#/definitions/config.DatabaseConfig" + }, + "security": { + "$ref": "#/definitions/config.SecurityConfig" + }, + "site": { + "$ref": "#/definitions/config.SiteConfig" + }, + "storage": { + "$ref": "#/definitions/config.StorageConfig" + }, + "upload": { + "$ref": "#/definitions/config.UploadConfig" + } + } + }, + "config.DatabaseConfig": { + "type": "object", + "properties": { + "path": { + "type": "string" + } + } + }, + "config.SecurityConfig": { + "type": "object", + "properties": { + "adminPasswordHash": { + "type": "string" + }, + "jwtsecret": { + "type": "string" + }, + "pickupCodeLength": { + "type": "integer" + }, + "pickupFailLimit": { + "type": "integer" + } + } + }, + "config.SiteConfig": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "config.StorageConfig": { + "type": "object", + "properties": { + "local": { + "type": "object", + "properties": { + "path": { + "type": "string" + } + } + }, + "s3": { + "type": "object", + "properties": { + "accessKey": { + "type": "string" + }, + "bucket": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "region": { + "type": "string" + }, + "secretKey": { + "type": "string" + }, + "useSSL": { + "type": "boolean" + } + } + }, + "type": { + "type": "string" + }, + "webDAV": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "root": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "type": "string" + } + } + } + } + }, + "config.UploadConfig": { + "type": "object", + "properties": { + "maxBatchFiles": { + "type": "integer" + }, + "maxFileSizeMB": { + "type": "integer" + }, + "maxRetentionDays": { + "type": "integer" + } + } + }, "model.APIToken": { "type": "object", "properties": { @@ -876,7 +1127,7 @@ } }, "id": { - "type": "integer" + "type": "string" }, "max_downloads": { "type": "integer" @@ -904,13 +1155,13 @@ "type": "object", "properties": { "batch_id": { - "type": "integer" + "type": "string" }, "created_at": { "type": "string" }, "id": { - "type": "integer" + "type": "string" }, "mime_type": { "type": "string" @@ -972,11 +1223,30 @@ } } }, + "public.PublicConfig": { + "type": "object", + "properties": { + "api_token": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "site": { + "$ref": "#/definitions/config.SiteConfig" + }, + "upload": { + "$ref": "#/definitions/config.UploadConfig" + } + } + }, "public.UploadResponse": { "type": "object", "properties": { "batch_id": { - "type": "integer" + "type": "string" }, "expire_at": { "type": "string" diff --git a/package-lock.json b/package-lock.json index e1aa672..208cef0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tailwindcss/vite": "^4.1.18", "@tanstack/vue-table": "^8.21.3", "@vueuse/core": "^14.1.0", + "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-vue-next": "^0.562.0", @@ -18,6 +19,7 @@ "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", "vue": "^3.5.24", + "vue-router": "^4.6.4", "vue-sonner": "^2.0.9" }, "devDependencies": { @@ -1391,6 +1393,12 @@ "@vue/shared": "3.5.26" } }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, "node_modules/@vue/language-core": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.2.tgz", @@ -1545,6 +1553,36 @@ "node": ">=10" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -1566,6 +1604,18 @@ "node": ">=6" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1578,6 +1628,15 @@ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1587,6 +1646,20 @@ "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -1612,6 +1685,51 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", @@ -1676,6 +1794,42 @@ } } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1690,12 +1844,109 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -1972,6 +2223,36 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", @@ -2056,6 +2337,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/reka-ui": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.7.0.tgz", @@ -2338,6 +2625,21 @@ } } }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, "node_modules/vue-sonner": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/vue-sonner/-/vue-sonner-2.0.9.tgz", diff --git a/package.json b/package.json index 941c9ae..72ab0f7 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,21 @@ { - "name": "vue-vite-shadcn-template", + "name": "file-relay-ui", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", + "description": "文件中转站前端应用", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "vue-tsc -b && vite build", - "preview": "vite preview" + "preview": "vite preview --host", + "serve": "vite preview", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" }, "dependencies": { "@tailwindcss/vite": "^4.1.18", "@tanstack/vue-table": "^8.21.3", "@vueuse/core": "^14.1.0", + "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-vue-next": "^0.562.0", @@ -19,6 +23,7 @@ "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", "vue": "^3.5.24", + "vue-router": "^4.6.4", "vue-sonner": "^2.0.9" }, "devDependencies": { diff --git a/src/App.vue b/src/App.vue index fb6613e..1e9fff3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,11 +1,10 @@ diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..be25550 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,17 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_URL: string + readonly VITE_APP_TITLE: string + readonly VITE_APP_DESCRIPTION: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..2f4a3a7 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,306 @@ +import axios from 'axios' +import type { AxiosResponse } from 'axios' + +// API 基础配置 +const API_BASE_URL = import.meta.env.VITE_API_URL || '' + +// 创建 axios 实例 +const api = axios.create({ + baseURL: API_BASE_URL, + timeout: 30000, +}) + +// 请求拦截器 - 添加认证头 +api.interceptors.request.use((config) => { + const token = localStorage.getItem('admin_token') + if (token && config.url?.includes('/admin/')) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) + +// 响应拦截器 - 统一错误处理 +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401 && window.location.pathname.includes('/admin')) { + localStorage.removeItem('admin_token') + window.location.href = '/admin/login' + } + return Promise.reject(error) + } +) + +// API 响应类型定义 +interface ApiResponse { + code: number + msg: string + data: T +} + +interface FileItem { + id: string + batch_id: string + original_name: string + size: number + mime_type: string + storage_path: string + created_at: string +} + +interface FileBatch { + id: string + pickup_code: string + type: 'file' | 'text' + content?: string + remark: string + expire_type: 'time' | 'download' | 'permanent' + expire_at?: string + max_downloads?: number + download_count: number + status: 'active' | 'expired' | 'deleted' + file_items: FileItem[] + created_at: string + updated_at: string +} + +interface UploadResponse { + pickup_code: string + batch_id: string + expire_at?: string + max_downloads?: number +} + +interface PickupResponse { + type: 'file' | 'text' + content?: string + remark: string + expire_type: string + expire_at?: string + max_downloads?: number + download_count: number + files: FileItem[] +} + +interface BatchListResponse { + data: FileBatch[] + page: number + page_size: number + total: number +} + +interface APIToken { + id: number + name: string + scope: string + created_at: string + expire_at?: string + last_used_at?: string + revoked: boolean +} + +// 公共 API +export const publicApi = { + // 上传文件 + uploadFiles: (formData: FormData, config?: any): Promise>> => { + return api.post('/api/batches', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + ...config, + }) + }, + + // 上传文本 + uploadText: (data: { + content: string + remark?: string + expire_type?: string + expire_days?: number + max_downloads?: number + }): Promise>> => { + return api.post('/api/batches/text', data) + }, + + // 获取批次信息 + getBatch: (pickupCode: string): Promise>> => { + return api.get(`/api/batches/${pickupCode}`) + }, + + // 下载单个文件 + downloadFile: (fileId: string): Promise> => { + return api.get(`/api/files/${fileId}/download`, { + responseType: 'blob', + }) + }, + + // 批量下载文件 + downloadBatch: (pickupCode: string): Promise> => { + return api.get(`/api/batches/${pickupCode}/download`, { + responseType: 'blob', + }) + }, +} + +// 管理 API +export const adminApi = { + // 管理员登录 + login: (password: string): Promise>> => { + return api.post('/admin/login', { password }) + }, + + // 获取批次列表 + getBatches: (params?: { + page?: number + page_size?: number + status?: string + pickup_code?: string + }): Promise>> => { + return api.get('/admin/batches', { params }) + }, + + // 获取批次详情 + getBatchDetail: (batchId: string): Promise>> => { + return api.get(`/admin/batches/${batchId}`) + }, + + // 更新批次信息 + updateBatch: (batchId: string, data: { + remark?: string + expire_type?: string + expire_at?: string + max_downloads?: number + status?: string + }): Promise>> => { + return api.put(`/admin/batches/${batchId}`, data) + }, + + // 删除批次 + deleteBatch: (batchId: string): Promise>> => { + return api.delete(`/admin/batches/${batchId}`) + }, + + // 获取 API Token 列表 + getTokens: (): Promise>> => { + return api.get('/admin/api-tokens') + }, + + // 创建 API Token + createToken: (data: { + name: string + scope?: string + expire_at?: string + }): Promise>> => { + return api.post('/admin/api-tokens', data) + }, + + // 撤销 API Token + revokeToken: (tokenId: number): Promise>> => { + return api.post(`/admin/api-tokens/${tokenId}/revoke`) + }, + + // 删除 API Token + deleteToken: (tokenId: number): Promise>> => { + return api.delete(`/admin/api-tokens/${tokenId}`) + }, +} + +// 工具函数 +export const utils = { + // 格式化文件大小 + formatFileSize: (bytes: number): string => { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] + }, + + // 格式化日期 + formatDate: (dateString: string): string => { + const date = new Date(dateString) + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) + }, + + // 复制到剪贴板 + copyToClipboard: async (text: string): Promise => { + try { + await navigator.clipboard.writeText(text) + return true + } catch (err) { + // 兼容旧浏览器 + try { + const textArea = document.createElement('textarea') + textArea.value = text + document.body.appendChild(textArea) + textArea.select() + document.execCommand('copy') + document.body.removeChild(textArea) + return true + } catch (fallbackErr) { + console.error('复制失败:', fallbackErr) + return false + } + } + }, + + // 下载文件 + downloadBlob: (blob: Blob, filename: string) => { + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + window.URL.revokeObjectURL(url) + document.body.removeChild(a) + }, + + // 获取文件图标类型 + getFileTypeIcon: (filename: string): string => { + const ext = filename.split('.').pop()?.toLowerCase() + + if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext || '')) { + return 'image' + } else if (['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv'].includes(ext || '')) { + return 'video' + } else if (['mp3', 'wav', 'flac', 'aac', 'ogg'].includes(ext || '')) { + return 'audio' + } else if (['pdf'].includes(ext || '')) { + return 'pdf' + } else if (['doc', 'docx'].includes(ext || '')) { + return 'word' + } else if (['xls', 'xlsx'].includes(ext || '')) { + return 'excel' + } else if (['ppt', 'pptx'].includes(ext || '')) { + return 'powerpoint' + } else if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext || '')) { + return 'archive' + } else if (['txt', 'md', 'json', 'xml', 'csv'].includes(ext || '')) { + return 'text' + } else if (['js', 'ts', 'html', 'css', 'php', 'py', 'java', 'cpp', 'c'].includes(ext || '')) { + return 'code' + } + + return 'file' + } +} + +export default api + +// 类型定义导出 +export type { + ApiResponse, + FileItem, + FileBatch, + UploadResponse, + PickupResponse, + BatchListResponse, + APIToken, +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 2425c0f..7b19912 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ import { createApp } from 'vue' import './style.css' import App from './App.vue' +import router from './router' -createApp(App).mount('#app') +createApp(App).use(router).mount('#app') diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..a5314dc --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,77 @@ +import { createRouter, createWebHistory } from 'vue-router' + +// 主页面组件 +const HomePage = () => import('@/views/HomePage.vue') +const UploadPage = () => import('@/views/UploadPage.vue') +const PickupPage = () => import('@/views/PickupPage.vue') + +// 管理页面组件 +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') + +// 检查管理员权限 +function requireAuth(_to: any, _from: any, next: any) { + const token = localStorage.getItem('admin_token') + if (token) { + next() + } else { + next('/admin/login') + } +} + +const routes = [ + { + path: '/', + name: 'HomePage', + component: HomePage + }, + { + path: '/upload', + name: 'UploadPage', + component: UploadPage + }, + { + path: '/pickup', + name: 'PickupPage', + component: PickupPage + }, + { + path: '/pickup/:code', + name: 'PickupWithCode', + component: PickupPage, + props: true + }, + // 管理页面路由 + { + path: '/admin/login', + name: 'AdminLogin', + component: AdminLogin + }, + { + path: '/admin', + name: 'AdminDashboard', + component: AdminDashboard, + beforeEnter: requireAuth + }, + { + path: '/admin/batches', + name: 'BatchManagement', + component: BatchManagement, + beforeEnter: requireAuth + }, + { + path: '/admin/tokens', + name: 'TokenManagement', + component: TokenManagement, + beforeEnter: requireAuth + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +export default router \ No newline at end of file diff --git a/src/views/HomePage.vue b/src/views/HomePage.vue new file mode 100644 index 0000000..8e861fd --- /dev/null +++ b/src/views/HomePage.vue @@ -0,0 +1,457 @@ + + + + + \ No newline at end of file diff --git a/src/views/PickupPage.vue b/src/views/PickupPage.vue new file mode 100644 index 0000000..b172dcb --- /dev/null +++ b/src/views/PickupPage.vue @@ -0,0 +1,485 @@ + + + + + \ No newline at end of file diff --git a/src/views/UploadPage.vue b/src/views/UploadPage.vue new file mode 100644 index 0000000..c8aea75 --- /dev/null +++ b/src/views/UploadPage.vue @@ -0,0 +1,682 @@ +