Vue 3.3 + Vite 4.3 + TypeScript 5+ Element-Plus:从零到一构建企业级后台管理系统(前后端开源)(三)

简介: Vue 3.3 + Vite 4.3 + TypeScript 5+ Element-Plus:从零到一构建企业级后台管理系统(前后端开源)(三)

跨域处理

跨域原理


浏览器同源策略: 协议、域名和端口都相同是同源,浏览器会限制非同源请求读取响应结果。


本地开发环境通过 Vite 配置反向代理解决浏览器跨域问题,生产环境则是通过 nginx 配置反向代理 。


vite.config.ts 配置代理

微信图片_20230706152346.png



表面肉眼看到的请求地址: http://localhost:3000/dev-api/api/v1/users/me


真实访问的代理目标地址: http://vapi.youlai.tech/api/v1/users/me

微信图片_20230706152401.png



整合 Axios

Axios 基于promise可以用于浏览器和node.js的网络请求库


参考: Axios 官方文档


安装依赖


npm install axios

1

Axios 工具类封装


//  src/utils/request.ts

import axios, { InternalAxiosRequestConfig, AxiosResponse } from 'axios';

import { useUserStoreHook } from '@/store/modules/user';


// 创建 axios 实例

const service = axios.create({

 baseURL: import.meta.env.VITE_APP_BASE_API,

 timeout: 50000,

 headers: { 'Content-Type': 'application/json;charset=utf-8' }

});


// 请求拦截器

service.interceptors.request.use(

 (config: InternalAxiosRequestConfig) => {

   const userStore = useUserStoreHook();

   if (userStore.token) {

     config.headers.Authorization = userStore.token;

   }

   return config;

 },

 (error: any) => {

   return Promise.reject(error);

 }

);


// 响应拦截器

service.interceptors.response.use(

 (response: AxiosResponse) => {

   const { code, msg } = response.data;

   // 登录成功

   if (code === '00000') {

     return response.data;

   }


   ElMessage.error(msg || '系统出错');

   return Promise.reject(new Error(msg || 'Error'));

 },

 (error: any) => {

   if (error.response.data) {

     const { code, msg } = error.response.data;

     // token 过期,跳转登录页

     if (code === 'A0230') {

       ElMessageBox.confirm('当前页面已失效,请重新登录', '提示', {

         confirmButtonText: '确定',

         type: 'warning'

       }).then(() => {

         localStorage.clear(); // @vueuse/core 自动导入

         window.location.href = '/';

       });

     }else{

         ElMessage.error(msg || '系统出错');

     }

   }

   return Promise.reject(error.message);

 }

);


// 导出 axios 实例

export default service;




登录接口实战


访问 vue3-element-admin 在线接口文档, 查看登录接口请求参数和响应数据类型

微信图片_20230706152417.png



点击 生成代码 获取登录响应数据 TypeScript 类型定义


微信图片_20230706152433.png


将类型定义复制到 src/api/auth/types.ts 文件中


/**

* 登录请求参数

*/

export interface LoginData {

 /**

  * 用户名

  */

 username: string;

 /**

  * 密码

  */

 password: string;

}


/**

* 登录响应

*/

export interface LoginResult {

 /**

  * 访问token

  */

 accessToken?: string;

 /**

  * 过期时间(单位:毫秒)

  */

 expires?: number;

 /**

  * 刷新token

  */

 refreshToken?: string;

 /**

  * token 类型

  */

 tokenType?: string;

}


登录 API 定义


// src/api/auth/index.ts

import request from '@/utils/request';

import { AxiosPromise } from 'axios';

import { LoginData, LoginResult } from './types';


/**

* 登录API

*

* @param data {LoginData}

* @returns

*/

export function loginApi(data: LoginData): AxiosPromise {

 return request({

   url: '/api/v1/auth/login',

   method: 'post',

   params: data

 });

}


登录 API 调用


// src/store/modules/user.ts

import { loginApi } from '@/api/auth';

import { LoginData } from '@/api/auth/types';


/**

* 登录调用

*

* @param {LoginData}

* @returns

*/

