通过Express + Vue3从零构建一个用户认证与授权系统(三)前端应用工程构建

devtools/2024/10/19 7:55:47/

前言

接下来,我们将使用 Vue 3TypeScriptVite 构建一个前端应用,与之前构建的后端 API 无缝对接。此前端将处理用户认证、显示用户数据、管理角色和权限,并确保与后端的安全通信。首先,我们来构建一个满足基本开发的前端应用工程。

1.项目初始化 

首先,使用 Vite 快速创建一个 Vue 3 + TypeScript 项目,我这里使用是npm。

# 使用 npm
npm create vite@latest frontend -- --template vue-ts# 或者使用 yarn
yarn create vite frontend --template vue-ts# 或者使用 pnpm
pnpm create vite frontend -- --template vue-ts

进入项目目录:

cd frontend

项目结构如下:

/frontend
├── public/                 # 静态资源
├── src/
│   ├── assets/             # 静态资源(图片、样式等)
│   ├── components/         # 公共组件
│   ├── layouts/            # 布局组件
│   ├── views/              # 各页面视图
│   ├── router/             # 路由配置
│   ├── store/              # 状态管理(Vuex/Pinia)
│   ├── services/           # 接口请求服务 (Axios等)
│   ├── utils/              # 工具函数
│   ├── App.vue             # 根组件
│   └── main.ts             # 入口文件
├── package.json
├── tsconfig.app.json       # TypeScript 配置
├── tsconfig.json           # TypeScript 配置
├── tsconfig.node.json      # TypeScript 配置
└── vite.config.js          # Vite 配置

2. 安装依赖

// 必要的运行时依赖
// Vue Router用于管理前端路由, Pinia是Vue3推荐的状态管理库, Axios基于promise的HTTP库,
// pinia-plugin-persistedstate状态持久化插件
npm install axios pinia vue-router@4 pinia-plugin-persistedstate// 开发依赖
// ‌提供Node.js的类型定义文件
npm install -D @types/node

3.配置 TypeScript 

tsconfig.json

// \tsconfig.json
{"files": [],"references": [{ "path": "./tsconfig.app.json" },{ "path": "./tsconfig.node.json" }]
}

tsconfig.app.json 

