Add 'frontend/' from commit '94f710850fa74ad3aaca5b65318f2fec9e1a2cdf'
git-subtree-dir: frontend git-subtree-mainline: 3f114b2cc3ad1bfe7399c1c78b54da47a88a3638 git-subtree-split: 94f710850fa74ad3aaca5b65318f2fec9e1a2cdf
This commit is contained in:
commit
b705bbfa7d
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
3
frontend/.vscode/extensions.json
vendored
Normal file
3
frontend/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
5
frontend/README.md
Normal file
5
frontend/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
131
frontend/devdoc.md
Normal file
131
frontend/devdoc.md
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
## 项目概述
|
||||||
|
|
||||||
|
本项目是一个用户管理系统,前端部分使用 Vue.js 作为主要框架,结合 Vue Router 进行路由管理,Pinia 进行状态管理,Element Plus 作为 UI 组件库。前端需要实现登录页面和用户管理页面,并根据用户角色控制页面元素的显示与隐藏,特别是对修改和删除按钮的权限控制。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
* Typescript
|
||||||
|
* Vue.js:前端框架,用于构建用户界面。
|
||||||
|
* Vue Router:用于管理前端路由。
|
||||||
|
* Pinia:用于状态管理,存储用户登录状态、角色信息等。
|
||||||
|
* Element Plus:UI 组件库,提供丰富的 UI 组件。
|
||||||
|
* Axios:用于与后端 API 进行通信。
|
||||||
|
* Vite:构建工具,用于快速开发和打包项目。
|
||||||
|
* npm:包管理工具
|
||||||
|
* pinia-plugin-persistedstate:用于状态持久化存储
|
||||||
|
* jwt-decode:用于解析JWT令牌
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── api/ # API 服务相关
|
||||||
|
│ ├── components/ # 公共组件
|
||||||
|
│ ├── router/ # 路由配置
|
||||||
|
│ ├── store/ # 状态管理
|
||||||
|
│ ├── views/ # 页面视图
|
||||||
|
│ ├── App.vue # 根组件
|
||||||
|
│ ├── main.ts # 入口文件
|
||||||
|
│ └── style.css # 全局样式
|
||||||
|
├── index.html # 主页面
|
||||||
|
├── package.json # 项目依赖
|
||||||
|
├── vite.config.ts # Vite 配置
|
||||||
|
└── tsconfig.json # TypeScript 配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 页面设计
|
||||||
|
|
||||||
|
### 1. 登录页面 (`loginPage`)
|
||||||
|
|
||||||
|
#### UI 组件安排
|
||||||
|
|
||||||
|
* 用户名输入框:使用 Element Plus 的 `el-input` 组件,用于输入用户名。
|
||||||
|
* 密码输入框:使用 Element Plus 的 `el-input` 组件,类型为 `password`,用于输入密码。
|
||||||
|
* 登录按钮:使用 Element Plus 的 `el-button` 组件,点击后触发登录逻辑。
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
|
||||||
|
* 用户输入用户名和密码后,点击登录按钮,前端通过 Axios 发送登录请求到后端 `/api/auth/login` 接口。
|
||||||
|
* 登录成功后,前端将获取到的 `access_token` 和 `refresh_token` 存储在 Pinia 状态管理中,并跳转到用户管理页面。
|
||||||
|
* 登录失败时,前端显示错误提示。
|
||||||
|
|
||||||
|
### 2. 用户管理页面 (`managePage`)
|
||||||
|
|
||||||
|
#### UI 组件安排
|
||||||
|
|
||||||
|
* 导航栏:使用 Element Plus 的 `el-menu` 组件,显示欢迎信息和当前用户信息
|
||||||
|
* 登出按钮:使用 Element Plus 的 `el-button` 组件,位于页面右上角,点击后触发登出逻辑。
|
||||||
|
* 用户列表:使用 Element Plus 的 `el-table` 组件,展示用户信息,包括 `ID`、`用户名`、`角色`、`描述` 等字段。
|
||||||
|
* 创建用户按钮:使用 Element Plus 的 `el-button` 组件,仅管理员可见,点击后弹出创建用户对话框。
|
||||||
|
* 修改按钮:使用 Element Plus 的 `el-button` 组件,位于每一行的操作列中,点击后弹出修改对话框。
|
||||||
|
* 删除按钮:使用 Element Plus 的 `el-button` 组件,位于每一行的操作列中,点击后触发删除逻辑。
|
||||||
|
* 修改对话框:使用 Element Plus 的 `el-dialog` 组件,用于修改用户信息。
|
||||||
|
|
||||||
|
#### 功能描述
|
||||||
|
|
||||||
|
* 用户列表展示:前端通过 Axios 发送请求到 `/api/users` 接口,获取用户列表数据,并在表格中展示。
|
||||||
|
* 创建用户:管理员点击创建用户按钮后,弹出创建用户对话框,填写信息后提交。
|
||||||
|
* 修改按钮的显示与隐藏:根据当前登录用户的角色,动态控制修改按钮的显示与隐藏。只有 `系统管理员` 和 `管理员` 可以看到并操作修改按钮。
|
||||||
|
* 删除按钮的显示与隐藏:根据当前登录用户的角色,动态控制删除按钮的显示与隐藏。只有 `系统管理员` 和 `管理员` 可以看到并操作删除按钮。
|
||||||
|
* 修改用户信息:点击修改按钮后,弹出修改对话框,用户可以在对话框中修改用户名、角色和描述信息。修改完成后,前端通过 Axios 发送请求到 `/api/users/{user_id}` 接口,更新用户信息。
|
||||||
|
* 删除用户:点击删除按钮后,前端通过 Axios 发送请求到 `/api/users/{user_id}` 接口,删除对应用户。
|
||||||
|
|
||||||
|
## 权限控制
|
||||||
|
|
||||||
|
### 1. 修改和删除按钮的权限控制
|
||||||
|
|
||||||
|
* 系统管理员、管理员:可以看到并操作所有用户的修改和删除按钮。
|
||||||
|
* 普通用户:无法看到修改和删除按钮,只能查看用户列表。
|
||||||
|
|
||||||
|
#### 实现逻辑
|
||||||
|
|
||||||
|
* 前端在用户登录成功后,从后端获取用户角色信息,并存储在 Pinia 状态管理中。
|
||||||
|
* 在用户管理页面中,前端根据当前用户的角色动态渲染表格中的操作列。如果当前用户是 `普通用户`,则不渲染修改和删除按钮;如果是 `系统管理员` 或 `管理员`,则渲染这些按钮。
|
||||||
|
|
||||||
|
### 2. 路由权限控制
|
||||||
|
|
||||||
|
* 登录页面:所有用户都可以访问。
|
||||||
|
* 用户管理页面:只有登录成功的用户才能访问。如果用户未登录,尝试访问用户管理页面时,前端应自动跳转到登录页面。
|
||||||
|
|
||||||
|
#### 实现逻辑
|
||||||
|
|
||||||
|
* 使用 Vue Router 的导航守卫(`beforeEach`)进行路由权限控制。在每次路由跳转前,检查 Pinia 中是否存储了有效的 `access_token`。如果没有,则跳转到登录页面。
|
||||||
|
|
||||||
|
## Token 管理
|
||||||
|
|
||||||
|
### 1. Access Token 和 Refresh Token 的存储
|
||||||
|
|
||||||
|
* 登录成功后,前端将 `access_token` 和 `refresh_token` 存储在 Pinia 状态管理中,并设置 `access_token` 的过期时间。
|
||||||
|
* 每次发送 API 请求时,前端从 Pinia 中获取 `access_token` 并添加到请求头中。
|
||||||
|
* 使用 pinia-plugin-persistedstate 插件将 token 存储在 localStorage 中,实现状态持久化
|
||||||
|
|
||||||
|
### 2. Token 自动刷新
|
||||||
|
|
||||||
|
* 前端在每次发送 API 请求前,检查 `access_token` 是否即将过期(例如,剩余有效期小于 5 分钟)。如果即将过期,前端使用 `refresh_token` 调用 `/api/auth/refresh` 接口,获取新的 `access_token` 和 `refresh_token`,并更新 Pinia 中的存储。
|
||||||
|
* 使用 Axios 的拦截器统一处理 token 刷新逻辑,避免在每个请求中重复编写代码。
|
||||||
|
|
||||||
|
## 状态管理
|
||||||
|
|
||||||
|
### 用户登录状态
|
||||||
|
|
||||||
|
* 使用 Pinia 存储用户登录状态,包括 `access_token`、`refresh_token`、用户角色、用户名、用户ID等信息。
|
||||||
|
* 在用户登出时,清除 Pinia 中的登录状态。
|
||||||
|
* 使用 pinia-plugin-persistedstate 插件进行状态持久化,在应用启动时自动从 localStorage 恢复状态
|
||||||
|
|
||||||
|
## 开发环境配置
|
||||||
|
|
||||||
|
1. 安装依赖:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 启动开发服务器:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 构建生产环境:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>RBAC</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
603
frontend/openapi.json
Normal file
603
frontend/openapi.json
Normal file
@ -0,0 +1,603 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {
|
||||||
|
"title": "User Management System",
|
||||||
|
"description": "API for managing users with role-based access control",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/api/auth/login": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"auth",
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Login",
|
||||||
|
"operationId": "login_api_auth_login_post",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/LoginRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TokenResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/auth/refresh": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"auth",
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Refresh Token",
|
||||||
|
"operationId": "refresh_token_api_auth_refresh_post",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/RefreshTokenRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TokenResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/users/": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"users",
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Get Users List",
|
||||||
|
"operationId": "get_users_list_api_users__get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"title": "Page",
|
||||||
|
"type": "integer",
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"title": "Limit",
|
||||||
|
"type": "integer",
|
||||||
|
"default": 100
|
||||||
|
},
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"title": "Role",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": "role",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"title": "Response Get Users List Api Users Get",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UserResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"OAuth2PasswordBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"users",
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Create User",
|
||||||
|
"operationId": "create_user_api_users__post",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserCreate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"OAuth2PasswordBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/users/{user_id}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"users",
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Get User",
|
||||||
|
"operationId": "get_user_api_users__user_id__get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"title": "User Id",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "path"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"OAuth2PasswordBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"users",
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Update User",
|
||||||
|
"operationId": "update_user_api_users__user_id__put",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"title": "User Id",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "path"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserUpdate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"OAuth2PasswordBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"users",
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Delete User",
|
||||||
|
"operationId": "delete_user_api_users__user_id__delete",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"title": "User Id",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": "user_id",
|
||||||
|
"in": "path"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "Successful Response"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"OAuth2PasswordBearer": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"HTTPValidationError": {
|
||||||
|
"title": "HTTPValidationError",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"title": "Detail",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/ValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LoginRequest": {
|
||||||
|
"title": "LoginRequest",
|
||||||
|
"required": [
|
||||||
|
"username",
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"username": {
|
||||||
|
"title": "Username",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"title": "Password",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RefreshTokenRequest": {
|
||||||
|
"title": "RefreshTokenRequest",
|
||||||
|
"required": [
|
||||||
|
"refresh_token"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"refresh_token": {
|
||||||
|
"title": "Refresh Token",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TokenResponse": {
|
||||||
|
"title": "TokenResponse",
|
||||||
|
"required": [
|
||||||
|
"access_token",
|
||||||
|
"refresh_token",
|
||||||
|
"token_type",
|
||||||
|
"access_token_exp",
|
||||||
|
"refresh_token_exp"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_token": {
|
||||||
|
"title": "Access Token",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"title": "Refresh Token",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"token_type": {
|
||||||
|
"title": "Token Type",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"access_token_exp": {
|
||||||
|
"title": "Access Token Exp",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"refresh_token_exp": {
|
||||||
|
"title": "Refresh Token Exp",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UserCreate": {
|
||||||
|
"title": "UserCreate",
|
||||||
|
"required": [
|
||||||
|
"username",
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"username": {
|
||||||
|
"title": "Username",
|
||||||
|
"maxLength": 50,
|
||||||
|
"type": "string",
|
||||||
|
"description": "用户名"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UserRole"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "用户角色",
|
||||||
|
"default": "user"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"title": "Description",
|
||||||
|
"maxLength": 255,
|
||||||
|
"type": "string",
|
||||||
|
"description": "用户描述"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"title": "Password",
|
||||||
|
"maxLength": 255,
|
||||||
|
"minLength": 6,
|
||||||
|
"type": "string",
|
||||||
|
"description": "用户密码"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UserResponse": {
|
||||||
|
"title": "UserResponse",
|
||||||
|
"required": [
|
||||||
|
"username",
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"updated_at"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"username": {
|
||||||
|
"title": "Username",
|
||||||
|
"maxLength": 50,
|
||||||
|
"type": "string",
|
||||||
|
"description": "用户名"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UserRole"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "用户角色",
|
||||||
|
"default": "user"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"title": "Description",
|
||||||
|
"maxLength": 255,
|
||||||
|
"type": "string",
|
||||||
|
"description": "用户描述"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"title": "Id",
|
||||||
|
"type": "integer",
|
||||||
|
"description": "用户ID"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"title": "Created At",
|
||||||
|
"type": "string",
|
||||||
|
"description": "创建时间",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"title": "Updated At",
|
||||||
|
"type": "string",
|
||||||
|
"description": "更新时间",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"UserRole": {
|
||||||
|
"title": "UserRole",
|
||||||
|
"enum": [
|
||||||
|
"system_admin",
|
||||||
|
"admin",
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"description": "An enumeration."
|
||||||
|
},
|
||||||
|
"UserUpdate": {
|
||||||
|
"title": "UserUpdate",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"username": {
|
||||||
|
"title": "Username",
|
||||||
|
"maxLength": 50,
|
||||||
|
"type": "string",
|
||||||
|
"description": "用户名"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UserRole"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "用户角色"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"title": "Description",
|
||||||
|
"maxLength": 255,
|
||||||
|
"type": "string",
|
||||||
|
"description": "用户描述"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ValidationError": {
|
||||||
|
"title": "ValidationError",
|
||||||
|
"required": [
|
||||||
|
"loc",
|
||||||
|
"msg",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"loc": {
|
||||||
|
"title": "Location",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"title": "Message",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"title": "Error Type",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes": {
|
||||||
|
"OAuth2PasswordBearer": {
|
||||||
|
"type": "oauth2",
|
||||||
|
"flows": {
|
||||||
|
"password": {
|
||||||
|
"scopes": {},
|
||||||
|
"tokenUrl": "auth/login"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4049
frontend/package-lock.json
generated
Normal file
4049
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
frontend/package.json
Normal file
29
frontend/package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"element-plus": "^2.9.3",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"pinia": "^2.3.1",
|
||||||
|
"pinia-plugin-persistedstate": "^4.2.0",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.13.4",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^6.0.5",
|
||||||
|
"vite-plugin-vue-devtools": "^7.7.2",
|
||||||
|
"vue-tsc": "^2.2.0"
|
||||||
|
}
|
||||||
|
}
|
12
frontend/src/App.vue
Normal file
12
frontend/src/App.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
30
frontend/src/api/authService.ts
Normal file
30
frontend/src/api/authService.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import apiClient from './axiosInstance';
|
||||||
|
import type { TokenResponse } from './types';
|
||||||
|
|
||||||
|
const API_BASE_URL = '/auth';
|
||||||
|
|
||||||
|
export const authService = {
|
||||||
|
async login(username: string, password: string): Promise<TokenResponse> {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post(`${API_BASE_URL}/login`, { username, password });
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.data?.detail?.[0]?.msg) {
|
||||||
|
throw new Error(error.response.data.detail[0].msg);
|
||||||
|
}
|
||||||
|
throw new Error('Login failed');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshToken(refreshToken: string): Promise<TokenResponse> {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post(`${API_BASE_URL}/refresh`, { refresh_token: refreshToken });
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.data?.detail?.[0]?.msg) {
|
||||||
|
throw new Error(error.response.data.detail[0].msg);
|
||||||
|
}
|
||||||
|
throw new Error('Token refresh failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
70
frontend/src/api/axiosInstance.ts
Normal file
70
frontend/src/api/axiosInstance.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { userStore } from '../store/userStore';
|
||||||
|
import router from '../router';
|
||||||
|
|
||||||
|
// 创建axios实例
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
apiClient.interceptors.request.use(
|
||||||
|
async (config) => {
|
||||||
|
const store = userStore();
|
||||||
|
|
||||||
|
// 只有已登录状态才添加Authorization头
|
||||||
|
if (store.isLoggedIn) {
|
||||||
|
const accessToken = store.accessToken;
|
||||||
|
if (accessToken) {
|
||||||
|
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
async (error) => {
|
||||||
|
const store = userStore(); // 获取用户状态
|
||||||
|
|
||||||
|
// 如果未登录,直接返回错误
|
||||||
|
if (!store.isLoggedIn) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalRequest = error.config;
|
||||||
|
|
||||||
|
// 如果401错误且不是刷新token的请求
|
||||||
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||||
|
originalRequest._retry = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试刷新 token
|
||||||
|
await store.refreshTokenMethod();
|
||||||
|
|
||||||
|
// 更新请求头并重试原始请求
|
||||||
|
originalRequest.headers.Authorization = `Bearer ${store.accessToken}`;
|
||||||
|
return apiClient(originalRequest);
|
||||||
|
} catch (refreshError) {
|
||||||
|
// 刷新 token 失败,清除登录状态并跳转登录页
|
||||||
|
store.logout();
|
||||||
|
router.push({ name: 'login' });
|
||||||
|
return Promise.reject(refreshError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非 401 错误或已重试过,直接返回错误
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default apiClient;
|
41
frontend/src/api/types.ts
Normal file
41
frontend/src/api/types.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
export interface UserResponse {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
role: UserRole;
|
||||||
|
description?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserCreate {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
role?: UserRole;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserUpdate {
|
||||||
|
username?: string;
|
||||||
|
role?: UserRole;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenResponse {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
token_type: string;
|
||||||
|
access_token_exp: number;
|
||||||
|
refresh_token_exp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HTTPValidationError {
|
||||||
|
detail: ValidationError[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationError {
|
||||||
|
loc: (string | number)[];
|
||||||
|
msg: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserRole = 'system_admin' | 'admin' | 'user';
|
32
frontend/src/api/userService.ts
Normal file
32
frontend/src/api/userService.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import apiClient from './axiosInstance';
|
||||||
|
import type { UserResponse, UserCreate, UserUpdate } from './types';
|
||||||
|
|
||||||
|
const API_BASE_URL = '/users';
|
||||||
|
|
||||||
|
export const userService = {
|
||||||
|
async getUsers(page = 1, limit = 100, role?: string): Promise<UserResponse[]> {
|
||||||
|
const response = await apiClient.get(API_BASE_URL, {
|
||||||
|
params: { page, limit, role }
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createUser(userData: UserCreate): Promise<UserResponse> {
|
||||||
|
const response = await apiClient.post(API_BASE_URL, userData);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateUser(userId: number, userData: UserUpdate): Promise<UserResponse> {
|
||||||
|
const response = await apiClient.put(`${API_BASE_URL}/${userId}`, userData);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteUser(userId: number): Promise<void> {
|
||||||
|
await apiClient.delete(`${API_BASE_URL}/${userId}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUser(userId: number): Promise<UserResponse> {
|
||||||
|
const response = await apiClient.get(`${API_BASE_URL}/${userId}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
118
frontend/src/components/EditUserDialog.vue
Normal file
118
frontend/src/components/EditUserDialog.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="dialogVisible" :title="props.mode === 'create' ? '创建用户' : '修改用户信息'">
|
||||||
|
<el-form :model="editForm">
|
||||||
|
<el-form-item v-if="props.mode === 'edit'" label="用户ID">
|
||||||
|
<el-input v-model="editForm.id" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="editForm.username" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色">
|
||||||
|
<el-select v-model="editForm.role">
|
||||||
|
<el-option label="系统管理员" value="system_admin" />
|
||||||
|
<el-option label="管理员" value="admin" />
|
||||||
|
<el-option label="普通用户" value="user" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="props.mode === 'create'" label="密码">
|
||||||
|
<el-input v-model="editForm.password" type="password" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述">
|
||||||
|
<el-input v-model="editForm.description" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleConfirm">确认</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import type { UserResponse, UserCreate } from '@/api/types'
|
||||||
|
import { userService } from '@/api/userService'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mode: 'create' | 'edit',
|
||||||
|
userId?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const options = ref<{ userId?: number, mode?: 'create' | 'edit', onConfirm?: () => void }>({})
|
||||||
|
const editForm = ref<UserResponse & { password?: string }>({
|
||||||
|
id: 0,
|
||||||
|
username: '',
|
||||||
|
role: 'user',
|
||||||
|
description: '',
|
||||||
|
created_at: '',
|
||||||
|
updated_at: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['confirm'])
|
||||||
|
|
||||||
|
const open = async (opts: { userId?: number, mode?: 'create' | 'edit', onConfirm?: () => void }) => {
|
||||||
|
options.value = opts
|
||||||
|
if (options.value.mode === 'edit' && options.value.userId) {
|
||||||
|
const user = await userService.getUser(options.value.userId)
|
||||||
|
editForm.value = { ...user }
|
||||||
|
} else {
|
||||||
|
editForm.value = {
|
||||||
|
id: 0,
|
||||||
|
username: '',
|
||||||
|
role: 'user',
|
||||||
|
description: '',
|
||||||
|
created_at: '',
|
||||||
|
updated_at: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.value.onConfirm) {
|
||||||
|
emit('confirm', options.value.onConfirm)
|
||||||
|
}
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
try {
|
||||||
|
if (props.mode === 'create') {
|
||||||
|
// 调用创建用户API
|
||||||
|
const { id, created_at, updated_at, ...createData } = editForm.value
|
||||||
|
if (!createData.password) {
|
||||||
|
throw new Error('密码不能为空')
|
||||||
|
}
|
||||||
|
await userService.createUser({
|
||||||
|
username: createData.username,
|
||||||
|
password: createData.password,
|
||||||
|
role: createData.role,
|
||||||
|
description: createData.description
|
||||||
|
} as UserCreate)
|
||||||
|
} else {
|
||||||
|
// 调用更新用户API
|
||||||
|
const { id, created_at, updated_at, ...updateData } = editForm.value
|
||||||
|
await userService.updateUser(id, updateData)
|
||||||
|
}
|
||||||
|
emit('confirm', editForm.value)
|
||||||
|
if (options.value.onConfirm) {
|
||||||
|
options.value.onConfirm()
|
||||||
|
}
|
||||||
|
ElMessage({
|
||||||
|
type: 'success',
|
||||||
|
message: props.mode === 'create' ? '用户创建成功' : '用户信息更新成功'
|
||||||
|
})
|
||||||
|
dialogVisible.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('操作失败:', error)
|
||||||
|
ElMessage({
|
||||||
|
type: 'error',
|
||||||
|
message: props.mode === 'create' ? '用户创建失败' : '用户信息更新失败'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
open,
|
||||||
|
handleConfirm
|
||||||
|
})
|
||||||
|
</script>
|
74
frontend/src/components/UserTable.vue
Normal file
74
frontend/src/components/UserTable.vue
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<el-table :data="users" style="width: 100%">
|
||||||
|
<el-table-column prop="id" label="ID" width="100" />
|
||||||
|
<el-table-column prop="username" label="用户名" />
|
||||||
|
<el-table-column prop="role" label="角色" />
|
||||||
|
<el-table-column prop="description" label="描述" />
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ new Date(scope.row.created_at).toLocaleString() }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ new Date(scope.row.updated_at).toLocaleString() }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150" v-if="isAdmin">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button type="primary" size="small" @click="handleEdit(scope.row)">修改</el-button>
|
||||||
|
<el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<EditUserDialog ref="editDialogRef" mode="edit" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { userService } from '../api/userService';
|
||||||
|
import { userStore } from '../store/userStore';
|
||||||
|
import type { UserResponse } from '../api/types';
|
||||||
|
import EditUserDialog from './EditUserDialog.vue';
|
||||||
|
|
||||||
|
const store = userStore();
|
||||||
|
const users = ref<UserResponse[]>([]);
|
||||||
|
const isAdmin = store.isAdmin;
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
const data = await userService.getUsers();
|
||||||
|
console.log('获取用户列表成功:', data);
|
||||||
|
users.value = data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const editDialogRef = ref<InstanceType<typeof EditUserDialog>>();
|
||||||
|
|
||||||
|
const handleEdit = (user: UserResponse) => {
|
||||||
|
editDialogRef.value?.open({
|
||||||
|
userId: user.id,
|
||||||
|
mode: 'edit',
|
||||||
|
onConfirm: () => fetchUsers()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (user: UserResponse) => {
|
||||||
|
try {
|
||||||
|
await userService.deleteUser(user.id);
|
||||||
|
await fetchUsers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除用户失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchUsers();
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
fetchUsers
|
||||||
|
});
|
||||||
|
</script>
|
46
frontend/src/main.ts
Normal file
46
frontend/src/main.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import './style.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import apiClient from './api/axiosInstance'
|
||||||
|
import { userStore } from './store/userStore'
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 配置路由和状态管理
|
||||||
|
app.use(router)
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(ElementPlus)
|
||||||
|
|
||||||
|
// 初始化userStore
|
||||||
|
const store = userStore()
|
||||||
|
|
||||||
|
// 使用插件进行持久化,监听 store 变化并保存到本地存储
|
||||||
|
store.$subscribe((_, state) => {
|
||||||
|
localStorage.setItem('authStore', JSON.stringify(state));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 action 执行,登录和刷新令牌后保存状态到本地存储
|
||||||
|
store.$onAction(({ name, store, after }) => {
|
||||||
|
if (name === 'login' || name === 'refreshTokenMethod') {
|
||||||
|
after(() => {
|
||||||
|
localStorage.setItem('authStore', JSON.stringify(store.$state));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化时从本地存储恢复状态
|
||||||
|
const persistedState = JSON.parse(localStorage.getItem('authStore') || '{}');
|
||||||
|
if (persistedState.accessToken && persistedState.refreshTokenToken && persistedState.role && persistedState.username && persistedState.userId !== null) {
|
||||||
|
store.$patch(persistedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置axios拦截器
|
||||||
|
apiClient
|
||||||
|
|
||||||
|
app.mount('#app')
|
44
frontend/src/router/index.ts
Normal file
44
frontend/src/router/index.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import { userStore } from '@/store/userStore'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import('@/views/LoginPage.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/manage',
|
||||||
|
name: 'manage',
|
||||||
|
component: () => import('@/views/ManagePage.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
redirect: '/manage'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
router.beforeEach((to, _, next) => {
|
||||||
|
//const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 如果路由需要认证但用户未登录
|
||||||
|
if (to.meta.requiresAuth && !userStore().isLoggedIn) {
|
||||||
|
next({ name: 'login' })
|
||||||
|
}
|
||||||
|
// 如果用户已登录但访问登录页
|
||||||
|
else if (to.name === 'login' && userStore().isLoggedIn) {
|
||||||
|
next({ name: 'manage' })
|
||||||
|
}
|
||||||
|
// 其他情况正常导航
|
||||||
|
else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
67
frontend/src/store/userStore.ts
Normal file
67
frontend/src/store/userStore.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { authService } from '../api/authService';
|
||||||
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
|
||||||
|
// 定义 store,包含用户认证相关状态和方法
|
||||||
|
export const userStore = defineStore('user', () => {
|
||||||
|
const accessToken = ref('');
|
||||||
|
const refreshToken = ref('');
|
||||||
|
const role = ref('');
|
||||||
|
const username = ref('');
|
||||||
|
const id = ref<number | null>(null);
|
||||||
|
|
||||||
|
// 计算属性,用于快速判断用户是否登录
|
||||||
|
const isLoggedIn = computed(() => !!accessToken.value);
|
||||||
|
const isAdmin = computed(() => ['system_admin', 'admin'].includes(role.value));
|
||||||
|
|
||||||
|
// 设置访问和刷新令牌的方法
|
||||||
|
function setTokens(tokens: { access_token: string; refresh_token: string }) {
|
||||||
|
accessToken.value = tokens.access_token;
|
||||||
|
refreshToken.value = tokens.refresh_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置用户角色的方法
|
||||||
|
function setRole(userRole: string) {
|
||||||
|
role.value = userRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户登录方法,调用 authService.login 获取令牌并设置状态
|
||||||
|
async function login(usernameParam: string, password: string) {
|
||||||
|
try {
|
||||||
|
const { access_token, refresh_token } = await authService.login(usernameParam, password);
|
||||||
|
const decoded = jwtDecode<{ sub: string; role: string; username: string; id: string }>(access_token);
|
||||||
|
setTokens({ access_token, refresh_token });
|
||||||
|
setRole(decoded.role);
|
||||||
|
username.value = decoded.username;
|
||||||
|
id.value = parseInt(decoded.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login failed:', error);
|
||||||
|
const err = new Error('登录失败');
|
||||||
|
err.name = 'LoginError';
|
||||||
|
err.cause = error;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户登出方法,清空所有状态
|
||||||
|
async function logout() {
|
||||||
|
accessToken.value = '';
|
||||||
|
refreshToken.value = '';
|
||||||
|
role.value = '';
|
||||||
|
username.value = '';
|
||||||
|
id.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新访问令牌的方法,调用 authService.refreshToken 获取新令牌并设置状态
|
||||||
|
async function refreshTokenMethod() {
|
||||||
|
try {
|
||||||
|
const { access_token, refresh_token } = await authService.refreshToken(refreshToken.value);
|
||||||
|
setTokens({ access_token, refresh_token });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Token refresh failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { accessToken, refreshTokenToken: refreshToken, role, username, userId: id, isLoggedIn, isAdmin, login, logout, refreshTokenMethod };
|
||||||
|
});
|
79
frontend/src/style.css
Normal file
79
frontend/src/style.css
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
87
frontend/src/views/LoginPage.vue
Normal file
87
frontend/src/views/LoginPage.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<h2 class="login-title">用户登录</h2>
|
||||||
|
<el-form :model="form" label-width="80px">
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="form.username" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码">
|
||||||
|
<el-input v-model="form.password" type="password" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleLogin">登录</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { userStore } from '@/store/userStore'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
interface LoginForm {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const store = userStore()
|
||||||
|
|
||||||
|
const form = ref<LoginForm>({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
const { username, password } = form.value
|
||||||
|
if (!username || !password) {
|
||||||
|
throw new Error('用户名和密码不能为空')
|
||||||
|
}
|
||||||
|
await store.login(username, password)
|
||||||
|
router.push({ name: 'manage' })
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.message || '登录失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 100px auto;
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-container {
|
||||||
|
margin: 50px 20px;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
122
frontend/src/views/ManagePage.vue
Normal file
122
frontend/src/views/ManagePage.vue
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<div class="manage-page">
|
||||||
|
<el-menu
|
||||||
|
mode="horizontal"
|
||||||
|
class="nav-bar"
|
||||||
|
>
|
||||||
|
<el-menu-item index="1" class="nav-title">欢迎</el-menu-item>
|
||||||
|
<el-menu-item index="2" class="user-info">
|
||||||
|
<div class="user-detail">
|
||||||
|
<span class="username">{{ userStore.username }}</span>
|
||||||
|
<span class="role">({{ userStore.role }})</span>
|
||||||
|
</div>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="3">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
class="logout-btn"
|
||||||
|
@click="handleLogout"
|
||||||
|
>
|
||||||
|
登出
|
||||||
|
</el-button>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
<el-card class="page-container">
|
||||||
|
<el-text class="system-title" type="primary" size="large" tag="h1">
|
||||||
|
用户管理系统
|
||||||
|
</el-text>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
class="create-btn"
|
||||||
|
@click="handleCreateUser"
|
||||||
|
v-if="userStore.isAdmin"
|
||||||
|
>
|
||||||
|
创建用户
|
||||||
|
</el-button>
|
||||||
|
<UserTable/>
|
||||||
|
<EditUserDialog ref="editUserDialog" mode="create" />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { userStore as store } from '@/store/userStore'
|
||||||
|
import UserTable from '@/components/UserTable.vue'
|
||||||
|
import EditUserDialog from '@/components/EditUserDialog.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = store()
|
||||||
|
|
||||||
|
const editUserDialog = ref<InstanceType<typeof EditUserDialog>>()
|
||||||
|
const userTableRef = ref<InstanceType<typeof UserTable>>()
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
userStore.logout()
|
||||||
|
router.push({ name: 'login' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateUser = () => {
|
||||||
|
editUserDialog.value?.open({
|
||||||
|
mode: 'create',
|
||||||
|
onConfirm: () => {
|
||||||
|
userTableRef.value?.fetchUsers()
|
||||||
|
console.log('用户创建成功,刷新用户列表')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
userTableRef.value?.fetchUsers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.manage-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-bar {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-detail {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-title {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
18
frontend/tsconfig.app.json
Normal file
18
frontend/tsconfig.app.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
7
frontend/tsconfig.json
Normal file
7
frontend/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
24
frontend/tsconfig.node.json
Normal file
24
frontend/tsconfig.node.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
32
frontend/vite.config.ts
Normal file
32
frontend/vite.config.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue()
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
strictPort: true,
|
||||||
|
open: true,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
rewrite: (path) => path,
|
||||||
|
followRedirects: true, // Ensure 307 redirects are handled correctly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
strictPort: true
|
||||||
|
}
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user