Compare commits

..

11 Commits

Author SHA1 Message Date
8b09394295 Update src/views/user/UserLoginView.vue
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-11 21:58:43 +08:00
82a2acbdbe Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-11 21:58:19 +08:00
2dfb891465 Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-11 21:58:01 +08:00
4f679e8eb6 Update src/api/file/file.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-11 21:57:39 +08:00
67db6e8d53 Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-11 21:57:25 +08:00
d4c2572cf3 Update src/api/file/file.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-11 21:56:49 +08:00
9d18d05a58 feat(api): 新增文件上传相关API及用户邮箱字段
refactor(auth): 使用isApiSuccess统一校验API响应
refactor(store): 更新用户信息获取逻辑以适配新响应格式

chore: 添加eslint和prettier配置及脚本
style: 调整vite代理配置端口号

新增文件上传相关API接口及类型定义
扩展用户信息接口添加邮箱相关字段
统一API响应校验逻辑
更新package.json添加代码格式化工具
2026-01-11 21:50:38 +08:00
d01117c6ea feat(router): 添加根路径重定向到首页的路由配置
添加根路径("/")的重定向配置,使其自动跳转到首页("/home"),并设置该路由不在菜单中显示
2026-01-10 19:37:39 +08:00
40b2ae3126 feat(claude): 添加 Vue3 前端工程师 Agent 并优化项目文档
- 新增 vue3-frontend-engineer.md 专用 Agent,用于处理 Vue 3 相关开发任务
- 重构 CLAUDE.md 文档,改为中英文双语结构
- 优化项目文档结构,新增核心功能模块详细说明
- 完善技术栈、开发命令和项目结构的文档描述

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-07 20:31:46 +08:00
fad13e00e5 feat(auth): 实现用户认证状态持久化及权限控制优化
- 新增 pinia-plugin-persistedstate 实现用户状态本地存储
- 重构 API 响应类型为统一模块管理
- 优化路由守卫逻辑,支持 token 自动登录
- 修复权限检查逻辑错误,调整 AI 聊天页访问权限
- 新增用户信息获取接口及类型定义
2026-01-07 20:14:40 +08:00
8e5558d9a2 feat(auth): 实现认证API和token自动刷新功能
添加认证服务API模块,包括登录、token刷新和验证功能
在axios拦截器中实现token自动刷新机制,处理401错误
更新tsconfig配置以支持ES2020特性
重构API导入路径以使用新的auth模块结构
2026-01-06 22:17:31 +08:00
22 changed files with 5045 additions and 188 deletions

View File