function login(loginData: LoginData) {

 return new Promise((resolve, reject) => {

   loginApi(loginData)

     .then(response => {

       const { tokenType, accessToken } = response.data;

       token.value = tokenType + ' ' + accessToken; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx

       resolve();

     })

     .catch(error => {

       reject(error);

     });

 });

}


动态路由

安装 vue-router


npm install vue-router@next

1

路由实例


创建路由实例,顺带初始化静态路由,而动态路由需要用户登录,根据用户拥有的角色进行权限校验后进行初始化


// src/router/index.ts

import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';


export const Layout = () => import('@/layout/index.vue');


// 静态路由

export const constantRoutes: RouteRecordRaw[] = [

 {

   path: '/redirect',

   component: Layout,

   meta: { hidden: true },

   children: [

     {

       path: '/redirect/:path(.*)',

       component: () => import('@/views/redirect/index.vue')

     }

   ]

 },


 {

   path: '/login',

   component: () => import('@/views/login/index.vue'),

   meta: { hidden: true }

 },


 {

   path: '/',

   component: Layout,

   redirect: '/dashboard',

   children: [

     {

       path: 'dashboard',

       component: () => import('@/views/dashboard/index.vue'),

       name: 'Dashboard',

       meta: { title: 'dashboard', icon: 'homepage', affix: true }

     }

   ]

 }

];


/**

* 创建路由

*/

const router = createRouter({

 history: createWebHashHistory(),

 routes: constantRoutes as RouteRecordRaw[],

 // 刷新时,滚动条位置还原

 scrollBehavior: () => ({ left: 0, top: 0 })

});


/**

* 重置路由

*/

export function resetRouter() {

 router.replace({ path: '/login' });

 location.reload();

}


export default router;


全局注册路由实例


// main.ts

import router from "@/router";


app.use(router).mount('#app')


动态权限路由


路由守卫 src/permission.ts ,获取当前登录用户的角色信息进行动态路由的初始化

微信图片_20230706152449.png



最终调用 permissionStore.generateRoutes(roles) 方法生成动态路由


// src/store/modules/permission.ts

import { listRoutes } from '@/api/menu';


export const usePermissionStore = defineStore('permission', () => {

 const routes = ref([]);


 function setRoutes(newRoutes: RouteRecordRaw[]) {

   routes.value = constantRoutes.concat(newRoutes);

 }

 /**

  * 生成动态路由

  *

  * @param roles 用户角色集合

  * @returns

  */

 function generateRoutes(roles: string[]) {

   return new Promise((resolve, reject) => {

     // 接口获取所有路由

     listRoutes()

       .then(({ data: asyncRoutes }) => {

         // 根据角色获取有访问权限的路由

         const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);

         setRoutes(accessedRoutes);

         resolve(accessedRoutes);

       })

       .catch(error => {

         reject(error);

       });

   });

 }

 // 导出 store 的动态路由数据 routes

 return { routes, setRoutes, generateRoutes };

});


接口获取得到的路由数据

微信图片_20230706152540.png



根据路由数据 (routes)生成菜单的关键代码

微信图片_20230706152554.png

src/layout/componets/Sidebar/index.vue src/layout/componets/Sidebar/SidebarItem.vue


按钮权限

除了 Vue 内置的一系列指令 (比如 v-model 或 v-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives),以下就通过自定义指令的方式实现按钮权限控制。


参考:Vue 官方文档-自定义指令


**自定义指令 **


// src/directive/permission/index.ts


import { useUserStoreHook } from '@/store/modules/user';

import { Directive, DirectiveBinding } from 'vue';


/**

* 按钮权限

*/

export const hasPerm: Directive = {

 mounted(el: HTMLElement, binding: DirectiveBinding) {

   // 「超级管理员」拥有所有的按钮权限

   const { roles, perms } = useUserStoreHook();

   if (roles.includes('ROOT')) {

     return true;

   }

   // 「其他角色」按钮权限校验

   const { value } = binding;

   if (value) {

     const requiredPerms = value; // DOM绑定需要的按钮权限标识


     const hasPerm = perms?.some(perm => {

       return requiredPerms.includes(perm);

     });


     if (!hasPerm) {

       el.parentNode && el.parentNode.removeChild(el);

     }

   } else {

     throw new Error(

       "need perms! Like v-has-perm=\"['sys:user:add','sys:user:edit']\""

     );

   }

 }

};


