From 62ac723c958b6ee603c0e8e755d897e127652cb4 Mon Sep 17 00:00:00 2001 From: hanxuanyu <2252193204@qq.com> Date: Wed, 28 Jan 2026 15:35:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=90=8E=E7=AB=AF=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webapp/README.md | 72 ++++ webapp/src/App.vue | 2 + webapp/src/lib/api-service.ts | 3 + webapp/src/lib/api-types.ts | 107 +++--- webapp/src/lib/http-client.ts | 27 +- webapp/src/router/index.ts | 45 ++- webapp/src/views/Admin.vue | 199 ++++++++++ webapp/src/views/AdminConfig.vue | 607 +++++++++++++++++++++++++++++++ webapp/src/views/AdminLogin.vue | 79 ++++ webapp/src/views/AdminTasks.vue | 189 ++++++++++ webapp/src/views/AdminTokens.vue | 236 ++++++++++++ 11 files changed, 1515 insertions(+), 51 deletions(-) create mode 100644 webapp/src/views/Admin.vue create mode 100644 webapp/src/views/AdminConfig.vue create mode 100644 webapp/src/views/AdminLogin.vue create mode 100644 webapp/src/views/AdminTasks.vue create mode 100644 webapp/src/views/AdminTokens.vue diff --git a/webapp/README.md b/webapp/README.md index 070dde5..836c0fa 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -15,6 +15,7 @@ BingPaper 的前端 Web 应用,使用 Vue 3 + TypeScript + Vite 构建。 - ⚡ 浏览器缓存优化(内容哈希 + 代码分割) - 🌐 支持自定义后端路径 - 📁 自动输出到上级目录的 web 文件夹 +- 🔐 完整的管理后台系统(Token 管理、定时任务、系统配置) ## 快速开始 @@ -93,6 +94,77 @@ const images = await bingPaperApi.getImages({ limit: 10 }) ## 项目结构 +``` +src/ +├── assets/ # 静态资源 +├── components/ # Vue 组件 +│ └── ui/ # shadcn-vue UI 组件库 +├── composables/ # 可组合函数 +│ └── useImages.ts # 图片管理相关逻辑 +├── lib/ # 核心库 +│ ├── api-config.ts # API 配置 +│ ├── api-service.ts # API 服务类 +│ ├── api-types.ts # TypeScript 类型定义 +│ ├── http-client.ts # HTTP 客户端 +│ └── utils.ts # 工具函数 +├── router/ # 路由配置 +│ └── index.ts +├── views/ # 页面组件 +│ ├── Home.vue # 首页 +│ ├── ImageView.vue # 图片详情页 +│ ├── ApiDocs.vue # API 文档页 +│ ├── AdminLogin.vue # 管理员登录页 +│ ├── Admin.vue # 管理后台主页面 +│ ├── AdminTokens.vue # Token 管理 +│ ├── AdminTasks.vue # 定时任务管理 +│ └── AdminConfig.vue # 系统配置管理 +├── App.vue # 根组件 +└── main.ts # 入口文件 +``` + +## 管理后台 + +访问 `/admin/login` 进入管理后台登录页面。 + +### 功能模块 + +#### 1. Token 管理 +- 查看所有 API Token +- 创建新的 Token(支持设置过期时间) +- 启用/禁用 Token +- 删除 Token + +#### 2. 定时任务管理 +- 手动触发图片抓取(可指定抓取天数) +- 手动触发旧图片清理 +- 查看任务执行历史 + +#### 3. 系统配置管理 +- **JSON 编辑模式**:直接编辑配置 JSON +- **表单编辑模式**:通过友好的表单界面修改配置 +- 支持的配置项: + - 服务器配置(端口、基础 URL) + - API 模式(本地/重定向) + - 定时任务配置 + - 数据库配置 + - 存储配置(本地/S3/WebDAV) + - 图片保留策略 + - Token 配置 + - 日志配置 + - 功能特性开关 + +#### 4. 密码管理 +- 修改管理员密码 +- 安全退出登录 + +### 安全特性 +- 基于 JWT Token 的身份验证 +- Token 过期自动跳转登录页 +- 路由守卫保护管理页面 +- 密码修改后强制重新登录 + +## 项目结构 + ``` src/ ├── lib/ # 核心库 diff --git a/webapp/src/App.vue b/webapp/src/App.vue index 34be9f6..4593445 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -1,10 +1,12 @@ diff --git a/webapp/src/lib/api-service.ts b/webapp/src/lib/api-service.ts index 022b02c..66dacde 100644 --- a/webapp/src/lib/api-service.ts +++ b/webapp/src/lib/api-service.ts @@ -181,6 +181,9 @@ export class BingPaperApiService { // 导出默认实例 export const bingPaperApi = new BingPaperApiService() +// 为了兼容性,也导出为 apiService +export const apiService = bingPaperApi + // 导出便捷方法 export const { login, diff --git a/webapp/src/lib/api-types.ts b/webapp/src/lib/api-types.ts index c33e288..720a39f 100644 --- a/webapp/src/lib/api-types.ts +++ b/webapp/src/lib/api-types.ts @@ -50,97 +50,97 @@ export interface ChangePasswordRequest { // ===== 配置相关 ===== export interface Config { - admin: AdminConfig - api: APIConfig - cron: CronConfig - db: DBConfig - feature: FeatureConfig - log: LogConfig - retention: RetentionConfig - server: ServerConfig - storage: StorageConfig - token: TokenConfig - web: WebConfig + Server: ServerConfig + Log: LogConfig + API: APIConfig + Cron: CronConfig + Retention: RetentionConfig + DB: DBConfig + Storage: StorageConfig + Admin: AdminConfig + Token: TokenConfig + Feature: FeatureConfig + Web: WebConfig } export interface AdminConfig { - passwordBcrypt: string + PasswordBcrypt: string } export interface APIConfig { - mode: string // 'local' | 'redirect' + Mode: string // 'local' | 'redirect' } export interface CronConfig { - enabled: boolean - dailySpec: string + Enabled: boolean + DailySpec: string } export interface DBConfig { - type: string // 'sqlite' | 'mysql' | 'postgres' - dsn: string + Type: string // 'sqlite' | 'mysql' | 'postgres' + DSN: string } export interface FeatureConfig { - writeDailyFiles: boolean + WriteDailyFiles: boolean } export interface LogConfig { - level: string - filename: string - dbfilename: string - dblogLevel: string - logConsole: boolean - showDBLog: boolean - maxSize: number - maxAge: number - maxBackups: number - compress: boolean + Level: string + Filename: string + DBFilename: string + DBLogLevel: string + LogConsole: boolean + ShowDBLog: boolean + MaxSize: number + MaxAge: number + MaxBackups: number + Compress: boolean } export interface RetentionConfig { - days: number + Days: number } export interface ServerConfig { - port: number - baseURL: string + Port: number + BaseURL: string } export interface StorageConfig { - type: string // 'local' | 's3' | 'webdav' - local: LocalConfig - s3: S3Config - webDAV: WebDAVConfig + Type: string // 'local' | 's3' | 'webdav' + Local: LocalConfig + S3: S3Config + WebDAV: WebDAVConfig } export interface LocalConfig { - root: string + Root: string } export interface S3Config { - endpoint: string - accessKey: string - secretKey: string - bucket: string - region: string - forcePathStyle: boolean - publicURLPrefix: string + Endpoint: string + AccessKey: string + SecretKey: string + Bucket: string + Region: string + ForcePathStyle: boolean + PublicURLPrefix: string } export interface WebDAVConfig { - url: string - username: string - password: string - publicURLPrefix: string + URL: string + Username: string + Password: string + PublicURLPrefix: string } export interface TokenConfig { - defaultTTL: string + DefaultTTL: string } export interface WebConfig { - path: string + Path: string } // ===== 图片相关 ===== @@ -157,9 +157,18 @@ export interface ImageMeta { url?: string variant?: string format?: string + variants?: ImageVariantResp[] // 图片变体列表 [key: string]: any } +export interface ImageVariantResp { + variant: string // 分辨率变体 (UHD, 1920x1080, 等) + format: string // 格式 (jpg) + url: string // 访问 URL + storage_key: string // 存储键 + size: number // 文件大小(字节) +} + export interface ImageListParams extends PaginationParams { page?: number // 页码(从1开始) page_size?: number // 每页数量 diff --git a/webapp/src/lib/http-client.ts b/webapp/src/lib/http-client.ts index 9da9feb..dca3784 100644 --- a/webapp/src/lib/http-client.ts +++ b/webapp/src/lib/http-client.ts @@ -108,11 +108,18 @@ export class ApiClient { // 检查响应状态 if (!response.ok) { const errorData = await this.parseResponse(response) - throw new ApiError( + const apiError = new ApiError( errorData?.message || `HTTP ${response.status}: ${response.statusText}`, response.status, errorData ) + + // 401 未授权错误,自动跳转到登录页 + if (response.status === 401) { + this.handle401Error() + } + + throw apiError } return await this.parseResponse(response) @@ -130,6 +137,24 @@ export class ApiClient { } } + /** + * 处理 401 错误 + */ + private handle401Error() { + // 清除本地存储的 token + localStorage.removeItem('admin_token') + localStorage.removeItem('admin_token_expires') + this.clearAuthToken() + + // 只有在管理页面时才跳转到登录页 + if (typeof window !== 'undefined' && window.location.pathname.startsWith('/admin')) { + // 避免重复跳转 + if (!window.location.pathname.includes('/admin/login')) { + window.location.href = '/admin/login' + } + } + } + /** * 解析响应数据 */ diff --git a/webapp/src/router/index.ts b/webapp/src/router/index.ts index 685df61..72782b8 100644 --- a/webapp/src/router/index.ts +++ b/webapp/src/router/index.ts @@ -2,6 +2,8 @@ import { createRouter, createWebHistory } from 'vue-router' import Home from '@/views/Home.vue' import ImageView from '@/views/ImageView.vue' import ApiDocs from '@/views/ApiDocs.vue' +import AdminLogin from '@/views/AdminLogin.vue' +import Admin from '@/views/Admin.vue' const router = createRouter({ history: createWebHistory(), @@ -29,13 +31,54 @@ const router = createRouter({ meta: { title: 'API 文档' } + }, + { + path: '/admin/login', + name: 'AdminLogin', + component: AdminLogin, + meta: { + title: '管理员登录' + } + }, + { + path: '/admin', + name: 'Admin', + component: Admin, + meta: { + title: '管理后台', + requiresAuth: true + } } ] }) -// 路由守卫 - 更新页面标题 +// 路由守卫 - 更新页面标题和认证检查 router.beforeEach((to, _from, next) => { document.title = (to.meta.title as string) || '必应每日一图' + + // 检查是否需要认证 + if (to.meta.requiresAuth) { + const token = localStorage.getItem('admin_token') + if (!token) { + // 未登录,重定向到登录页 + next('/admin/login') + return + } + + // 检查 token 是否过期 + const expiresAt = localStorage.getItem('admin_token_expires') + if (expiresAt) { + const expireDate = new Date(expiresAt) + if (expireDate < new Date()) { + // token 已过期,清除并重定向到登录页 + localStorage.removeItem('admin_token') + localStorage.removeItem('admin_token_expires') + next('/admin/login') + return + } + } + } + next() }) diff --git a/webapp/src/views/Admin.vue b/webapp/src/views/Admin.vue new file mode 100644 index 0000000..6a02680 --- /dev/null +++ b/webapp/src/views/Admin.vue @@ -0,0 +1,199 @@ + + + diff --git a/webapp/src/views/AdminConfig.vue b/webapp/src/views/AdminConfig.vue new file mode 100644 index 0000000..d0585b1 --- /dev/null +++ b/webapp/src/views/AdminConfig.vue @@ -0,0 +1,607 @@ +