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

news2024/11/28 23:49:51

前言

接下来,我们将使用 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) => x
  insertPragma: false, // 不在顶部插入 @format
  proseWrap: "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 Plus

npm install -D sass // 安装 CSS 预处理器 Sass 

项目中引入 Element Plus

// src/main.ts

import { 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.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'), // 设置路径别名
    },
  },
  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-item
        v-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-input
            v-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"
        stripe
        border
      >
        <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-pagination
          background
          :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-input
            v-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"
        stripe
        border
      >
        <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-select
            v-model="roleForm.permissions"
            multiple
            placeholder="请选择权限"
            filterable
            clearable
            collapse-tags
            popper-class="custom-header"
            :max-collapse-tags="1"
          >
          <template #header>
            <el-checkbox
              v-model="checkAll"
              :indeterminate="indeterminate"
              @change="handleCheckAll"
            >
              全选
            </el-checkbox>
          </template>
            <el-option
              v-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 = false
    indeterminate.value = false
  } else if (val.length === permissions.value.length) {
    checkAll.value = true
    indeterminate.value = false
  } else {
    indeterminate.value = true
  }
})

const handleCheckAll = (val: CheckboxValueType) => {
  indeterminate.value = false
  if (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"
        stripe
        border
      >
        <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-pagination
          background
          :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 的 SCSS

import 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.coloradmin.cn/o/2218806.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring6梳理14——依赖注入之P命名空间

以上笔记来源&#xff1a; 尚硅谷Spring零基础入门到进阶&#xff0c;一套搞定spring6全套视频教程&#xff08;源码级讲解&#xff09;https://www.bilibili.com/video/BV1kR4y1b7Qc 目录 ①搭建模块 ②引入配置文件 ③创建bean-dip.xml文件 ④创建课程类文件 ⑤创建学生…

【C++】string类(2)

&#x1f973;个人主页: 起名字真南 &#x1f973;个人专栏:【数据结构初阶】 【C语言】 【C】 目录 引言1 模拟实现string类基本框架2 实现string类中的主要成员函数2.1 Push_Back 函数2.2 reserve 函数2.3 append 函数2.4 c_str 函数2.5 begin ,end 函数2.5 operator 函数2.6…

FileLink内外网文件交换——致力企业高效安全文件共享

随着数字化转型的推进&#xff0c;企业之间的文件交流需求日益增加。然而&#xff0c;传统的文件传输方式往往无法满足速度和安全性的双重要求。FileLink作为一款专注于跨网文件交换的工具&#xff0c;致力于为企业提供高效、安全的文件共享解决方案。 应用场景一&#xff1a;项…

Python酷玩之旅_数据分析入门(matplotlib)

导览 前言matplotlib入门1. 简介1.1 Pairwise data1.2 Statistical distributions1.3 Gridded data1.4 Irregularly gridded data1.5 3D and volumetric data 2. 实践2.1 安装2.2 示例 结语系列回顾 前言 翻看日历&#xff0c;今年的日子已划到了2024年10月19日&#xff0c;今天…

网络空间安全之一个WH的超前沿全栈技术深入学习之路(一:渗透测试行业术语扫盲)作者——LJS

欢迎各位彦祖与热巴畅游本人专栏与博客 你的三连是我最大的动力 以下图片仅代表专栏特色 [点击箭头指向的专栏名即可闪现] 专栏跑道一 ➡️网络空间安全——全栈前沿技术持续深入学习 专栏跑道二➡️ 24 Network Security -LJS ​ ​ ​ 专栏跑道三 ➡️ MYSQL REDIS Advanc…

vue登录页面

这里写目录标题 登录业务流程表单如何进行校验自定义校验规则整个表单的统一内容校验 封装登录接口axios的二次封装整个项目api的统一管理 调用接口 登录业务流程 表单如何进行校验 ElementPlus表单组件内置了表单校验功能&#xff0c;只需要按照组件要求配置必要参数即可 1.…

【880线代】线性代数一刷错题整理

第一章 行列式 2024.8.20日 1. 2. 3. 第二章 矩阵 2024.8.23日 1. 2024.8.26日 1. 2. 3. 4. 5. 2024.8.28日 1. 2. 3. 4. 第四章 线性方程组 2024.9.13日 1. 2. 3. 4. 5. 2024.9.14日 1. 第五章 相似矩阵 2024.9.14日 1. 2024.9.15日 1. 2. 3. 4. 5. 6. 7. 2024.9.…

蚂蚁华东师范大学:从零开始学习定义和解决一般优化问题LLMOPT

&#x1f3af; 推荐指数&#xff1a;&#x1f31f;&#x1f31f;&#x1f31f; &#x1f4d6; title&#xff1a;LLMOPT: Learning to Define and Solve General Optimization Problems from Scratch &#x1f525; code&#xff1a;https://github.com/caigaojiang/LLMOPT &am…

YOLOv11改进-卷积-空间和通道重构卷积SCConv

本篇文章将介绍一个新的改进模块——SCConv&#xff08;小波空间和通道重构卷积&#xff09;&#xff0c;并阐述如何将其应用于YOLOv11中&#xff0c;显著提升模型性能。为了减少YOLOv11模型的空间和通道维度上的冗余&#xff0c;我们引入空间和通道重构卷积。首先&#xff0c;…

如何开启华为交换机 http

系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目…

pc轨迹回放制作

亲爱的小伙伴&#xff0c;在您浏览之前&#xff0c;烦请关注一下&#xff0c;在此深表感谢&#xff01; 课程主题&#xff1a;pc轨迹回放制作 主要内容&#xff1a;制作车辆轨迹操作页&#xff0c;包括查询条件、动态轨迹回放、车辆轨迹详情表单等 应用场景&#xff1a;车辆…

14.归一化——关键的数据预处理方法

引言 在人工智能&#xff08;AI&#xff09;和机器学习中&#xff0c;归一化&#xff08;Normalization&#xff09;是一个重要的预处理步骤。它的主要目的是将数据转换到某个特定的范围。归一化可以帮助模型更高效地学习和提高预测的准确性。归一化在数据预处理方法中占据核心…

Jupyter Notebook中 Save and Export Notebook As不显示选项

问题 Jupyter Notebook中 Save and Export Notebook As 不显示选项&#xff08;保存和导出没有选项&#xff09; 解决 在jupyter notebook所在环境卸载jupyter_contrib_nbextensions&#xff0c;这是我之前安装的一个扩展工具集&#xff0c;从而导致上面的问题。 pip unin…

自动化数据处理:使用Selenium与Excel打造的数据爬取管道

随着互联网信息爆炸式增长&#xff0c;获取有效数据成为决策者的重要任务。人工爬取数据不仅耗时且效率低下&#xff0c;因此自动化数据处理成为一种高效解决方案。本文将介绍如何使用Selenium与Excel实现数据爬取与处理&#xff0c;结合代理IP技术构建一个可稳定运行的数据爬取…

Nodejs使用http模块创建Web服务器接收解析RFID读卡器刷卡数据

本示例使用设备&#xff1a; https://item.taobao.com/item.htm?spma21dvs.23580594.0.0.1d292c1buHvw58&ftt&id22173428704 Javascript源码 //引用http模块创建web服务器&#xff0c;监听指定的端口获取以GET、POST、JSON等方式上传的数据&#xff0c;并回应驱动读卡…

图像梯度-Sobel算子、scharrx算子和lapkacian算子

文章目录 一、认识什么是图像梯度和Sobel算子二、Sobel算子的具体使用三、scharrx算子与lapkacian(拉普拉斯)算子 一、认识什么是图像梯度和Sobel算子 图像的梯度是指图像亮度变化的空间导数&#xff0c;它描述了图像在不同方向上的强度变化。在图像处理和计算机视觉中&#x…

CUDA error: out of memory问题

加载模型时&#xff0c;模型也不大&#xff0c;GPU内存也完全够&#xff0c;但就是出现这个CUDA内存溢出问题。 究其原因&#xff0c;在于model.load_state_dict(torch.load(‘pretrain-model.pth’, map_locationdevice))这个代码省略了map_locationdevice 通过torch.load加载…

YOLOv11来了 | 自定义目标检测

概述 YOLO11 在 2024 年 9 月 27 日的 YOLO Vision 2024 活动中宣布&#xff1a;https://www.youtube.com/watch?vrfI5vOo3-_A。 YOLO11 是 Ultralytics YOLO 系列的最新版本&#xff0c;结合了尖端的准确性、速度和效率&#xff0c;用于目标检测、分割、分类、定向边界框和…

问题清除指南|alimama-creative/FLUX-Controlnet-Inpainting 运行注意事项

前言&#xff1a;近日验证想法需要用到inpainting技术&#xff0c;选择了https://github.com/alimama-creative/FLUX-Controlnet-Inpainting进行测试&#xff0c;在实现过程中遇到几个小问题&#xff0c;在此分享一下解决经验。 1. 下载预训练模型到本地 由于在huggingface官网…

React Agent 自定义实现

目录 背景 langchin 中的 agent langchin 中 agent 的问题 langchain 的 agent 案例 自定义 React Agent 大模型 工具定义 问题设定 问题改写&#xff0c;挖掘潜在意图 React Prompt 下一步规划 问题总结 代码 背景 之前使用过 langchian 中的 agent 去实现过一些…