全局注册自定义指令


// src/directive/index.ts

import type { App } from 'vue';


import { hasPerm } from './permission';


// 全局注册 directive 方法

export function setupDirective(app: App) {

 // 使 v-hasPerm 在所有组件中都可用

 app.directive('hasPerm', hasPerm);

}


// src/main.ts

import { setupDirective } from '@/directive';


const app = createApp(App);

// 全局注册 自定义指令(directive)

setupDirective(app);


组件使用自定义指令


// src/views/system/user/index.vue

新增

删除


国际化

国际化分为两个部分,Element Plus 框架国际化(官方提供了国际化方式)和自定义国际化(通过 vue-i18n 国际化插件)


Element Plus 国际化

简单的使用方式请参考 Element Plus 官方文档-国际化示例,以下介绍 vue3-element-admin 整合 pinia 实现国际化语言切换。


Element Plus 提供了一个 Vue 组件 ConfigProvider 用于全局配置国际化的设置。

</code></div><div><code>import { ElConfigProvider } from 'element-plus';</code></div><div><code>import { useAppStore } from '@/store/modules/app';</code></div><div><code>const appStore = useAppStore();</code></div><div><code>


定义 store


// src/store/modules/app.ts

import { defineStore } from 'pinia';

import { useStorage } from '@vueuse/core';

import defaultSettings from '@/settings';


// 导入 Element Plus 中英文语言包

import zhCn from 'element-plus/es/locale/lang/zh-cn';

import en from 'element-plus/es/locale/lang/en';


// setup

export const useAppStore = defineStore('app', () => {

 

 const language = useStorage('language', defaultSettings.language);

 

 /**

  * 根据语言标识读取对应的语言包

  */

 const locale = computed(() => {

   if (language?.value == 'en') {

     return en;

   } else {

     return zhCn;

   }

 });


 /**

  * 切换语言

  */

 function changeLanguage(val: string) {

   language.value = val;

 }


 return {

   language,

   locale,

   changeLanguage

 };

});




切换语言组件调用




import { useI18n } from 'vue-i18n';

import SvgIcon from '@/components/SvgIcon/index.vue';

import { useAppStore } from '@/store/modules/app';


const appStore = useAppStore();

const { locale } = useI18n();


function handleLanguageChange(lang: string) {

 locale.value = lang;

 appStore.changeLanguage(lang);

 if (lang == 'en') {

   ElMessage.success('Switch Language Successful!');

 } else {

   ElMessage.success('切换语言成功!');

 }

}





从 Element Plus 分页组件看下国际化的效果

微信图片_20230706152632.png微信图片_20230706152636.png





vue-i18n 自定义国际化

i18n 英文全拼 internationalization ,国际化的意思,英文 i 和 n 中间18个英文字母


参考:vue-i18n 官方文档 - installation


安装 vue-i18n


npm install vue-i18n@9

1

自定义语言包


创建 src/lang/package 语言包目录,存放自定义的语言文件


中文语言包 zh-cn.ts 英文语言包 en.ts

微信图片_20230706152701.png微信图片_20230706152707.png

创建 i18n 实例


// src/lang/index.ts

import { createI18n } from 'vue-i18n';

import { useAppStore } from '@/store/modules/app';


const appStore = useAppStore();

// 本地语言包

import enLocale from './package/en';

import zhCnLocale from './package/zh-cn';


const messages = {

 'zh-cn': {

   ...zhCnLocale

 },

 en: {

   ...enLocale

 }

};

// 创建 i18n 实例

const i18n = createI18n({

 legacy: false,

 locale: appStore.language,

 messages: messages

});

// 导出 i18n 实例

export default i18n;


i18n 全局注册


// main.ts


// 国际化

import i18n from '@/lang/index';


app.use(i18n).mount('#app');


登录页面国际化使用


$t 是 i18n 提供的根据 key 从语言包翻译对应的 value 方法


{{ $t("login.title") }}

1

在登录页面 src/view/login/index.vue 查看如何使用


效果预览


微信图片_20230706152727.gif


暗黑模式

Element Plus 2.2.0 版本开始支持暗黑模式,启用方式参考 Element Plus 官方文档 - 暗黑模式, 官方也提供了示例 element-plus-vite-starter 模版 。