// \tsconfig.app.json
{"compilerOptions": {"target": "ES2020","useDefineForClassFields": true,"module": "ESNext","lib": ["ES2020", "DOM", "DOM.Iterable"],"skipLibCheck": true,"ignoreDeprecations": "5.0","baseUrl": ".","paths": {"@/*": ["src/*"] // 将 '@/*' 映射到 'src/*'},/* Bundler mode */"moduleResolution": "bundler","allowImportingTsExtensions": true,"isolatedModules": true,"moduleDetection": "force","noEmit": true,"jsx": "preserve",/* Linting */"strict": true,"noUnusedLocals": true,"noUnusedParameters": true,"noFallthroughCasesInSwitch": true},"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

tsconfig.node.json

// \tsconfig.node.json
{"compilerOptions": {"target": "ES2022","lib": ["ES2023"],"module": "ESNext","skipLibCheck": true,// 其他配置项..."types": ["node"],"moduleResolution": "node","esModuleInterop": true,"allowSyntheticDefaultImports": true,/* Bundler mode */"allowImportingTsExtensions": true,"isolatedModules": true,"moduleDetection": "force","noEmit": true,/* Linting */"strict": true,"noUnusedLocals": true,"noUnusedParameters": true,"noFallthroughCasesInSwitch": true},"include": ["vite.config.ts"]
}

 3.代码规范

代码规范是指定编程风格、程序结构和编码标准的文件,旨在提高代码的可读性、一致性和可维护性。使用prettier插件格式化代码,ESLint插件检测代码,参考如下配置:

prettier格式化工具

安装prettier

npm install prettier -D

配置.prettierrc文件,根目录下新建.prettierrc.js文件

module.exports = {printWidth: 100, // 每行最多显示100个字符tabWidth: 2, // 缩进2个字符semi: true, // 是否加分号vueIndentScriptAndStyle: true, // 缩进Vue文件中的脚本和样式标签singleQuote: true, // js中使用单引号quoteProps: "as-needed", // 仅在需要时在对象属性周围添加引号bracketSpacing: true, // 花括号空格trailingComma: "es5", // none - 无尾逗号 es5 - 添加es5中被支持的尾逗号 all - 所有可能的地方都被添加尾逗号jsxBracketSameLine: false, // 使html 标签的末尾> 单独一行jsxSingleQuote: false, // JSX中使用双引号arrowParens: "always", // 为单行箭头函数的参数添加圆括号 (x) => xinsertPragma: false, // 不在顶部插入 @formatproseWrap: "never",htmlWhitespaceSensitivity: "strict", // html中空格被认为是敏感的endOfLine: "auto", // 保持现有的行尾rangeStart: 0,
};

配置.prettierignore忽略文件,根目录下新建.prettierignore文件

/dist/*
.local
.output.js
/node_modules/****/*.svg
**/*.sh/public/*
ESLint检测工具

安装插件eslint-plugin-prettier 、eslint-config-prettier

npm i eslint-plugin-prettier eslint-config-prettier -D

配置eslintrc.js文件,根目录下新建.eslintrc.js文件

module.exports = {root: true,"env": {"browser": true,"es6": true,"node": true},"globals": {"process": true,"Plyr": true,"AMap": true},"parser": "babel-eslint","parserOptions": {"sourceType": "module","ecmaFeatures": {"experimentalObjectRestSpread": true}},extends: ['plugin:vue/vue3-essential','eslint:recommended','@vue/typescript/recommended','@vue/prettier','@vue/prettier/@typescript-eslint','plugin:prettier/recommended'],"plugins": ['html' // 插件,此插件用于识别文件中的js代码,没有MIME类型标识没有script标签也可以识别到,因此拿来识别.vue文件中的js代码],"rules": {/*** 代码中可能的错误或逻辑错误*/"no-cond-assign": ["error", "always"], // 禁止条件表达式中出现赋值操作符"no-console": ["error", { allow: ["warn", "error"] }], // 禁用 console"no-constant-condition": ["error", { "checkLoops": true }], // 禁止在条件中使用常量表达式"no-control-regex": ["error"], // 禁止在正则表达式中使用控制字符"no-debugger": ["error"], // 禁用 debugger"no-dupe-args": ["error"], // 禁止 function 定义中出现重名参数"no-dupe-keys": ["error"], // 禁止对象字面量中出现重复的 key"no-duplicate-case": ["error"], // 禁止出现重复的 case 标签"no-empty": ["error", { "allowEmptyCatch": true }], // 禁止出现空语句块"no-empty-character-class": ["error"], // 禁止在正则表达式中使用空字符集"no-ex-assign": ["error"], // 禁止对 catch 子句的参数重新赋值"no-extra-boolean-cast": ["error"], // 禁止不必要的布尔转换"no-extra-semi": ["error"], // 禁止不必要的分号"no-func-assign": ["warn"], // 禁止对 function 声明重新赋值"no-inner-declarations": ["error"], // 禁止在嵌套的块中出现变量声明或 function 声明"no-invalid-regexp": ["error", { "allowConstructorFlags": [] }], // 禁止 RegExp 构造函数中存在无效的正则表达式字符串"no-irregular-whitespace": ["error"], // 禁止在字符串和注释之外不规则的空白"no-obj-calls": ["error"], // 禁止把全局对象作为函数调用"no-regex-spaces": ["error"], // 禁止正则表达式字面量中出现多个空格"no-sparse-arrays": ["error"], // 禁用稀疏数组"no-unexpected-multiline": ["error"], // 禁止出现令人困惑的多行表达式"no-unsafe-finally": ["error"], // 禁止在 finally 语句块中出现控制流语句"no-unsafe-negation": ["error"], // 禁止对关系运算符的左操作数使用否定操作符"use-isnan": ["error"], // 要求使用 isNaN() 检查 NaN/*** 最佳实践*/"default-case": ["error"], // 要求 switch 语句中有 default 分支"dot-notation": ["error"], // 强制尽可能地使用点号"eqeqeq": ["warn"], // 要求使用 === 和 !=="no-caller": ["error"], // 禁用 arguments.caller 或 arguments.callee"no-case-declarations": ["error"], // 不允许在 case 子句中使用词法声明"no-empty-function": ["error"], // 禁止出现空函数"no-empty-pattern": ["error"], // 禁止使用空解构模式"no-eval": ["error"], // 禁用 eval()"no-global-assign": ["error"], // 禁止对原生对象或只读的全局对象进行赋值// "no-magic-numbers": ["error", { "ignoreArrayIndexes": true }], // 禁用魔术数字"no-redeclare": ["error", { "builtinGlobals": true }], // 禁止重新声明变量"no-self-assign": ["error", { props: true }], // 禁止自我赋值"no-unused-labels": ["error"], // 禁用出现未使用过的标"no-useless-escape": ["error"], // 禁用不必要的转义字符"radix": ["error"], // 强制在parseInt()使用基数参数/*** 变量声明*/"no-delete-var": ["error"], // 禁止删除变量"no-undef": ["error"], // 禁用未声明的变量,除非它们在 /*global */ 注释中被提到"no-unused-vars": ["error"], // 禁止出现未使用过的变量"no-use-before-define": ["error"], // 禁止在变量定义之前使用它们/*** 风格指南*/"array-bracket-newline": ["error", { "multiline": true }], // 在数组开括号后和闭括号前强制换行"array-bracket-spacing": ["error", "never"], // 强制数组方括号中使用一致的空2"block-spacing": ["error", "never"], // 禁止或强制在代码块中开括号前和闭括号后有空格"brace-style": ["error", "1tbs",], // 强制在代码块中使用一致的大括号风格"comma-dangle": ["error", "never"], // 要求或禁止末尾逗号"comma-spacing": ["error", { "before": false, "after": true }], // 强制在逗号前后使用一致的空格"comma-style": ["error", "last"], // 强制使用一致的逗号风格"computed-property-spacing": ["error", "never"], // 强制在计算的属性的方括号中使用一致的空格"consistent-this": ["error", "that"], // 当获取当前执行环境的上下文时,强制使用一致的命名"eol-last": ["error", "always"], // 要求或禁止文件末尾存在空行"func-call-spacing": ["error", "never"], // 要求或禁止在函数标识符和其调用之间有空格"func-names": ["error", "always"], // 要求或禁止使用命名的 function 表达式"func-style": ["error", "declaration", { "allowArrowFunctions": true }], // 强制一致地使用 function 声明或表达式"function-paren-newline": ["error", "multiline"], // 强制在函数括号内使用一致的换行"implicit-arrow-linebreak": ["error", "beside"], // 强制隐式返回的箭头函数体的位置"indent": ["error", 2, { "SwitchCase": 1 }], // 两个空格缩进"jsx-quotes": ["error", "prefer-double"], // 强制在 JSX 属性中一致地使用双引号或单引号"key-spacing": ["error", { "beforeColon": false, "afterColon": true }], // 强制在对象字面量的属性中键和值之间使用一致的间距"line-comment-position": ["error", { "position": "above", "ignorePattern": "ETC" }], // 强制行注释的位置"linebreak-style": ["error", "unix"], // 换行符风格"max-depth": ["error", 4], // 强制可嵌套的块的最大深度"max-nested-callbacks": ["error", 3], // 强制回调函数最大嵌套深度"max-params": ["error", 6], // 强制函数定义中最多允许的参数数量"multiline-comment-style": ["error", "starred-block"], // 强制对多行注释使用特定风格"multiline-ternary": ["error", "always-multiline"], // 要求或禁止在三元操作数中间换行"new-cap": ["error", { "capIsNew": false }], // 要求构造函数首字母大写"no-array-constructor": ["error"], // 禁用 Array 构造函数"no-mixed-operators": ["error"], // 禁止混合使用不同的操作符"no-mixed-spaces-and-tabs": ["error"], // 禁止空格和 tab 的混合缩进"no-multiple-empty-lines": ["error"], // 禁止出现多行空行"no-new-object": ["error"], // 禁用 Object 的构造函数"no-tabs": ["error"], // 禁用 tab"no-trailing-spaces": ["error", { "skipBlankLines": false, "ignoreComments": false }], // 禁用行尾空白"no-whitespace-before-property": ["error"], // 禁止属性前有空白"nonblock-statement-body-position": ["error", "beside"], // 强制单个语句的位置"object-curly-spacing": ["error", "never"], // 强制在大括号中使用一致的空格"operator-linebreak": ["error", "after"], // 强制操作符使用一致的换行符"quotes": ["error", "single"], // 使用单引号"semi": ["error", "always"], // 要求或禁止使用分号代替 ASI"semi-spacing": ["error", { "before": false, "after": true }], // 强制分号之前和之后使用一致的空格"space-before-function-paren": ["error", "never"], // 强制在 function的左括号之前使用一致的空格"space-in-parens": ["error", "never"], // 强制在圆括号内使用一致的空格"space-infix-ops": ["error"], // 要求操作符周围有空格"space-unary-ops": ["error", { "words": true, "nonwords": false }], // 强制在一元操作符前后使用一致的空格"spaced-comment": ["error", "always"], // 强制在注释中 // 或 /* 使用一致的空格/*** ECMAScript 6*/"arrow-spacing": ["error", { "before": true, "after": true }], // 强制箭头函数的箭头前后使用一致的空格"no-var": ["error"], // 要求使用 let 或 const 而不是 var"object-shorthand": ["error", "always"], // 要求或禁止对象字面量中方法和属性使用简写语法"prefer-arrow-callback": ["error", { "allowNamedFunctions": false }], // 要求回调函数使用箭头函数}
};

配置.eslintignore忽略文件,根目录下新建.eslintignore文件

build/*.js
src/assets
public
dist

4.UI 库和 css重置

安装相关依赖

css重置 可以帮助你消除不同浏览器默认样式的差异,确保在不同环境下应用样式的一致性。常用的CS 重置库有 normalize.css 和 reset.css。这里我们使用 normalize.css

UI组件库我使用的Element Plus。

如果希望使用更高级的 CSS 功能,如变量、嵌套、混入(mixins)等,可以引入 CSS 预处理器,如 Sass(SCSS)。

npm install normalize.css // 安装 normalize.css:npm install element-plus // 安装 Element Plusnpm install -D sass // 安装 CSS 预处理器 Sass 

项目中引入 Element Plus

// src/main.tsimport { createApp } from 'vue'
import App from './App.vue'import ElementPlus from 'element-plus'
// 引入 Element Plus 的 SCSS
import 'element-plus/theme-chalk/src/index.scss'; const app = createApp(App);app.use(ElementPlus, { size: 'default' });
app.mount('#app');

CSS重置和自定义Element Plus 主题

src/assets 目录下新建styles目录,新建global.scss和variables.scss文件。

global.scss

说明:

  • 首先引入 normalize.css 以消除默认样式差异。

  • 然后引入自定义的 element-variables.scss 文件,以覆盖 Element Plus 的默认主题变量。

  • 定义了一些全局样式,如 bodyabuttontable.page_main_container 等。

// src/assets/styles/global.scss/* 引入 normalize.css */
@import 'normalize.css';/* 引入 Element Plus 自定义变量 */
@import './variables.scss';body {font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;background-color: $background_color;color: $main_text;margin: 0;padding: 0;
}.page_main_container {max-width: $content_width;margin: 0 auto;padding: 1rem;
}

variables.scss 自定义主题变量和全局bian

// src/assets/styles/variables.scss$primary: #19be6b;
$info: #2db7f5;
$warning: #ff9900;
$danger: #ed4014;$title_text: #17233d; // 标题文字颜色
$main_text: #515a6e;  // 内容文字颜色
$sub_text: #808695;   // 次要文字颜色
$disabled: #c5c8ce;   // 禁用颜色$border_color: #dcdee2; // 边框颜色
$divider_color: #e8eaec; // 分割线颜色
$background_color: #f8f8f9; // 背景颜色@forward "element-plus/theme-chalk/src/common/var.scss" with ($colors: ("primary": ("base": $primary,),"warning": ("base": $warning,),"info": ("base": $info,),"danger": ("base": $danger,),)
);$header_height: 60px;
$content_width: 1280px;

配置 Vite以支持全局SCSS

// vite.config.tsimport { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path'; // 确保没有拼写错误export default defineConfig({plugins: [vue()],resolve: {alias: {'@': path.resolve(__dirname, './src'), // 设置路径别名},},css: {preprocessorOptions: {scss: {additionalData: `@use "@/assets/styles/global.scss" as *;`,},},},
});