@@ -0,0 +1,117 @@
---
name: vue3-frontend-engineer
description: Use this agent when you need to develop, modify, or review Vue 3 frontend code for the AI OJ project. This includes tasks like: creating new components or views, implementing features using Vue 3 Composition API, working with Pinia stores, integrating Arco Design Vue components, setting up API calls with Axios, configuring routing and permissions, or building the project. Examples: 1) User: 'Add a new problem submission page' → Assistant: 'I'll use the vue3-frontend-engineer agent to implement this feature following the project structure.' 2) User: 'The user authentication isn't persisting' → Assistant: 'Let me use the vue3-frontend-engineer agent to investigate the token storage and Pinia store configuration.' 3) User: 'Refactor this component to use TypeScript properly' → Assistant: 'I'll invoke the vue3-frontend-engineer agent to ensure proper TypeScript typing and Vue 3 best practices.'
model: inherit
color: green
---
You are a Senior Frontend Engineer specializing in Vue 3 ecosystem and frontend engineering best practices. You are the technical expert for the AI OJ By MeowRain project, a Vue 3 + TypeScript Online Judge platform.
## Your Core Expertise
You possess deep knowledge of:
- **Vue 3**: Composition API, `<script setup>`, reactive system, lifecycle hooks, components
- **TypeScript**: Advanced typing, generics, utility types, strict mode compliance
- **Vite**: Build configuration, plugins, optimization, dev server setup
- **Pinia**: State management, stores, actions, getters, persistence plugins
- **Vue Router 4**: Route guards, lazy loading, hash mode, navigation
- **Arco Design Vue**: Component library usage, theming, form validation
- **Axios**: Interceptors, request/response handling, error management
- **SCSS**: Preprocessor usage, modular styles, design tokens
## Project-Specific Knowledge (AI OJ)
You understand the AI OJ project architecture:
**Development Environment**:
- Package manager: `bun` (preferred) or `npm`
- Dev server: Port 3000 with `/api` proxy to `localhost:8085`
- Environment files: `.env.dev`, `.env.prod`, `.env.test`
**Key Technical Patterns**:
- Authentication: Token-based with `access_token` and `refresh_token`
- State persistence: `pinia-plugin-persistedstate` via localStorage
- Permission system: Role-based (NOT_LOGIN, USER, ADMIN) via route `meta.access`
- Path alias: `@` maps to `src/`
- Route guards: `src/access/checkAccess.ts` for permission validation
**Project Structure**:
```
src/
├── access/ # Permission control
├── api/ # API integration (Auth, User, AI Chat)
├── components/ # Global shared components
├── layouts/ # Page layouts (BasicLayout)
├── store/ # Pinia stores
├── views/ # Page views
└── plugins/ # Vue plugins (Axios setup)
```
## Your Working Methodology
When working on tasks:
1. **Code Investigation**: Before making changes, thoroughly examine the existing codebase. Use grep/find to locate related files. Understand current patterns and conventions.
2. **CLAUDE.md Compliance**: Always reference and strictly follow the instructions in CLAUDE.md. These project guidelines override generic best practices.
3. **Consistency**: Match existing code patterns:
- Use Composition API with `<script setup>`
- Follow TypeScript strict typing
- Maintain consistent import ordering
- Use Arco Design Vue components appropriately
- Follow the established folder structure
4. **Quality Standards**:
- Write self-documenting code with clear variable names
- Add TypeScript interfaces/types for all data structures
- Include proper error handling for async operations
- Implement proper loading states and error boundaries
- Ensure accessibility in UI components
- Add comments only when complex logic requires explanation
5. **Best Practices**:
- Prefer composables for reusable logic
- Use Pinia stores for shared state, not props drilling
- Implement proper route guards for protected pages
- Handle token refresh and expiration gracefully
- Optimize bundle size with lazy loading
- Use environment variables for configuration
## When Modifying Code
1. **Locate**: Use search to find all related files. Never assume file locations.
2. **Analyze**: Understand the existing implementation and its dependencies.
3. **Plan**: Consider impacts on other components, stores, routes, and permissions.
4. **Implement**: Make targeted, minimal changes that solve the specific problem.
5. **Verify**: Ensure TypeScript compilation passes (`npm run type-check`)
## When Creating New Features
1. **Place in correct directory**: Components in `src/components/`, views in `src/views/`, stores in `src/store/`
2. **Set up routing**: Add route in router config with appropriate `meta.access`
3. **Create API layer**: Add API functions in `src/api/` if backend interaction needed
4. **Implement state management**: Use Pinia store if state is shared across components
5. **Apply permissions**: Ensure proper access control based on user role
6. **Use Arco components**: Leverage the component library for consistent UI
## Code Review Checklist
Before completing any task, verify:
- [ ] TypeScript types are properly defined (no `any` types)
- [ ] Code follows existing patterns in the codebase
- [ ] CLAUDE.md guidelines are followed
- [ ] Proper error handling is in place
- [ ] Authentication/permission requirements are met
- [ ] Arco Design Vue components are used correctly
- [ ] Code is readable and maintainable
## Communication Style
- Explain your approach before making changes
- Highlight any deviations from existing patterns and why
- Point out potential issues or improvements
- Suggest refactoring opportunities when appropriate
- Ask for clarification when requirements are ambiguous
You are proactive, detail-oriented, and committed to maintaining code quality while delivering features efficiently. Every change you make should improve the codebase or solve a specific user need.

236
CLAUDE.md
View File