这里根据官方文档和示例讲述 vue3-element-admin 是如何使用 VueUse 的 useDark 方法实现暗黑模式的动态切换。


导入 Element Plus 暗黑模式变量


// src/main.ts

import 'element-plus/theme-chalk/dark/css-vars.css'

1

2

切换暗黑模式设置





import IconEpSunny from '~icons/ep/sunny';

import IconEpMoon from '~icons/ep/moon';


/**

* 暗黑模式

*/

const settingsStore = useSettingsStore();

const isDark = useDark();

const toggleDark = () => useToggle(isDark);






自定义变量


除了 Element Plus 组件样式之外,应用中还有很多自定义的组件和样式,像这样的:

微信图片_20230706152741.png



应对自定义组件样式实现暗黑模式步骤如下:


新建 src/styles/dark.scss


html.dark {

 /* 修改自定义元素的样式 */  

 .navbar {

   background-color: #141414;

 }

}


在 Element Plus 的样式之后导入它


// main.ts

import 'element-plus/theme-chalk/dark/css-vars.css'

import '@/styles/dark.scss';

1

2

3

效果预览

微信图片_20230706152800.gif

相关文章
|
2月前
|
JavaScript 前端开发 开发者
深入理解TypeScript:类型系统与实用技巧
【10月更文挑战第8天】深入理解TypeScript:类型系统与实用技巧
|
4月前
|
JavaScript 前端开发 IDE
[译] 用 Typescript + Composition API 重构 Vue 3 组件
[译] 用 Typescript + Composition API 重构 Vue 3 组件
[译] 用 Typescript + Composition API 重构 Vue 3 组件
|
23天前
|
开发框架 JavaScript 前端开发
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势。通过明确的类型定义,TypeScript 能够在编码阶段发现潜在错误,提高代码质量;支持组件的清晰定义与复用,增强代码的可维护性;与 React、Vue 等框架结合,提供更佳的开发体验;适用于大型项目,优化代码结构和性能。随着 Web 技术的发展,TypeScript 的应用前景广阔,将继续引领 Web 开发的新趋势。
35 2
|
2月前
|
JavaScript 前端开发 开发者
深入理解TypeScript:类型系统与最佳实践
【10月更文挑战第8天】深入理解TypeScript:类型系统与最佳实践
|
2月前
|
JavaScript 安全 开发工具
在 Vue 3 中使用 TypeScript
【10月更文挑战第3天】
|
1月前
|
JavaScript 前端开发 安全
TypeScript进阶:类型系统与高级类型的应用
【10月更文挑战第25天】TypeScript作为JavaScript的超集,其类型系统是其核心特性之一。本文通过代码示例介绍了TypeScript的基本数据类型、联合类型、交叉类型、泛型和条件类型等高级类型的应用。这些特性不仅提高了代码的可读性和可维护性,还帮助开发者构建更健壮的应用程序。
31 0
|
2月前
|
JavaScript 前端开发 开发者
深入理解TypeScript:类型系统与实用技巧
【10月更文挑战第8天】深入理解TypeScript:类型系统与实用技巧
|
3月前
|
JavaScript
typeScript进阶(9)_type类型别名
本文介绍了TypeScript中类型别名的概念和用法。类型别名使用`type`关键字定义,可以为现有类型起一个新的名字,使代码更加清晰易懂。文章通过具体示例展示了如何定义类型别名以及如何在函数中使用类型别名。
47 1
typeScript进阶(9)_type类型别名
|
2月前
|
JavaScript 前端开发 安全
深入理解TypeScript:增强JavaScript的类型安全性
【10月更文挑战第8天】深入理解TypeScript:增强JavaScript的类型安全性
60 0
|
3月前
|
存储 JavaScript
typeScript进阶(11)_元组类型
本文介绍了TypeScript中的元组(Tuple)类型,它是一种特殊的数组类型,可以存储不同类型的元素。文章通过示例展示了如何声明元组类型以及如何给元组赋值。元组类型在定义时需要指定数组中每一项的类型,且在赋值时必须满足这些类型约束。此外,还探讨了如何给元组类型添加额外的元素,这些元素必须符合元组类型中定义的类型联合。
52 0