验证主题修改成功

5. Vue Router

通过Vue Router 来管理前端路由,创建src/router/index.ts:

// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';import Layout from '@/components/Layouts/index.vue';const routes: Array<RouteRecordRaw> = [{path: '/',component: Layout,meta: { requiresAuth: true },children: [{path: '/',name: 'Dashboard',component: () => import('@/views/dashboard/index.vue')},{path: '/users',name: 'Users',component: () => import('@/views/users/index.vue')},{path: '/roles',name: 'Roles',component: () => import('@/views/roles/index.vue')},{path: '/permissions',name: 'Permissions',component: () => import('@/views/permissions/index.vue')},]},{path: '/login',name: 'Login',component: () => import('@/views/login/index.vue')},{path: '/register',name: 'Register',component: () => import('@/views/register/index.vue')},{path: '/:pathMatch(.*)*',name: 'NotFound',component: () => import('@/views/errorPage/404.vue')}
];const router = createRouter({history: createWebHistory(),routes
});// 导航守卫
router.beforeEach((to, from, next) => {const isAuthenticated = localStorage.getItem('accessToken');if (to.meta.requiresAuth && !isAuthenticated) {next({ name: 'Login' });} else {next();}
});export default router;

6. 状态管理:Pinia 

Pinia 是 Vue 3 推荐的状态管理库。 在 src/store 目录下创建 auth.ts

// src/store/auth.ts
import { defineStore } from 'pinia';
import authService from '@/api/authService';
import { User } from '@/types/api';
import router from '@/router';
import { ElNotification } from 'element-plus';interface AuthState {user: User | null;accessToken: string | null;refreshToken: any;
}export const useAuthStore = defineStore('auth', {state: (): AuthState => ({user: null,accessToken: localStorage.getItem('accessToken'),refreshToken: localStorage.getItem('refreshToken'),}),getters: {isAuthenticated: (state) => !!state.accessToken && !!state.user,},persist: true, // 状态持久化actions: {async login(params: { username: string; password: string }) {try {const { data } = await authService.login(params);this.accessToken = data.token;this.refreshToken = data.refreshToken;localStorage.setItem('accessToken', this.accessToken);localStorage.setItem('refreshToken', this.refreshToken);await this.fetchUser();ElNotification({title: '提示',message: '欢迎进入系统!',type: 'success',});router.push({ name: 'Dashboard' });} catch (error) {ElNotification({title: '登录失败',message: '请检查用户名和密码。',type: 'error',});throw error;}},async register(params: { username: string; email: string; password: string }) {try {await authService.register(params);ElNotification({title: '成功',message: '注册成功!请登录。',type: 'success',});} catch (error) {throw error;}},async fetchUser() {try {const { data } = await authService.getUser();this.user = data || {};} catch (error) {console.error('获取用户信息失败', error);}},logout() {this.user = null;this.accessToken = null;this.refreshToken = null;localStorage.removeItem('accessToken');localStorage.removeItem('refreshToken');router.push({ name: 'Login' });},async refreshToken() {try {const { data } = await authService.refreshToken(this.accessToken || '');this.accessToken = data.token;this.refreshToken = data.refreshToken;localStorage.setItem('accessToken', this.accessToken);localStorage.setItem('refreshToken', this.refreshToken);await this.fetchUser();} catch (error) {throw error;}},},
});

7. Axios 配置

创建 Axios 实例,src/services目录下创建axiosInstance.ts:

// src/services/axiosInstance.ts
import axios, { AxiosInstance, AxiosError, AxiosResponse } from 'axios';
import { useAuthStore } from '@/store/auth';
import router from '@/router';
import { ElNotification } from 'element-plus';const axiosInstance: AxiosInstance = axios.create({baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',// baseURL: '/api',timeout: 10000,headers: {'Content-Type': 'application/json',},
});// 请求拦截器
axiosInstance.interceptors.request.use((config) => {const authStore = useAuthStore();const token = authStore.accessToken;if (token) {config.headers.Authorization = `Bearer ${token}`;}return config;},(error) => {return Promise.reject(error);}
);// 响应拦截器
axiosInstance.interceptors.response.use((response: AxiosResponse) => {return response.data;},async (error: AxiosError) => {const authStore = useAuthStore();const originalRequest = error.config as any;// 如果响应状态码为 401,尝试刷新令牌if (error.response && error.response.status === 401 && !originalRequest._retry) {originalRequest._retry = true;try {await authStore.refreshToken(); // 调用刷新令牌的方法originalRequest.headers['Authorization'] = `Bearer ${authStore.accessToken}`;return axiosInstance(originalRequest);} catch (err) {authStore.logout();router.push({ name: 'Login' });ElNotification({title: '登录过期',message: '请重新登录。',type: 'error',});return Promise.reject(err);}}// 返回业务逻辑错误信息if (error.response && error.response.data) {return Promise.reject(error.response.data);}return Promise.reject(error);}
);export default axiosInstance;

封装http请求,src/services目录下创建apiClient.ts:

// src/services/apiClient.ts
import axiosInstance from './axiosInstance';interface RequestOptions {headers?: Record<string, string>;params?: Record<string, any>;data?: Record<string, any>;[key: string]: any;
}interface ApiResponse<T> {data: T;status: number;statusText: string;headers: any;config: any;request?: any;
}const apiClient = {async get<T>(url: string, options?: RequestOptions): Promise<ApiResponse<T>> {const response = await axiosInstance.get<T>(url, {...options,params: options?.params,});return response;},async post<T>(url: string, data?: any, options?: RequestOptions): Promise<ApiResponse<T>> {const response = await axiosInstance.post<T>(url, data, options);return response;},async put<T>(url: string, data?: any, options?: RequestOptions): Promise<ApiResponse<T>> {const response = await axiosInstance.put<T>(url, data, options);return response;},async delete<T>(url: string, options?: RequestOptions): Promise<ApiResponse<T>> {const response = await axiosInstance.delete<T>(url, options);return response;},async patch<T>(url: string, data?: any, options?: RequestOptions): Promise<ApiResponse<T>> {const response = await axiosInstance.patch<T>(url, data, options);return response;},// 其他 HTTP 方法(如 HEAD, OPTIONS)可以根据需要添加
};export default apiClient;

8. API接口文件

统一管理api接口,在 src/api 目录下创建接口文件xxxx.ts:

authService.ts

// src/api/authService.ts
import apiClient from '@/services/apiClient';
import { LoginResponse, RegisterResponse, RefreshTokenResponse, User } from '@/types/api';const authService = {login(params: { username: string; password: string }) {return apiClient.post<LoginResponse>('/login', params);},register(params: { username: string; password: string; email: string }) {return apiClient.post<RegisterResponse>('/register', params);},refreshToken(refreshToken: string) {return apiClient.post<RefreshTokenResponse>('/refreshToken', { refreshToken });},logout() {return apiClient.post<void>('/logout');},getUser() {return apiClient.get<User>('/getUserInfo');},
};export default authService;

usersService.ts 

// src/api/usersService.ts
import apiClient from '@/services/apiClient';
import { User, PaginatedResponse } from '@/types/api';interface GetUsersParams {pageNo: number;pageSize: number;username?: string;email?: string;phone?: string;[prop: string]: any;
}const usersService = {// 获取用户列表getUsers(params: GetUsersParams) {return apiClient.get<PaginatedResponse<User>>('/users', { params });},// 添加用户addUser(user: Partial<User> & { password: string }) {return apiClient.post<User>('/users', user);},// 更新用户updateUser(id: number, user: Partial<User>) {return apiClient.put<User>(`/users/${id}`, user);},// 删除用户deleteUser(id: number) {return apiClient.delete(`/users/${id}`);},
}export default usersService;

roleService.ts

// src/api/rolesService.ts
import apiClient from '@/services/apiClient';
import { Role } from '@/types/api';interface GetRolesParams {name?: string;
}const rolesService = {getRoles(params: GetRolesParams) {return apiClient.get<Role[]>('/roles', { params });},addRole(role: Partial<Role>) {return apiClient.post<Role>('/roles', role);},updateRole(id: number, role: Partial<Role>) {return apiClient.put<Role>(`/roles/${id}`, role);},deleteRole(id: number) {return apiClient.delete(`/roles/${id}`);},
};export default rolesService;

permissionsService.ts 

// src/api/rolesService.ts
import apiClient from '@/services/apiClient';
import { Role } from '@/types/api';interface GetRolesParams {name?: string;
}const rolesService = {getRoles(params: GetRolesParams) {return apiClient.get<Role[]>('/roles', { params });},addRole(role: Partial<Role>) {return apiClient.post<Role>('/roles', role);},updateRole(id: number, role: Partial<Role>) {return apiClient.put<Role>(`/roles/${id}`, role);},deleteRole(id: number) {return apiClient.delete(`/roles/${id}`);},
};export default rolesService;

9. API类型文件

/src/types/api.d.ts

// src/types/api.d.ts
// 认证相关
export interface LoginResponse {token: string;refreshToken: string;[prop: string]: any;
}export interface RegisterResponse {message: string;[prop: string]: any;
}export interface RefreshTokenResponse {token: string;refreshToken: string;[prop: string]: any;
}
// 首页统计数据
export interface DashboardStats {userCount: number;activeUserCount: string;permissionCount: number;[prop: string]: any;
}// 分页返回数据类型
export interface PaginatedResponse<T> {rows: T[];total: number;page: number;pageSize: number;[prop: string]: any;
}// 用户相关
export interface User {id: number;username: string;email: string;role_id: number;avatar: string;phone: string;createdAt: string;updatedAt: string;[prop: string]: any;
}
// 角色相关
export interface Role {id: number;name: string;description?: string;createdAt: string;updatedAt: string;[prop: string]: any;
}
// 权限相关
export interface Permission {id: number;name: string;description?: string;createdAt: string;updatedAt: string;[prop: string]: any;
}// 其他类型定义...

10. 页面组件实现

Layouts公共布局组件,创建src/components/Layout/index.vue

<!-- src/components/Layout/index.vue -->
<template><div class="layout"><Header /><div class="page-container"><Sidebar /><main class="main-content"><router-view /></main></div><!-- <Footer /> --></div>
</template><script lang="ts" setup>
import Header from './Header.vue';
import Sidebar from './Sidebar.vue';
// import Footer from './Footer.vue';</script><style scoped>
.layout {width: 100%;display: flex;flex-direction: column;
}
.page-container {width: 100%;height: calc(100vh - 60px);display: flex;
}
.main-content {flex: 1;padding: 1rem;box-sizing: border-box;
}
</style>

头部公共组件 src/components/Layout/Header.vue

<!-- src/components/Layout/Header.vue -->
<template><el-header height="60px" class="header"><div class="header-content"><!-- 左侧系统名称 --><div class="logo"><h2>用户权限系统</h2></div><!-- 右侧用户信息和操作 --><div class="user-info"><el-dropdown trigger="click"><span class="el-dropdown-link"><el-avatar:src="user?.avatar || defaultAvatar":size="36"class="avatar"></el-avatar><span class="username">{{ user?.username }}</span></span><template #dropdown><el-dropdown-menu><el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item></el-dropdown-menu></template></el-dropdown></div></div></el-header>
</template><script lang="ts" setup>
import { computed } from 'vue';
import { useAuthStore } from '@/store/auth';
import { ElMessageBox, ElMessage } from 'element-plus';const authStore = useAuthStore();// 获取用户信息
const user = computed(() => authStore.user);// 默认头像
const defaultAvatar = 'http://img.zzqlyx.com/20240903/yk.png';// 处理退出登录
const handleLogout = () => {ElMessageBox.confirm('确定要退出登录吗?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'info',}).then(() => {authStore.logout();}).catch(() => {// 用户取消退出});
};
</script><style lang="scss" scoped>
.header {background-color: $primary; /* Element Plus 默认主题色 */display: flex;align-items: center;padding: 0 20px;box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}.header-content {width: 100%;display: flex;justify-content: space-between;align-items: center;
}.logo h2 {color: white;margin: 0;font-size: 1.5rem;
}.user-info {display: flex;align-items: center;
}.el-dropdown-link {display: flex;align-items: center;
}.avatar {margin-right: 8px;
}.username {color: white;margin-right: 16px;cursor: pointer;
}.guest-info {display: flex;align-items: center;
}.guest-info .btn-text {color: white;margin-left: 8px;
}.guest-info .btn-text:hover {color: #ffd04b;
}
</style>

左侧菜单树公共组件 src/components/Layout/Sidebar.vue

<!-- src/components/Layout/Sidebar.vue -->
<template><el-aside class="sidebar"><el-menu:default-active="activeMenu"class="el-menu-vertical"router><el-menu-itemv-for="item in menuItems":key="item.path":index="item.path"><!-- <el-icon :component="item.icon"></el-icon> --><component class="el-icon" :is="item.icon"></component><span>{{ item.title }}</span></el-menu-item></el-menu></el-aside>
</template><script lang="ts" setup>
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import {House,User,Setting, // 使用存在的图标名称Lock,
} from '@element-plus/icons-vue';
import type { Component } from 'vue';interface MenuItem {path: string;name: string;icon: Component;title: string;[prop: string]: any; 
}const menuItems: MenuItem[] = [{ path: '/', name: 'Dashboard', icon: House, title: '仪表盘' },{ path: '/users', name: 'Users', icon: User, title: '用户管理' },{ path: '/roles', name: 'Roles', icon: Setting, title: '角色管理' }, // 确保使用正确的图标名称{ path: '/permissions', name: 'Permissions', icon: Lock, title: '权限管理' },
];const route = useRoute();
const router = useRouter();const activeMenu = computed(() => route.path);// 导航方法(可选)
const navigate = (path: string) => {router.push(path);
};
</script><style lang="scss" scoped>
.sidebar {width: $sidebar_width;height: calc(100vh - $header_height); /* 减去 Header 的高度 */
}.el-menu-vertical {height: 100%;border-right: none;
}.el-menu-vertical .el-menu-item {display: flex;align-items: center;
}.el-menu-vertical .el-icon {font-size: 1rem;
}/* 响应式设计:在小屏幕下隐藏侧边栏 */
@media (max-width: 768px) {.sidebar {display: none;}
}
</style>

登录页面src/views/login.vue

<!-- src/views/login.vue -->
<template><div class="login-container"><el-card class="login-card"><h2 class="login-title">登录</h2><el-form :model="form" :rules="rules" ref="loginForm" label-width="80px"><el-form-item label="用户名" prop="username"><el-input v-model="form.username" placeholder="请输入用户名"></el-input></el-form-item><el-form-item label="密码" prop="password"><el-inputv-model="form.password"type="password"placeholder="请输入密码"></el-input></el-form-item><el-form-item><el-button type="primary" @click="handleLogin" :loading="loading">登录</el-button></el-form-item></el-form><p class="register-link">还没有账号?<router-link type="primary" to="/register">注册</router-link></p></el-card></div>
</template><script lang="ts" setup>
import { ref } from 'vue';
import { useAuthStore } from '@/store/auth';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { FormInstance } from 'element-plus';// 定义表单模型
interface LoginForm {username: string;password: string;
}const form = ref<LoginForm>({username: '',password: '',
});// 表单验证规则
const rules = {username: [{ required: true, message: '请输入用户名', trigger: 'blur' },{ min: 3, max: 30, message: '用户名长度在3到30个字符', trigger: 'blur' },],password: [{ required: true, message: '请输入密码', trigger: 'blur' },{ min: 6, message: '密码长度至少6个字符', trigger: 'blur' },],
};const loginForm = ref<FormInstance>();const authStore = useAuthStore();
const router = useRouter();const loading = ref(false);const handleLogin = async () => {try {await loginForm.value?.validate();loading.value = true;const params = { username: form.value.username, password: form.value.password };await authStore.login(params);router.push({ name: 'Dashboard' });} catch (error: any) {if (error instanceof Error) {ElMessage.error(error.message);} else {// ElMessage.error('登录失败,请稍后重试');}} finally {loading.value = false;}
};
</script><style lang="scss" scoped>
.login-container {width: 100vw;height: 100vh;// background-image: url('//img.zzqlyx.com/user-system/background.jpg'); /* 本地图片路径 *//* 如果使用在线图片,示例:background-image: url('https://source.unsplash.com/random/1920x1080');*/background-size: cover;background-position: center;display: flex;justify-content: center;align-items: center;
}.login-card {width: 380px;padding: 2rem;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);border-radius: 6px;background-color: rgba(255, 255, 255, 1); /* 半透明背景以提升可读性 */
}.login-title {text-align: center;margin-bottom: 1.5rem;font-size: 1.5rem;
}.register-link {font-size: 12px;text-align: center;margin-top: 1rem;
}.register-link a {color: $primary;text-decoration: none;
}.register-link a:hover {text-decoration: underline;
}
</style>

首页仪表盘页面 src/views/dashboard/index.vue

<!-- src/views/dashboard/index.vue -->
<template><div class="dashboard"><!-- 欢迎卡片 --><el-card class="box-card"><div class="user-info"><el-avatar  :src="user.avatar || defaultAvatar" :size="258" /><div class="user-details"><h2>欢迎, {{ user.username }}!</h2><el-descriptions title="基本信息" :column="1"><el-descriptions-item label="邮箱:">{{ user.email }}</el-descriptions-item><el-descriptions-item label="角色:">{{ getRoleName(user.role_id) }}</el-descriptions-item><el-descriptions-item label="电话:">{{ user.phone }}</el-descriptions-item><el-descriptions-item label="注册时间:">{{ formatDate(user.createdAt) }}</el-descriptions-item></el-descriptions></div></div></el-card><!-- 统计信息卡片 --><el-row :gutter="20" class="stat-row"><el-col :span="8"><el-card><div class="stat-card"><el-icon><User /></el-icon><div class="stat-content"><h3>总用户数</h3><p>{{ dashboardStats?.userCount || 0 }}</p></div></div></el-card></el-col><el-col :span="8"><el-card><div class="stat-card"><el-icon><User /></el-icon><div class="stat-content"><h3>活跃用户</h3><p>{{ dashboardStats?.activeUserCount || 0 }}</p></div></div></el-card></el-col><el-col :span="8"><el-card><div class="stat-card"><el-icon><Lock /></el-icon><div class="stat-content"><h3>权限数量</h3><p>{{ dashboardStats?.permissionCount || 0 }}</p></div></div></el-card></el-col></el-row></div>
</template><script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { useAuthStore } from '@/store/auth';
import { User, Lock } from '@element-plus/icons-vue';import statsService from '@/api/statsService';
import type { DashboardStats } from '@/types/api';const authStore = useAuthStore();// 获取用户信息
const user = computed(() => authStore.user || {username: '未登录',email: '未登录',role_id: 0,phone: '未登录',avatar: '',createdAt: '',
});const dashboardStats = ref<DashboardStats>();// 默认头像
const defaultAvatar = 'http://img.zzqlyx.com/20240903/yk.png';// 模拟角色名称映射(实际应从后端获取或在 store 中定义)
enum roles  {'超级管理员','管理员','普通用户',
};// 获取角色名称
const getRoleName = (roleId: number): string => {return roles[roleId] || '未知角色';
};// 格式化日期
const formatDate = (dateString: string): string => {const date = new Date(dateString);return date.toLocaleString();
};
// 获取统计数据
const getDashboardStats = async ():Promise<void> => {try {const { data } = await statsService.getDashboardStats();console.log(data);dashboardStats.value = data ?? {};} catch (error) {}
}onMounted(() => {getDashboardStats();
})
</script><style scoped lang="scss">
.dashboard {.box-card {margin-bottom: 1rem;// padding: 1rem;.user-info {display: flex;align-items: center;.user-details {margin-left: 1rem;}}}.stat-row {.el-col {.el-card {padding: 1rem;.stat-card {display: flex;align-items: center;.el-icon {font-size: 2rem;margin-right: 1rem;}.stat-content {h3 {margin: 0;font-size: 1rem;}p {margin: 5px 0 0;font-size: 1.5rem;font-weight: bold;color: $primary;}}}}}}
}
</style>

用户管理页面 src/views/users//index.vue 

<!-- src/views/users.vue -->
<template><div class="users-container"><!-- 搜索表单 --><el-card class="search-card"><el-form :model="searchForm" inline><el-form-item label="用户名"><el-input v-model="searchForm.username" placeholder="请输入用户名"></el-input></el-form-item><el-form-item label="手机号"><el-input v-model="searchForm.phone" placeholder="请输入手机号"></el-input></el-form-item><el-form-item label="邮箱"><el-input v-model="searchForm.email" placeholder="请输入邮箱"></el-input></el-form-item><el-form-item><el-button type="primary" @click="handleSearch">搜索</el-button><el-button @click="handleReset">重置</el-button><el-button type="primary" @click="openAddUserDialog">添加用户</el-button></el-form-item></el-form></el-card><!-- 操作按钮 --><el-card><!-- 用户列表表格 --><el-table:data="users"style="width: 100%":loading="loading"stripeborder><el-table-column prop="id" label="ID" width="60"></el-table-column><el-table-column prop="username" label="用户名" min-width="150"></el-table-column><el-table-column prop="phone" label="手机号" min-width="150"></el-table-column><el-table-column prop="email" label="邮箱" min-width="150"></el-table-column><el-table-column prop="createdAt" label="注册时间" min-width="150"><template #default="{ row }">{{ formatDate(row.createdAt) }}</template></el-table-column><el-table-column label="操作" width="180" fixed="right"><template #default="{ row }"><el-button type="primary" link  size="small" @click="openEditUserDialog(row)">编辑</el-button><el-button type="danger" link size="small" @click="handleDeleteUser(row.id)">删除</el-button></template></el-table-column></el-table><!-- 分页组件 --><div class="pagination"><el-paginationbackground:page-sizes="[10, 20, 50, 100]"layout="total, sizes, prev, pager, next, jumper":current-page="pagination.pageNo":total="pagination.total"@current-change="handlePageChange"@size-change="handleSizeChange"></el-pagination></div></el-card><!-- 添加/编辑用户的弹窗 --><el-dialog:title="isEdit ? '编辑用户' : '添加用户'"v-model="userDialogVisible"width="500px"@close="handleCloseDialog"><el-form :model="userForm" :rules="userFormRules" ref="userFormRef" label-width="80px"><el-form-item label="用户名" prop="username"><el-input v-model="userForm.username" placeholder="请输入用户名"></el-input></el-form-item><el-form-item label="邮箱" prop="email"><el-input v-model="userForm.email" placeholder="请输入邮箱"></el-input></el-form-item><el-form-item label="电话" prop="phone"><el-input v-model="userForm.phone" placeholder="请输入电话"></el-input></el-form-item><el-form-item label="密码" prop="password" v-if="!isEdit"><el-inputv-model="userForm.password"type="password"placeholder="请输入密码"></el-input></el-form-item></el-form><template #footer><el-button @click="handleCloseDialog">取消</el-button><el-button type="primary" @click="handleSubmitUser">确认</el-button></template></el-dialog></div>
</template><script lang="ts" setup>
import { ref, reactive, computed, onMounted } from 'vue';import usersService from '@/api/usersService';
import { User } from '@/types/api';import { ElMessage, ElMessageBox } from 'element-plus';// 搜索表单数据
const searchForm = reactive({username: '',email: '',phone: '',
});// 用户表单数据(用于添加/编辑)
const userForm = reactive({id: null as number | null,username: '',email: '',phone: '',password: '',
});// 表单验证规则
const userFormRules = {username: [{ required: true, message: '请输入用户名', trigger: 'blur' },{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },],email: [{ required: false, message: '请输入邮箱', trigger: 'blur' },{ type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'change'] },],phone: [{ required: false, message: '请输入电话', trigger: 'blur' },{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号码', trigger: 'blur' },],password: [{ required: true, message: '请输入密码', trigger: 'blur' },{ min: 6, message: '密码至少6位', trigger: 'blur' },],
};// 控制弹窗显示
const userDialogVisible = ref(false);
const isEdit = ref(false);// 表单引用
const userFormRef = ref();// 用户列表和相关状态
const users = ref<User[]>([]);
const loading = ref(false);// 分页信息
const pagination = reactive({pageNo: 1,pageSize: 10,total: 0,
});// 格式化日期
const formatDate = (dateString: string): string => {const date = new Date(dateString);return date.toLocaleString();
};// 获取用户列表
const fetchUsers = async () => {loading.value = true;try {const params = {pageNo: pagination.pageNo,pageSize: pagination.pageSize,username: searchForm.username,email: searchForm.email,};const { data } = await usersService.getUsers(params);users.value = data?.rows?.length ? data.rows : [];pagination.total = data.total;} catch (error: any) {ElMessage.error(error.response?.data?.message || '获取用户列表失败');} finally {loading.value = false;}
};// 搜索用户
const handleSearch = () => {pagination.pageNo = 1;fetchUsers();
};// 重置搜索表单
const handleReset = () => {searchForm.username = '';searchForm.email = '';searchForm.phone = '';pagination.pageNo = 1;fetchUsers();
};// 打开添加用户弹窗
const openAddUserDialog = () => {isEdit.value = false;userForm.id = null;userForm.username = '';userForm.email = '';userForm.phone = '';userForm.password = '';userDialogVisible.value = true;
};// 打开编辑用户弹窗
const openEditUserDialog = (user: User) => {isEdit.value = true;userForm.id = user.id;userForm.username = user.username;userForm.email = user.email;userForm.phone = user.phone;userForm.password = ''; // 不显示密码userDialogVisible.value = true;
};// 提交用户表单(添加或编辑)
const handleSubmitUser = () => {userFormRef.value?.validate(async (valid: boolean) => {if (valid) {if (isEdit.value) {// 编辑用户try {await usersService.updateUser(userForm.id!, {username: userForm.username,email: userForm.email,phone: userForm.phone,});ElMessage.success('用户编辑成功');userDialogVisible.value = false;fetchUsers();} catch (error: any) {ElMessage.error(error.response?.data?.message || '编辑用户失败');}} else {// 添加用户try {await usersService.addUser({username: userForm.username,email: userForm.email,phone: userForm.phone,password: userForm.password,});ElMessage.success('用户添加成功');userDialogVisible.value = false;fetchUsers();} catch (error: any) {ElMessage.error(error.response?.data?.message || '添加用户失败');}}} else {ElMessage.error('请正确填写表单');return false;}});
};// 关闭弹窗
const handleCloseDialog = () => {userDialogVisible.value = false;
};// 删除用户
const handleDeleteUser = (id: number) => {ElMessageBox.confirm('确定要删除该用户吗?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning',}).then(async () => {try {await usersService.deleteUser(id);ElMessage.success('用户删除成功');fetchUsers();} catch (error: any) {ElMessage.error(error.response?.data?.message || '删除用户失败');}}).catch(() => {// 取消删除});
};const handleSizeChange = (val: number) => {pagination.pageSize = val;fetchUsers();
}
// 分页更改
const handlePageChange = (val: number) => {pagination.pageNo = val;fetchUsers();
};// 在组件挂载时获取用户列表
onMounted(() => {fetchUsers();
});</script><style scoped lang="scss">
.users-container {// padding: 20px;.search-card {margin-bottom: 1rem;}.pagination {margin-top: 1.5rem;width: 100%;display: flex;justify-content: flex-end;}.el-table .el-button {margin-right: 5px;}.el-dialog {/* 可根据需要自定义弹窗样式 */}
}
</style>

角色管理页面 src/views/roles//index.vue

<!-- src/views/Roles.vue -->
<template><div class="roles-container"><!-- 搜索表单 --><el-card class="search-card"><el-form :model="searchForm" label-width="80px" inline><el-form-item label="角色名称"><el-input v-model="searchForm.name" placeholder="请输入角色名称"></el-input></el-form-item><el-form-item><el-button type="primary" @click="handleSearch">搜索</el-button><el-button @click="handleReset">重置</el-button><el-button type="primary" @click="openAddRoleDialog">添加角色</el-button></el-form-item></el-form></el-card><el-card><!-- 角色列表表格 --><el-table:data="roles"style="width: 100%":loading="loading"stripeborder><el-table-column prop="id" label="ID" width="60"></el-table-column><el-table-column prop="name" label="角色名称" width="150"></el-table-column><el-table-column prop="description" label="描述" min-width="200"></el-table-column><el-table-column prop="createdAt" label="权限" min-width="180"><template #default="{ row }">{{ formatPermissions(row.permissions) }}</template></el-table-column><el-table-column prop="createdAt" label="创建时间" width="180"><template #default="{ row }">{{ formatDate(row.createdAt) }}</template></el-table-column><el-table-column prop="updatedAt" label="更新时间" width="180"><template #default="{ row }">{{ formatDate(row.updatedAt) }}</template></el-table-column><el-table-column label="操作" width="180" fixed="right"><template #default="{ row }"><el-button type="primary" link @click="openEditRoleDialog(row)">编辑</el-button><el-button type="danger" link @click="handleDeleteRole(row.id)">删除</el-button></template></el-table-column></el-table></el-card><!-- 添加/编辑角色的弹窗 --><el-dialog:title="isEdit ? '编辑角色' : '添加角色'"v-model="roleDialogVisible"width="500px"@close="handleCloseDialog"><el-form :model="roleForm" :rules="roleFormRules" ref="roleFormRef" label-width="80px"><el-form-item label="角色名称" prop="name"><el-input v-model="roleForm.name" placeholder="请输入角色名称"></el-input></el-form-item><el-form-item label="描述" prop="description"><el-input :rows="2" type="textarea" v-model="roleForm.description" maxlength="100" show-word-limit placeholder="请输入描述"></el-input></el-form-item><el-form-item label="权限" prop="permissions"><el-selectv-model="roleForm.permissions"multipleplaceholder="请选择权限"filterableclearablecollapse-tagspopper-class="custom-header":max-collapse-tags="1"><template #header><el-checkboxv-model="checkAll":indeterminate="indeterminate"@change="handleCheckAll">全选</el-checkbox></template><el-optionv-for="permission in permissions":key="permission.id":label="permission.name":value="permission.id"></el-option></el-select></el-form-item></el-form><template #footer><el-button @click="handleCloseDialog">取消</el-button><el-button type="primary" @click="handleSubmitRole">确认</el-button></template></el-dialog></div>
</template><script lang="ts" setup>
import { ref, reactive, watch, onMounted } from 'vue';
import rolesService from '@/api/roleService';
import permissionsService from '@/api/permissionsService';import { ElMessage, ElMessageBox } from 'element-plus';
import type { CheckboxValueType } from 'element-plus'// 定义角色类型
interface Role {id: number;name: string;description?: string;createdAt: string;updatedAt: string;[prop: string]: any;
}// 定义权限类型
interface Permission {id: number;name: string;description?: string;[prop: string]: any;
}// 搜索表单数据
const searchForm = reactive({name: '',
});// 角色表单数据(用于添加/编辑)
const roleForm = reactive({id: null as number | null,name: '',description: '',permissions: [] as number[],
});// 表单验证规则
const roleFormRules = {name: [{ required: true, message: '请输入角色名称', trigger: 'blur' },{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },],description: [{ max: 100, message: '描述最多100个字符', trigger: 'blur' },],permissions: [{ type: 'array', required: true, message: '请选择至少一个权限', trigger: 'change' },],
};// 控制弹窗显示
const roleDialogVisible = ref(false);
const isEdit = ref(false);// 表单引用
const roleFormRef = ref();// 角色列表和相关状态
const roles = ref<Role[]>([]);
const loading = ref(false);// 权限列表
const permissions = ref<Permission[]>([]);// 格式化日期
const formatDate = (dateString: string): string => {const date = new Date(dateString);return date.toLocaleString();
};const formatPermissions = (permissions: Permission[]) => {return permissions.map((_) => _.name).join(', ');
};const checkAll = ref(false)
const indeterminate = ref(false)watch(() => roleForm.permissions, (val:any) => {if (val.length === 0) {checkAll.value = falseindeterminate.value = false} else if (val.length === permissions.value.length) {checkAll.value = trueindeterminate.value = false} else {indeterminate.value = true}
})const handleCheckAll = (val: CheckboxValueType) => {indeterminate.value = falseif (val) {roleForm.permissions = permissions.value.map((_) => _.id)console.log(roleForm.permissions)} else {roleForm.permissions = []}
}// 获取角色列表
const fetchRoles = async () => {loading.value = true;try {const params = {name: searchForm.name,};const { data } = await rolesService.getRoles(params);roles.value = data?.length ? data : [];} catch (error: any) {ElMessage.error(error.response?.data?.message || '获取角色列表失败');} finally {loading.value = false;}
};// 获取权限列表
const fetchPermissions = async () => {try {const params = {pageNo: 1,pageSize: 9999,};const { data } = await permissionsService.getPermissions(params);permissions.value = data?.rows?.length ? data.rows : [];} catch (error: any) {ElMessage.error(error.response?.data?.message || '获取权限列表失败');}
};// 搜索角色
const handleSearch = () => {fetchRoles();
};// 重置搜索表单
const handleReset = () => {searchForm.name = '';fetchRoles();
};// 打开添加角色弹窗
const openAddRoleDialog = () => {isEdit.value = false;roleForm.id = null;roleForm.name = '';roleForm.description = '';roleForm.permissions = [];roleDialogVisible.value = true;
};// 打开编辑角色弹窗
const openEditRoleDialog = (role: Role) => {isEdit.value = true;roleForm.id = role.id;roleForm.name = role.name;roleForm.description = role.description || '';roleForm.permissions = role.permissions.map((_: any) => _.id);roleDialogVisible.value = true;
};// 提交角色表单(添加或编辑)
const handleSubmitRole = () => {roleFormRef.value?.validate(async (valid: boolean) => {if (valid) {if (isEdit.value) {// 编辑角色try {await rolesService.updateRole(roleForm.id!, {name: roleForm.name,description: roleForm.description,permissionIds: roleForm.permissions,});ElMessage.success('角色编辑成功');roleDialogVisible.value = false;fetchRoles();} catch (error: any) {ElMessage.error(error.response?.data?.message || '编辑角色失败');}} else {// 添加角色try {await rolesService.addRole({name: roleForm.name,description: roleForm.description,});ElMessage.success('角色添加成功');roleDialogVisible.value = false;fetchRoles();} catch (error: any) {ElMessage.error(error.response?.data?.message || '添加角色失败');}}} else {ElMessage.error('请正确填写表单');return false;}});
};// 关闭弹窗
const handleCloseDialog = () => {roleDialogVisible.value = false;
};// 删除角色
const handleDeleteRole = (id: number) => {ElMessageBox.confirm('确定要删除该角色吗?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning',}).then(async () => {try {await rolesService.deleteRole(id);ElMessage.success('角色删除成功');fetchRoles();} catch (error: any) {ElMessage.error(error.response?.data?.message || '删除角色失败');}}).catch(() => {// 取消删除});
};// 在组件挂载时获取角色列表
onMounted(() => {fetchRoles();fetchPermissions();
});</script><style scoped lang="scss">
.roles-container {.search-card {margin-bottom: 1rem;}.pagination {margin-top: 1.5rem;width: 100%;display: flex;justify-content: flex-end;}.el-table .el-button {margin-right: 5px;}.el-dialog {/* 可根据需要自定义弹窗样式 */}
}
</style>

权限管理页面 src/views/permissions/index.vue

<!-- src/views/Permissions.vue -->
<template><div class="permissions-container"><!-- 搜索表单 --><el-card class="search-card"><el-form :model="searchForm" label-width="80px" inline><el-form-item label="权限名称"><el-input v-model="searchForm.name" placeholder="请输入权限名称"></el-input></el-form-item><el-form-item><el-button type="primary" @click="handleSearch">搜索</el-button><el-button @click="handleReset">重置</el-button><el-button type="primary" @click="openAddPermissionDialog">添加权限</el-button></el-form-item></el-form></el-card><!-- 操作按钮 --><el-card class="action-card"><!-- 权限列表表格 --><el-table:data="permissions"style="width: 100%":loading="loading"stripeborder><el-table-column prop="id" label="ID" width="60"></el-table-column><el-table-column prop="name" label="权限名称" width="150"></el-table-column><el-table-column prop="description" label="描述" min-width="200"></el-table-column><el-table-column prop="createdAt" label="创建时间" width="180"><template #default="{ row }">{{ formatDate(row.createdAt) }}</template></el-table-column><el-table-column prop="updatedAt" label="更新时间" width="180"><template #default="{ row }">{{ formatDate(row.updatedAt) }}</template></el-table-column><el-table-column label="操作" width="180" fixed="right"><template #default="{ row }"><el-button type="primary" link  @click="openEditPermissionDialog(row)">编辑</el-button><el-button type="danger" link  @click="handleDeletePermission(row.id)">删除</el-button></template></el-table-column></el-table><!-- 分页组件 --><div class="pagination"><el-paginationbackground:page-sizes="[10, 20, 50, 100]"layout="total, sizes, prev, pager, next, jumper":current-page="pagination.pageNo":total="pagination.total"@current-change="handlePageChange"@size-change="handleSizeChange"></el-pagination></div></el-card><!-- 添加/编辑权限的弹窗 --><el-dialog:title="isEdit ? '编辑权限' : '添加权限'"v-model="permissionDialogVisible"width="500px"@close="handleCloseDialog"><el-form :model="permissionForm" :rules="permissionFormRules" ref="permissionFormRef" label-width="80px"><el-form-item label="权限名称" prop="name"><el-input v-model="permissionForm.name" placeholder="请输入权限名称"></el-input></el-form-item><el-form-item label="描述" prop="description"><el-input v-model="permissionForm.description" placeholder="请输入描述"></el-input></el-form-item></el-form><template #footer><el-button @click="handleCloseDialog">取消</el-button><el-button type="primary" @click="handleSubmitPermission">确认</el-button></template></el-dialog></div>
</template><script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import permissionsService from '@/api/permissionsService';// 定义权限类型
interface Permission {id: number;name: string;description?: string;createdAt: string;updatedAt: string;
}// 搜索表单数据
const searchForm = reactive({name: '',
});// 权限表单数据(用于添加/编辑)
const permissionForm = reactive({id: null as number | null,name: '',description: '',
});// 表单验证规则
const permissionFormRules = {name: [{ required: true, message: '请输入权限名称', trigger: 'blur' },{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' },],description: [{ max: 200, message: '描述最多200个字符', trigger: 'blur' },],
};// 控制弹窗显示
const permissionDialogVisible = ref(false);
const isEdit = ref(false);// 表单引用
const permissionFormRef = ref();// 权限列表和相关状态
const permissions = ref<Permission[]>([]);
const loading = ref(false);// 分页信息
const pagination = reactive({pageNo: 1,pageSize: 10,total: 0,
});// 格式化日期
const formatDate = (dateString: string): string => {const date = new Date(dateString);return date.toLocaleString();
};// 获取权限列表
const fetchPermissions = async () => {loading.value = true;try {const params = {pageNo: pagination.pageNo,pageSize: pagination.pageSize,name: searchForm.name,};const { data } = await permissionsService.getPermissions(params);permissions.value = data.rows;pagination.total = data.total;} catch (error: any) {ElMessage.error(error.response?.data?.message || '获取权限列表失败');} finally {loading.value = false;}
};// 搜索权限
const handleSearch = () => {pagination.pageNo = 1;fetchPermissions();
};// 重置搜索表单
const handleReset = () => {searchForm.name = '';pagination.pageNo = 1;fetchPermissions();
};// 打开添加权限弹窗
const openAddPermissionDialog = () => {isEdit.value = false;permissionForm.id = null;permissionForm.name = '';permissionForm.description = '';permissionDialogVisible.value = true;
};// 打开编辑权限弹窗
const openEditPermissionDialog = (permission: Permission) => {isEdit.value = true;permissionForm.id = permission.id;permissionForm.name = permission.name;permissionForm.description = permission.description || '';permissionDialogVisible.value = true;
};// 提交权限表单(添加或编辑)
const handleSubmitPermission = () => {permissionFormRef.value?.validate(async (valid: boolean) => {if (valid) {if (isEdit.value) {// 编辑权限try {await permissionsService.updatePermission(permissionForm.id!, {name: permissionForm.name,description: permissionForm.description,});ElMessage.success('权限编辑成功');permissionDialogVisible.value = false;fetchPermissions();} catch (error: any) {ElMessage.error(error.response?.data?.message || '编辑权限失败');}} else {// 添加权限try {await permissionsService.addPermission({name: permissionForm.name,description: permissionForm.description,});ElMessage.success('权限添加成功');permissionDialogVisible.value = false;fetchPermissions();} catch (error: any) {ElMessage.error(error.response?.data?.message || '添加权限失败');}}} else {ElMessage.error('请正确填写表单');return false;}});
};// 关闭弹窗
const handleCloseDialog = () => {permissionDialogVisible.value = false;
};// 删除权限
const handleDeletePermission = (id: number) => {ElMessageBox.confirm('确定要删除该权限吗?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning',}).then(async () => {try {await permissionsService.deletePermission(id);ElMessage.success('权限删除成功');fetchPermissions();} catch (error: any) {ElMessage.error(error.response?.data?.message || '删除权限失败');}}).catch(() => {// 取消删除});
};const handleSizeChange = (val: number) => {pagination.pageSize = val;fetchPermissions();
}
// 分页更改
const handlePageChange = (val: number) => {pagination.pageNo = val;fetchPermissions();
};
// 在组件挂载时获取权限列表
onMounted(() => {fetchPermissions();
});
</script><style scoped lang="scss">
.permissions-container {.search-card {margin-bottom: 1rem;}.pagination {margin-top: 1.5rem;width: 100%;display: flex;justify-content: flex-end;}.el-table .el-button {margin-right: 5px;}.el-dialog {/* 可根据需要自定义弹窗样式 */}
}
</style>

11. 入口文件main.ts

import { createApp } from 'vue'
import App from './App.vue'import ElementPlus from 'element-plus'
import 'element-plus/theme-chalk/src/index.scss'; // 引入 Element Plus 的 SCSSimport router from './router';
import { createPinia } from 'pinia';
import createPersistedState from 'pinia-plugin-persistedstate' // 状态持久化const pinia = createPinia();
pinia.use(createPersistedState);const app = createApp(App);app.use(ElementPlus, { size: 'default' });
app.use(pinia);
app.use(router);app.mount('#app');

 11. vite.config.ts 

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path'; // 确保没有拼写错误export default defineConfig({plugins: [vue()],resolve: {alias: {'@': path.resolve(__dirname, './src'), // 设置路径别名},},server: {port: 8888,proxy: {// 代理 /api 开头的请求到后端服务器'/api': {target: 'http://localhost:3000/', // 后端服务器地址changeOrigin: true, // 是否改变源头rewrite: (path) => path.replace(/^\/api/, '/api'), // 重写路径,如果需要secure: false, // 如果后端使用了自签名证书,可以设置为 false},},},css: {preprocessorOptions: {scss: {additionalData: `@use "@/assets/styles/global.scss" as *;`,api: 'modern-compiler', // or 'modern'},},},
});

12. 启动项目

npm run dev

登录页面截图:

 首页:

角色管理: 权限管理:

总结

  • 登录、注册、权限、角色、用户管理等模块:已经分别实现了对应页面的增删查改功能,以及角色和权限关联。
  • 状态管理:使用了 Pinia 进行状态管理,使用 localStorage + pinia-plugin-persistedstate 插件实现用户信息和权限的持久化。
  • Vue Router: 使用了vue router进行路由管理

下一步计划

  1. 进一步完善功能:增加其他内容
  2. 部署:项目的打包和部署方案
  3. 其他

项目地址:vue3+node+typescript全栈用户认证与授权系统icon-default.png?t=O83Ahttps://gitee.com/zzqlyx/manage-system-demo


http://www.ppmy.cn/devtools/126958.html

相关文章

springboot web 和webflux两个都引用会怎样?

前一篇发了 springboot 启动 Check your application‘s dependencies for a supported reactive web server-CSDN博客 虽然是解决了&#xff0c;但还是要一探究竟 原因&#xff1a; 在我的项目里引用了pom.xml 引入了 spring.boot.parent 此时如果直接写SpringBootApplicat…

5G NR:UE初始接入信令流程浅介

UE初始接入信令流程 流程说明 用户设备&#xff08;UE&#xff09;向gNB-DU发送RRCSetupRequest消息。gNB-DU 包含 RRC 消息&#xff0c;如果 UE 被接纳&#xff0c;则在 INITIAL UL RRC MESSAGE TRANSFER 消息中包括为 UE 分配的低层配置&#xff0c;并将其传输到 gNB-CU。IN…

力扣 中等 82.删除排序链表中的重复元素 II

文章目录 题目介绍题解 题目介绍 题解 只需在83题基础上加一个while循环即可 class Solution {public ListNode deleteDuplicates(ListNode head) {ListNode dummy new ListNode(101, head);ListNode cur dummy;while (cur.next ! null && cur.next.next ! null) {…

重塑企业数字化未来:物联网与微服务架构的战略性深度融合

从物联网到微服务架构的战略价值解读 随着全球数字化转型的不断加速&#xff0c;企业需要重新审视其技术基础架构&#xff0c;以适应日益复杂的业务需求和市场变化。物联网&#xff08;IoT&#xff09;作为核心技术&#xff0c;已广泛应用于制造、农业、交通、医疗等各个行业&…

lstm基础知识

lstm前言 LSTM(Long short-term memory)通过刻意的设计来避免长期依赖问题&#xff0c;是一种特殊的RNN。长时间记住信息实际上是 LSTM 的默认行为&#xff0c;而不是需要努力学习的东西&#xff01; 在标准的RNN中&#xff0c;这个重复模块具有非常简单的结构&#xff0c;例…

Django 序列化serializers

在Django中&#xff0c;序列化通常指的是将数据库中的模型数据转换为JSON、XML或其他格式的过程。Django提供了内置的序列化工具&#xff0c;可以通过django.core.serializers模块进行序列化操作。 当你使用Django的序列化功能时&#xff0c;可以序列化以下两种对象类型&#…

Redis是单线程为何性能还高

背景 通常来讲&#xff0c;提到性能优化&#xff0c;我们都会说提高并行度。同样我们知道&#xff0c;Redis是单线程执行命令&#xff0c;那为何还能保持如此的高性能呢&#xff1f; 原因 基于内存访问 Redis 将所有数据存储在内存中&#xff0c;内存的读写速度远远高于磁盘&a…

从数据管理到功能优化:Vue+TS 项目实用技巧分享

引言 在项目开发过程中&#xff0c;优化用户界面和完善数据处理逻辑是提升用户体验的重要环节。本篇文章将带你一步步实现从修改项目图标、添加数据、优化日期显示&#xff0c;到新增自定义字段、调整按钮样式以及自定义按钮跳转等功能。这些操作不仅提升了项目的可视化效果&am…