@@ -1,161 +1,111 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目简介 (Project Overview)
## Project Overview
这是一个基于 **Vue 3** + **TypeScript** + **Vite** 的 AI 在线判题 (Online Judge) 平台前端项目。
项目采用 **Bun** 作为包管理器,集成 **Arco Design** UI 组件库,实现了完整的用户认证、权限管理和 AI 对话功能。
This is a Vue 3 + TypeScript frontend application for an AI Online Judge (OJ) platform called "AI OJ By MeowRain". The application uses modern Vue 3 patterns with Composition API and includes a comprehensive authentication and permission system.
## 技术栈 (Tech Stack)
## Development Commands
- **运行时/包管理**: [Bun](https://bun.sh/)
- **核心框架**: Vue 3 (Composition API + `<script setup>`)
- **构建工具**: Vite
- **语言**: TypeScript
- **UI 组件库**: Arco Design Vue (`@arco-design/web-vue`)
- **路由**: Vue Router 4 (Hash 模式)
- **状态管理**: Pinia (配合 `pinia-plugin-persistedstate` 实现持久化)
- **HTTP 客户端**: Axios
- **CSS 预处理**: SCSS
### Environment Setup
## 开发常用命令 (Development Commands)
- **Package Manager**: Uses `bun` (evidenced by bun.lock file)
- **Development Server**: `npm run dev` (runs on port 3000, opens browser automatically)
- **Production Development**: `npm run dev:prod` (development mode with production environment config)
本项目使用 **Bun** 进行管理,请确保已安装 Bun。
### Building
- **安装依赖**: `bun install`
- **启动开发服务器**: `bun run dev` (默认端口 3000, 代理 `/api``localhost:8085`)
- **生产环境预览**: `bun run dev:prod`
- **构建生产版本**: `bun run build:prod`
- **构建开发版本**: `bun run build`
- **预览构建产物**: `bun run preview`
- **代码检查 (Lint)**: `bun run lint`
- **类型检查**: `bun run type-check`
- **Development Build**: `npm run build` (builds with development environment)
- **Production Build**: `npm run build:prod` (builds with production environment)
- **Preview**: `npm run preview` (preview built application)
## 项目结构 (Project Structure)
### Code Quality
- **Type Checking**: `npm run type-check` (Vue TypeScript compiler check without emit)
- **Linting**: `npm run lint` (ESLint for .ts and .vue files)
- **Type Generation**: `npm run build:types` (generates TypeScript declaration files)
### Environment Configuration
The application uses environment-specific configuration files:
- `.env.dev` - Development environment (API: <http://localhost:8080>)
- `.env.prod` - Production environment (API: <http://localhost:8080>)
- `.env.test` - Test environment
## Architecture Overview
### Technology Stack
- **Frontend**: Vue 3 with Composition API (`<script setup>`)
- **Language**: TypeScript with strict type checking
- **Build Tool**: Vite (using rolldown-vite@7.2.2)
- **UI Framework**: Arco Design Vue
- **State Management**: Pinia stores
- **Routing**: Vue Router 4 with hash-based routing
- **HTTP Client**: Axios with interceptors
- **CSS**: SCSS support
### Project Structure
你可以使用 命令 tree src /F 在项目根目录下生成项目结构图,以下是主要目录说明:
```
```text
src/
├── access/ # Permission system core
├── api/ # API service layer (currently empty)
├── components/ # Reusable Vue components
├── config/ # Environment configuration
├── layouts/ # Application layout components
├── plugins/ # Vue plugins (Axios configuration)
├── router/ # Route definitions and configuration
├── store/ # Pinia state management
├── types/ # Global TypeScript type definitions
├── utils/ # Utility functions
── views/ # Page components
├── access/ # 权限控制核心逻辑
├── accessEnum.ts # 权限枚举 (NOT_LOGIN, USER, ADMIN)
├── checkAccess.ts # 权限校验函数
│ └── index.ts # 导出入口
├── api/ # 后端 API 接口定义
│ ├── auth/ # 认证相关接口 (登录, 注册, Token)
│ ├── aiChat.ts # AI 对话接口
│ ├── index.ts # API 统一导出
│ └── response.ts # 响应数据类型定义
├── assets/ # 静态资源 (Logo, 图片)
── components/ # 公共组件
│ └── GlobalHeader.vue # 全局顶部导航栏
├── config/ # 全局配置
│ └── index.ts # 环境配置导出
├── layouts/ # 页面布局组件
│ └── BasicLayout.vue # 基础布局 (包含 Header 和 Content)
├── plugins/ # 第三方插件配置
│ └── axios.ts # Axios 实例配置 (拦截器, 错误处理)
├── router/ # 路由配置
│ ├── index.ts # 路由守卫 (权限拦截)
│ └── router.ts # 路由表定义
├── store/ # Pinia 状态管理
│ ├── user.ts # 用户状态 (登录信息, Token)
│ └── types.d.ts # Store 类型定义
├── types/ # 全局 TypeScript 类型
├── utils/ # 工具函数
│ ├── requets.ts # 请求相关工具
│ └── token.ts # Token 存储与获取工具
└── views/ # 页面视图
├── ai/ # AI 相关页面
│ └── AiChatView.vue # AI 对话界面
├── user/ # 用户相关页面
│ ├── UserLoginView.vue # 登录页
│ └── UserRegisterView.vue # 注册页
├── HomeView.vue # 首页
├── AboutView.vue # 关于页
└── NoAuthView.vue # 403 无权限页
```
### Authentication & Permission System
## 核心功能模块 (Core Features)
**Access Levels**: Role-based access control with three levels:
### 1. 认证与用户管理 (Authentication)
- **实现位置**: `src/store/user.ts`, `src/api/auth/`
- **功能**:
- 用户登录/注册
- Token 管理 (Access Token + Refresh Token)
- 登录状态持久化 (Local Storage)
- 自动获取当前登录用户信息
- `NOT_LOGIN`: Public access (default)
- `USER`: Logged-in users
- `ADMIN`: Administrators
### 2. 权限控制 (Access Control / RBAC)
- **实现位置**: `src/access/`, `src/router/index.ts`
- **机制**:
- 定义了三种权限等级: `NOT_LOGIN`, `USER`, `ADMIN`
- 路由配置中通过 `meta.access` 指定所需权限
- 全局路由守卫 (`beforeEach`) 自动拦截未授权访问
- `checkAccess` 函数用于组件内的细粒度权限判断 (如菜单显隐)
**Permission Implementation**:
### 3. 网络请求 (Networking)
- **实现位置**: `src/plugins/axios.ts`
- **特性**:
- 全局拦截器自动附加 `Bearer Token`
- 统一的错误处理与提示
- 开发环境 `/api` 代理配置 (见 `vite.config.ts`)
- Routes include `meta.access` property defining required permission level
- `checkAccess.ts` handles permission validation logic
- Navigation menu filters routes based on user permissions
- Axios interceptors automatically attach Bearer tokens to API requests
### 4. AI 对话 (AI Chat)
- **实现位置**: `src/views/ai/AiChatView.vue`, `src/api/aiChat.ts`
- **功能**:
- 提供与 AI 模型交互的聊天界面
- 支持发送消息并接收流式/普通响应
**User Store** (`src/store/user.ts`):
- Manages authentication state using Pinia
- Stores user login information and tokens
- Currently uses mock data with placeholder `getLoginUser()` method
### Routing Architecture
- **Hash-based routing**: Uses `createWebHashHistory()` for compatibility
- **Route metadata**: Each route includes `hideInMenu` and `access` properties
- **Permission-based navigation**: Menu items filtered by user access level
- **Route guards**: Permission checks before route access
### HTTP Client Configuration
- **Base URL**: Configured via environment variables (`VITE_API_URL`)
- **Interceptors**: Automatic token attachment and response handling
- **Error Handling**: Centralized error handling in `src/plugins/axios.ts`
## Key Patterns and Conventions
### Vue 3 Patterns
- Uses `<script setup>` syntax for all components
- Composition API with reactive refs and computed properties
- TypeScript integration with proper type definitions
### State Management
- Pinia stores for centralized state management
- Type-safe store definitions with TypeScript interfaces
- Reactive state updates with proper reactivity
### Styling
- Arco Design Vue component library for consistent UI
- SCSS support for custom styling
- Global CSS in `src/style.css`
### Path Aliases
- `@` alias points to `src/` directory for clean imports
- Configured in both Vite config and TypeScript config
## Development Notes
### Current Implementation Status
- ✅ Basic Vue 3 + TypeScript setup
- ✅ Arco Design UI integration
- ✅ Permission-based routing system
- ✅ Authentication state management
- ✅ Environment configuration
- ✅ HTTP client with interceptors
- ⚠️ API service layer is empty
- ⚠️ User authentication uses mock data
- ❌ No backend integration yet
- ❌ No testing framework configured
### Known Issues
- Bug in `src/access/checkAccess.ts:24` - should return `true` for USER access
- `getLoginUser()` method in user store is not implemented
- No actual API endpoints implemented in `src/api/`
### Development Workflow
1. Use `npm run dev` for development server
2. Environment variables are loaded automatically based on mode
3. TypeScript strict mode is enabled
4. ESLint configuration for code quality
### Backend Integration
- Configured to connect to `localhost:8080` for API calls
- Uses Bearer token authentication
- Expected to work with a separate backend service
- OpenAPI codegen dependency available for API client generation
## 环境配置 (Configuration)
- `.env.dev`: 开发环境配置
- `.env.prod`: 生产环境配置
- `.env.test`: 测试环境配置
- `vite.config.ts`: Vite 构建配置,包含别名 (`@` -> `src`) 和代理设置

View File

@@ -8,6 +8,7 @@
"@arco-design/web-vue": "^2.57.0",
"axios": "^1.13.2",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"vue": "^3.5.24",
"vue-router": "4",
},
@@ -205,6 +206,8 @@
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
"defu": ["defu@6.1.4", "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
@@ -333,6 +336,8 @@
"pinia": ["pinia@3.0.4", "", { "dependencies": { "@vue/devtools-api": "^7.7.7" }, "peerDependencies": { "typescript": ">=4.5.0", "vue": "^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw=="],
"pinia-plugin-persistedstate": ["pinia-plugin-persistedstate@4.7.1", "https://registry.npmmirror.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.7.1.tgz", { "dependencies": { "defu": "^6.1.4" }, "peerDependencies": { "@nuxt/kit": ">=3.0.0", "@pinia/nuxt": ">=0.10.0", "pinia": ">=3.0.0" }, "optionalPeers": ["@nuxt/kit", "@pinia/nuxt", "pinia"] }, "sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],

4436
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,24 +10,55 @@
"build:prod": "vue-tsc -b && vite build --mode prod",
"preview": "vite preview",
"lint": "eslint . --ext .ts,.vue",
"lint:fix": "eslint . --ext .ts,.vue --fix",
"format": "prettier . --write",
"format:check": "prettier . --check",
"type-check": "vue-tsc --noEmit",
"build:types": "vue-tsc --declaration --emitDeclarationOnly"
},
"eslintConfig": {
"root": true,
"env": {
"browser": true,
"node": true,
"es2022": true
},
"extends": [
"eslint:recommended",
"plugin:vue/vue3-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": "latest",
"sourceType": "module"
}
},
"dependencies": {
"@arco-design/web-vue": "^2.57.0",
"axios": "^1.13.2",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"vue": "^3.5.24",
"vue-router": "4"
},
"devDependencies": {
"@types/node": "^24.10.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-vue": "^9.23.0",
"openapi-typescript-codegen": "^0.29.0",
"prettier": "^3.2.5",
"sass-embedded": "^1.93.3",
"typescript": "~5.9.3",
"vite": "npm:rolldown-vite@7.2.2",
"vue-eslint-parser": "^9.4.3",
"vue-tsc": "^3.1.3"
},
"overrides": {

View File

@@ -20,7 +20,7 @@ const checkAccess = (
}
// 如果用户登录才能访问
if (needAccess === ACCESS_ENUM.USER) {
if (loginUserAccess !== ACCESS_ENUM.NOT_LOGIN) {
if (loginUserAccess === ACCESS_ENUM.NOT_LOGIN) {
return false;
}
}
@@ -31,7 +31,7 @@ const checkAccess = (
return false;
}
}
// 如果说啥都不符合,那还说啥了,直接拒了
return false;
// 权限通过
return true;
};
export default checkAccess;

View File

@@ -5,8 +5,8 @@ import type { LoginUesr } from "../store/types";
import { useUserStore } from "../store/user";
import ACCESS_ENUM from "./accessEnum";
import checkAccess from "./checkAccess";
import { getAccessToken } from "@/utils/token";
const userStore = useUserStore();
/**
* 检查是否需要权限访问
* @param to 要访问的路由
@@ -39,13 +39,18 @@ const redirectWithAccess = (
};
// 这里接收异步函数,是因为下面要调用 userStore.getLoginUser()
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore(); // 必须在守卫内部获取 store确保 pinia 已安装
console.log("登陆用户信息", userStore.loginUser);
let loginUser = userStore.loginUser;
// 如果之前没登陆过,自动登录
if (!loginUser || !loginUser.userRole) {
// 如果之前没登陆过,且有 token尝试自动登录
// 没有 token 时不调用 API避免未登录状态重复弹出错误提示
if (!loginUser || !loginUser.userRole || loginUser.userRole === ACCESS_ENUM.NOT_LOGIN) {
const token = getAccessToken();
if (token) {
await userStore.getLoginUser();
loginUser = userStore.loginUser;
}
}
// 检查是否需要权限访问
const redirectUrl = redirectWithAccess(to, loginUser);

View File

@@ -1,15 +1,11 @@
import request from "@/plugins/axios";
import type { ApiResponse } from "../response";
/**
* API
*/
// 统一响应格式
export interface ApiResponse<T = any> {
success: boolean;
message: string;
data: T;
}
// 登录请求参数
export interface LoginRequest {
@@ -24,7 +20,8 @@ export interface LoginResponse {
unionId: string | null;
accessToken: string;
refreshToken: string;
expire: number | null;
accessTokenExpireTime: number | null;
refreshTokenExpireTime: number | null;
}
// 刷新令牌响应数据
@@ -34,7 +31,8 @@ export interface RefreshTokenResponse {
unionId: string | null;
accessToken: string;
refreshToken: string;
expire: number | null;
accessTokenExpireTime: number | null;
refreshTokenExpireTime: number | null;
}
/**

12
src/api/auth/index.ts Normal file
View File

@@ -0,0 +1,12 @@
export { chatAPI, type ChatMessage, type ChatRequest } from '../aiChat'
// 认证 API
export {
login,
refreshToken,
getAccessToken,
validateToken,
type LoginRequest,
type LoginResponse,
type RefreshTokenResponse,
} from './auth'

80
src/api/auth/user.ts Normal file
View File

@@ -0,0 +1,80 @@
import request from "@/plugins/axios";
import type { ApiResponse } from "../response";
/**
*
*/
/**
* 用户信息
*/
export interface UserInfo {
/**
* id
*/
id: number;
/**
* 用户账号
*/
userAccount: string;
/**
* 开放平台id
*/
unionId?: string;
/**
* 公众号openId
*/
mpOpenId?: string;
/**
* 用户昵称
*/
userName: string;
/**
* 用户头像
*/
userAvatar?: string;
/**
* 用户邮箱
*/
userEmail?: string;
/**
* 用户邮箱是否验证
*/
userEmailVerified?: boolean;
/**
* 用户简介
*/
userProfile?: string;
/**
* 用户角色user/admin/ban
*/
userRole: 'user' | 'admin' | 'ban';
/**
* 创建时间
*/
createTime: string;
/**
* 更新时间
*/
updateTime: string;
}
/**
* 获取当前登录用户信息
* @returns 当前登录用户信息
*/
export const getUserInfoByToken = (): Promise<ApiResponse<UserInfo>> => {
return request({
url: "/v1/auth/getUserInfo",
method: "GET",
})
}

121
src/api/file/file.ts Normal file
View File

@@ -0,0 +1,121 @@
import request from "@/plugins/axios";
/**
* 分页查询文件列表
*/
export const getFileList = async(current: number,size: number) =>{
return request({
url: "/v1/file/page",
method: "GET",
params: {
current,
size,
},
});
}
/**
* 根据ID查询文件详情
* @param id
* @returns
*/
export const getFileById = async (id: number) => {
return request({
url: "/v1/file/" + id,
method: "GET",
});
}
/**
* 检查文件是否已经上传,本地计算文件哈希
* @param hash
* @returns
*/
export const checkHash = async (hash: string) => {
return request({
url: "/v1/file/check",
method: "GET",
params: {
hash,
},
});
};
/**
* 上传文件
* @param file
* @returns
*/
export const uploadFile = async (file: File, hash: string) => {
const formData = new FormData();
formData.append("file", file);
formData.append("hash", hash);
return request({
url: "/v1/file/upload",
method: "POST",
data: formData,
});
}
/**
* 删除文件
* @param id
* @returns
*/
export const deleteFile = async (id: number) => {
return request({
url: "/v1/file/" + id,
method: "DELETE",
});
}
export interface FileListItem {
/* 文件记录的唯一标识 ID */
id: number;
/* 文件的原始名称(不包含扩展名) */
fileName: string;
/* 文件扩展名,例如 "png"、"jpg"、"pdf" 等 */
fileExtension: string;
/* 文件大小单位为字节Byte */
fileSize: number;
/* 文件内容的哈希值(例如 MD5 或 SHA-256用于校验与秒传 */
fileHash: string;
/* 文件的 MIME 类型,例如 "image/png"、"application/pdf" 等 */
mimeType: string;
/* 文件存储方式类型,例如本地存储、对象存储等 */
storageType: string;
/* 文件在存储系统中的物理或逻辑路径 */
storagePath: string;
/* 业务类型标识,用于区分文件所属的业务场景 */
businessType: string;
/* 关联的业务主键 ID例如订单 ID、用户资料 ID 等 */
businessId: number;
/* 上传该文件的用户 ID */
userId: number;
/* 图片文件的附加信息,例如宽高、分辨率等(非图片可为空) */
imageInfo: string;
/* 逻辑删除标记0 表示未删除1 表示已删除 */
isDeleted: number;
/* 记录创建时间信息 */
createdAt: Record<string, unknown>;
/* 记录最近一次更新时间信息 */
updatedAt: Record<string, unknown>;
}
export type FileListArray = FileListItem[];

View File

@@ -1,13 +1,3 @@
export { chatAPI, type ChatMessage, type ChatRequest } from './aiChat'
// 认证 API
export {
login,
refreshToken,
getAccessToken,
validateToken,
type LoginRequest,
type LoginResponse,
type RefreshTokenResponse,
type ApiResponse,
} from './auth'
} from './response'

11
src/api/response.ts Normal file
View File

@@ -0,0 +1,11 @@
// 统一响应格式
export interface ApiResponse<T = any> {
code?: string | number;
success?: boolean;
message?: string | null;
data: T;
}
export const isApiSuccess = (response: ApiResponse<unknown>): boolean => {
return response?.success === true || response?.code === 0 || response?.code === "0";
};

View File

@@ -4,11 +4,15 @@ import "@arco-design/web-vue/dist/arco.css";
import "./style.css";
import App from "./App.vue";
import router from "./router/router";
import {createPinia} from "pinia";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import "./access";
const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
app.use(pinia);
app.use(router);
app.use(ArcoVue);
app.use(pinia)
app.mount("#app");

View File

@@ -3,7 +3,19 @@ import axios, {
type InternalAxiosRequestConfig,
} from "axios";
import { ENV } from "../config";
import { getAccessToken } from "@/utils/token";
import {
getAccessToken,
getRefreshToken,
setTokens,
clearTokens,
} from "@/utils/token";
import { Message } from "@arco-design/web-vue";
import { isApiSuccess } from "@/api/response";
// 是否正在刷新 token
let isRefreshing = false;
// 存储因为 token 过期而挂起的请求
let requests: ((token: string) => void)[] = [];
const request: AxiosInstance = axios.create({
baseURL: ENV.API_BASE_URL,
@@ -32,17 +44,83 @@ request.interceptors.request.use(
// =======================
request.interceptors.response.use(
(response) => {
console.log("响应: ", response);
// console.log("响应: ", response);
const data = response.data;
/**TODO: 增加响应码处理 */
return data;
},
(error) => {
async (error) => {
const originalRequest = error.config;
// 处理 401 未授权情况 (Token 过期)
// 确保不是刷新 token 的请求本身 (避免死循环)
if (
error.response?.status === 401 &&
!originalRequest._retry &&
!originalRequest.url.includes("/auth/refresh")
) {
// 如果正在刷新,将当前请求加入队列等待
if (isRefreshing) {
return new Promise((resolve) => {
requests.push((token: string) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(request(originalRequest));
});
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = getRefreshToken();
if (!refreshToken) {
throw new Error("No refresh token available");
}
// 使用原生 axios 发送刷新请求,避免拦截器循环
const response = await axios.post(
`${ENV.API_BASE_URL}/v1/auth/refresh`,
null,
{
params: { refreshToken },
}
);
if (isApiSuccess(response.data)) {
const { accessToken, refreshToken: newRefreshToken } =
response.data.data;
// 更新本地存储
setTokens(accessToken, newRefreshToken);
// 执行队列中的请求
requests.forEach((cb) => cb(accessToken));
requests = [];
// 重试当前请求
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return request(originalRequest);
} else {
throw new Error("Refresh token failed");
}
} catch (refreshError) {
console.error("Token 刷新失败:", refreshError);
// 清除过期 token
clearTokens();
Message.error("登录已过期,请重新登录");
// 这里可以选择跳转到登录页,例如 window.location.href = '/user/login'
// 建议让路由守卫或者页面自行处理未登录状态
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
/**
* Promise.reject(error) 用来 返回一个状态为 rejected 的 Promise相当于“主动抛出错误”
*
* 让调用者能够在 .catch() 或 try/catch 中捕获这个错误。
它在 异步流程、拦截器、错误处理 中非常常见。
* 它在 异步流程、拦截器、错误处理 中非常常见。
*/
console.error("❌ 网络错误:", error);
return Promise.reject(error);

View File

@@ -9,6 +9,12 @@ import AiChatView from "../views/ai/AiChatView.vue";
* 路由配置
*/
export const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "root",
redirect: "/home",
meta: { hideInMenu: true },
},
{
path: "/home",
name: "HomeView",
@@ -25,7 +31,7 @@ export const routes: Array<RouteRecordRaw> = [
path: "/ai/chat",
name: "AiChatView",
component: AiChatView,
meta: { hideInMenu: false, access: ACCESS_ENUM.NOT_LOGIN },
meta: { hideInMenu: false, access: ACCESS_ENUM.USER },
},
{
path: "/user/login",

View File

@@ -1,6 +1,8 @@
import { defineStore } from "pinia";
import ACCESS_ENUM from "../access/accessEnum";
import type { LoginUesr } from "../store/types";
import { getUserInfoByToken } from "@/api/auth/user";
import { isApiSuccess } from "@/api/response";
/**
*
*/
@@ -11,13 +13,21 @@ export const useUserStore = defineStore("user", {
userRole: ACCESS_ENUM.NOT_LOGIN,
} as LoginUesr,
}),
persist: {
key: "user-store",
storage: localStorage,
pick: ["loginUser"], // 只持久化 loginUser
},
actions: {
// 获取登录用户
async getLoginUser() {
try {
// 从后端获取当前登录用户信息
}catch(e) {
const res = await getUserInfoByToken();
console.log("获取登录用户成功", res);
if (isApiSuccess(res)) {
this.updateUserLoginStatus(res.data);
}
} catch (e) {
console.error("获取登录用户失败", e);
// 网络错误情况也视为未登录
this.loginUser = {

View File

@@ -52,7 +52,7 @@
import { ref, nextTick, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconRobot, IconUser, IconSend } from '@arco-design/web-vue/es/icon'
import { chatAPI, type ChatMessage as APIChatMessage } from '@/api'
import { chatAPI, type ChatMessage as APIChatMessage } from '@/api/auth'
interface ChatMessage {
role: 'user' | 'assistant'

View File

@@ -74,10 +74,11 @@ import { reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { Message } from "@arco-design/web-vue";
import { IconUser, IconLock } from "@arco-design/web-vue/es/icon";
import { login } from "@/api/auth";
import { login } from "@/api/auth/auth";
import { setTokens } from "@/utils/token";
import { useUserStore } from "@/store/user";
import ACCESS_ENUM from "@/access/accessEnum";
import { isApiSuccess } from "@/api/response";
const router = useRouter();
const userStore = useUserStore();
@@ -114,7 +115,7 @@ const handleSubmit = async (data: any) => {
userPassword: form.userPassword,
});
if (response.success) {
if (isApiSuccess(response)) {
Message.success("登录成功!");
// 存储 token

View File

@@ -1,6 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"declaration": true,
"emitDeclarationOnly": false,
"declarationDir": "./dist/types",

View File

@@ -28,7 +28,7 @@ export default defineConfig(({ command, mode }) => {
proxy: {
// 代理所有 /api 开头的请求
'/api': {
target: 'http://localhost:8085', // 后端服务器地址
target: 'http://localhost:18085', // 后端服务器地址
changeOrigin: true, // 改变请求头中的 origin
secure: false, // 支持 https
// 如果后端 API 路径不包含 /api可以重写路径