# vue3-element-admin-template **Repository Path**: lishanji/vue3-element-admin-template ## Basic Information - **Project Name**: vue3-element-admin-template - **Description**: vue3-element-admin 是基于 Vue3 + Vite5+ TypeScript5 + Element-Plus + Pinia 等主流技术栈构建的免费开源的后台管理前端模板(配套后端源码) - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-05-15 - **Last Updated**: 2024-06-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 文档 https://juejin.cn/post/7228990409909108793 # 1 unplugin 自动导入 https://zhuanlan.zhihu.com/p/612397686 ```JS Element Plus 官方文档中推荐 按需自动导入 的方式,而此需要使用额外的插件 unplugin-auto-import 和 unplugin-vue-components 来导入要使用的组件。所以在整合 Element Plus 之前先了解下自动导入的概念和作用 ``` 概念 为了避免在多个页面重复引入 API 或 组件,由此而产生的自动导入插件来节省重复代码和提高开发效率。  vite.config.js ``` import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' ``` ``` plugins: [ AutoImport({ imports: ['vue', '@vueuse/core', 'pinia', 'vue-router', 'vue-i18n'], resolvers: [ ElementPlusResolver(), // 自动导入图标组件 IconsResolver({}), ], dts: path.resolve(pathSrc, 'types', 'auto-imports.d.ts'), // 自动导入组件类型声明文件位置,默认根目录 }), Components({ resolvers: [ ElementPlusResolver(), // 自动注册图标组件 IconsResolver({ enabledCollections: ['ep'], // element-plus图标库,其他图标库 https://icon-sets.iconify.design/ }), ], dts: path.resolve(pathSrc, 'types', 'components.d.ts'), // 自动导入组件类型声明文件位置,默认根目录 }), ], ``` eslint.config.js ``` import eslintrcAutoImport from './.eslintrc-auto-import.json' assert { type: 'json' } export default [ { languageOptions: { globals: globals.browser, ...eslintrcAutoImport } } ] ``` # 2 allowSyntheticDefaultImports https://www.cnblogs.com/longmo666/p/18120218 ```JS // tsconfig.json { "compilerOptions": { "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录 "paths": { //路径映射,相对于baseUrl "@/*": ["src/*"] }, "allowSyntheticDefaultImports": true // 允许默认导入 } } ``` # 3 import path from 'path'编译器报错:TS2307: Cannot find module 'path' or its corresponding type declarations 1 安装@types/node 2 TypeScript 编译配置 # 4 安装自动导入 Icon 依赖 https://www.cnblogs.com/fuct/p/17533365.html # 5 整合 SVG 图标 # 6 json文件引入 import eslintrcAutoImport from './.eslintrc-auto-import.json' assert { type: 'json' } # 7 整合 SCSS以及模块化 ``` // vite.config.ts css: { // CSS 预处理器 preprocessorOptions: { //define global scss variable scss: { javascriptEnabled: true, additionalData: `@use "@/styles/variables.scss" as *;` } } } ``` # 8 整合 UnoCSS  ## 8.1 ``` # 9 整合 Pinia # 10 环境变量 ``` interface ImportMetaEnv { /** 应用端口 */ VITE_APP_PORT: number; /** API 基础路径(代理前缀) */ VITE_APP_BASE_API: string; /** API 地址 */ VITE_APP_API_URL: string; /** 是否开启 Mock 服务 */ VITE_MOCK_DEV_SERVER: boolean; } interface ImportMeta { readonly env: ImportMetaEnv; } ``` ``` # 变量必须以 VITE_ 为前缀才能暴露给外部读取 VITE_APP_TITLE = 'vue3-element-admin' VITE_APP_PORT = 3000 VITE_APP_BASE_API = '/dev-api' ``` ```JS import.meta.env.VITE_APP_BASE_API ``` ###### interface ImportMeta与import.meta关联 ImportMeta 和 import.meta 是在 ES 模块和某些现代构建工具(如 Vite 和 Rollup)中使用的概念,它们与模块的元数据有关。下面是它们之间关系的简要解释: ImportMeta ImportMeta 是一个 TypeScript 接口,用于定义 import.meta 对象可能包含的属性。这个接口通常不会直接在代码中定义,而是由构建工具或 TypeScript 的类型定义文件(如 @types 包)提供。它告诉 TypeScript 编译器 import.meta 对象可能有哪些属性,以便在编写代码时可以获得类型检查和更好的开发者体验。 import.meta import.meta 是一个特殊的对象,它提供了关于当前模块的元数据。这个对象是在模块顶层作用域中自动可用的,不需要导入或定义。import.meta 对象的具体内容和属性取决于构建工具或运行时环境。 例如,Vite 使用 import.meta.glob 和 import.meta.hot 来提供特定的功能,如动态导入和模块热替换。这些属性并不是 ES 模块规范的一部分,而是由 Vite 添加的。 关联 ImportMeta 和 import.meta 的关联在于它们都是用来描述和处理模块的元数据的。ImportMeta 提供了 TypeScript 类型信息,以便在编写代码时能够理解和使用 import.meta 对象。而 import.meta 对象则提供了在运行时可以访问的元数据。 在 TypeScript 中,你可以定义一个符合 ImportMeta 接口的类型,然后使用该类型来注解 import.meta 对象,以便在代码中获取更好的类型检查和智能提示。但是,请注意,ImportMeta 本身并不是 ES 模块规范的一部分,而是 TypeScript 和某些构建工具提供的扩展。 总之,ImportMeta 和 import.meta 一起工作,使得开发者能够在 TypeScript 中以类型安全的方式访问和使用模块的元数据。 # 11 axios类型 **InternalAxiosRequestConfig** 、AxiosResponse # 12 store中app类型 ``` import type { App } from "vue"; import { createPinia } from "pinia"; const store = createPinia(); // 全局注册 store export function setupStore(app: App) { app.use(store); } ``` # 13 路由类型 ``` import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; ``` # 14 请求类型和返回结果 AxiosPromise ``` import { AxiosPromise } from 'axios'; import { LoginData, LoginResult } from './model'; export function loginApi(data: LoginData): AxiosPromise { return request({ url: '/api/v1/auth/login', method: 'post', params: data });} ``` ``` /** * 登录请求参数 */ export interface LoginData { /** * 用户名 */ username: string; /** * 密码 */ password: string; } /** * 登录响应 */ export interface LoginResult { /** * 访问token */ accessToken?: string; /** * 过期时间(单位:毫秒) */ expires?: number; /** * 刷新token */ refreshToken?: string; /** * token 类型 */ tokenType?: string; } ``` # 15 路由@/views/login/index.vue ``` "compilerOptions": { "paths": { //路径映射,相对于baseUrl "@/*": ["src/*"] } } ``` # # 17 全局声明类型变量 https://eslint.org/docs/latest/use/configure/language-options#specifying-globals 1 声明全局文件 global.d.ts ```JS declare global { interface OptionType = { /** * 组件数据源 */ /** 值 */ value: string | number /** 文本 */ label: string /** 子列表 */ children?: OptionType[] } } export { } ``` 2 eslint.config.js ```JS languageOptions: { globals: { OptionType: "readonly" } } ``` # 18 获取菜单树形列表与获取菜单表单数据 类型区别 # 19 可以通过扩展 RouteMeta 接口来输入 meta 字段 https://router.vuejs.org/zh/guide/advanced/meta.html#typescript router.d.ts ```JS import "vue-router"; declare module "vue-router" { // https://router.vuejs.org/zh/guide/advanced/meta.html#typescript // 可以通过扩展 RouteMeta 接口来输入 meta 字段 interface RouteMeta { /** 菜单名称 */ title?: string; /** 菜单图标 */ icon?: string; /** 菜单是否隐藏 */ hidden?: boolean; /** 只有一个子路由是否始终显示 */ alwaysShow?: boolean; /** 是否固定页签 */ affix?: boolean; /** 是否缓存页面 */ keepAlive?: boolean; /** 是否在面包屑上隐藏 */ breadcrumb?: boolean; /** 拥有菜单权限的角色编码集合 */ roles?: string[]; } } ``` # 20 import.meta.glob import.meta.glob 是 Vite 提供的一个特殊的函数,它允许你在 JavaScript 或 TypeScript 模块中动态地导入多个模块。这在处理一组相关文件或基于某些条件动态导入模块时非常有用。 import.meta.glob 函数接受一个 glob 表达式作为参数,并返回一个对象,其中键是匹配的文件路径(相对于当前模块),值是动态导入这些模块的函数。 例如,假设你有一个目录结构如下: bash src/ ├── components/ │ ├── ComponentA.vue │ ├── ComponentB.vue │ └── ... └── main.js 你可以使用 import.meta.glob 来动态地导入所有的 Component 文件: javascript // main.js const modules = import.meta.glob('./components/*.vue'); for (const path in modules) { modules[path]().then((mod) => { console.log(mod); // 这里你可以访问到每个组件的默认导出 }); } 在这个例子中,modules 是一个对象,其键是组件文件的路径,值是函数,这些函数在调用时会返回一个 Promise,该 Promise 解析为对应组件模块的导出。 需要注意的是,import.meta.glob 是 Vite 特有的功能,因此如果你正在使用其他构建工具或打包器,这个功能可能不可用。此外,由于它使用了动态导入,所以这些模块不会被包含在初始的 bundle 中,而是在运行时根据需要加载,这有助于减少初始加载时间。 # 21 vue 中const Layout = () => import("@/layout/index.vue")出现Cannot find module '@/layout/index.vue' or its corresponding type declarations # 22 自定义路由类型和路由类型以及两者间的相互转换 RouteVO、RouteRecordRaw ``` const filterAsyncRoutes = (routes: RouteVO[], roles: string[]) => { const asyncRoutes: RouteRecordRaw[] = []; routes.forEach((route) => { const tmpRoute = { ...route } as RouteRecordRaw; // 深拷贝 route 对象 避免污染 if (hasPermission(roles, tmpRoute)) { // 如果是顶级目录,替换为 Layout 组件 if (tmpRoute.component?.toString() == "Layout") { tmpRoute.component = Layout; } else { // 如果是子目录,动态加载组件 const component = modules[`../../views/${tmpRoute.component}.vue`]; if (component) { tmpRoute.component = component; } else { tmpRoute.component = modules[`../../views/error-page/404.vue`]; } } if (tmpRoute.children) { tmpRoute.children = filterAsyncRoutes(route.children, roles); } asyncRoutes.push(tmpRoute); } }); return asyncRoutes; }; ``` # 23 **自定义指令** https://cn.vuejs.org/guide/reusability/custom-directives.html # 24 重置路由 # 25 国际化分为两个部分,Element Plus 框架国际化(官方提供了国际化方式)和自定义国际化(通过 vue-i18n 国际化插件) # 26 全局变量 vite.config.js ``` /** 平台的名称、版本、运行所需的`node`版本、依赖、构建时间的类型提示 */ const __APP_INFO__ = { pkg: { name, version, engines, dependencies, devDependencies }, buildTimestamp: Date.now(), }; export default defineConfig({ define: { __APP_INFO__: JSON.stringify(__APP_INFO__) } }) ``` env.d.ts ``` /** * 平台的名称、版本、运行所需的`node`版本、依赖、构建时间的类型提示 */ declare const __APP_INFO__: { pkg: { name: string; version: string; engines: { node: string; }; dependencies: Record; devDependencies: Record; }; buildTimestamp: number; }; ``` # 27 declare声明变量还是接口 ``` /** * 平台的名称、版本、运行所需的`node`版本、依赖、构建时间的类型提示 */ declare const __APP_INFO__: { pkg: { name: string; version: string; engines: { node: string; }; dependencies: Record; devDependencies: Record; }; buildTimestamp: number; }; ``` # 28 dependencies: Record 在 TypeScript 或 JavaScript 的上下文中,Record 是一个从 TypeScript 的标准库中引入的泛型工具类型。它用于创建一个对象类型,其中对象的键是字符串类型(string),而值也是字符串类型(string)。 具体来说,Record 是一个泛型类型,其中 K 是键的类型,而 T 是值的类型。当你使用 Record 时,你正在创建一个对象类型,其键和值都是字符串。 例如: typescript const dependencies: Record = { "lodash": "^4.17.20", "express": "^4.17.1", "axios": "^0.21.1" }; 在这个例子中,dependencies 是一个对象,它的键(如 "lodash"、"express"、"axios")和值(如 "^4.17.20"、"^4.17.1"、"^0.21.1")都是字符串。这通常用于表示依赖项及其对应的版本号。 这个类型在多种场合下都很有用,特别是当你需要定义一个对象,其中键和值都是预定义的类型时。在包管理或配置文件的上下文中,Record 是一个常见的类型,用于表示键-值对,其中键和值都是字符串。 # 29 devDependencies中的类型 ``` /** * 平台的名称、版本、运行所需的`node`版本、依赖、构建时间的类型提示 */ declare const __APP_INFO__: { pkg: { name: string; version: string; engines: { node: string; }; dependencies: Record; devDependencies: Record; }; buildTimestamp: number; }; ``` # 30 useStorage ``` const useStorage: typeof import('@vueuse/core')['useStorage'] ``` 在Vue 3中,useStorage是VueUse库中的一个实用函数,基于Vue 3的Composition API。它主要用于实现本地存储(localStorage)或会话存储(sessionStorage)的功能。 useStorage函数接受几个参数,其中最重要的是键名(key)和默认值(defaults)。键名用于在本地存储中标识特定的值,而默认值是在本地存储中没有找到对应的值时使用的初始值。此外,它还可以接受一个可选的存储对象(默认为localStorage)和一些额外的配置选项。 使用useStorage时,你可以将键名和默认值作为参数传递给它,并获取一个响应式的引用,该引用可以用来读取和设置本地存储中的值。这使得在Vue组件中管理和响应本地存储中的变化变得更加简单和直观。 下面是一个简单的使用示例: javascriptimport { useStorage } from '@vueuse/core'; // 设置一个本地存储的值 const userInfoStore = useStorage('my-store', { tenantId: '', userInfo: {}, permission: '', roles: [], menuId: {}, menu: [], routerMenu: [], token: '', refreshToken: '', }); // 读取本地存储的值 const userInfo = userInfoStore.value.userInfo; // 更新本地存储的值 userInfoStore.value.userInfo = newData; 在上面的例子中,useStorage用于创建一个响应式的引用userInfoStore,它关联到本地存储中键名为'my-store'的项。通过修改userInfoStore.value.userInfo,你可以更新本地存储中的对应值,而当这个值发生变化时,你的Vue组件将会响应这些变化。 请注意,使用useStorage时要确保正确地处理默认值,并且在跨页面或组件访问值时,要意识到本地存储是全局共享的,所以要小心避免意外地覆盖其他部分代码的数据。 另外,虽然useStorage提供了方便的本地存储抽象,但在生产环境中使用时,仍需要考虑到数据安全性、隐私保护以及性能影响等因素。确保只存储必要的数据,并遵循最佳实践来处理敏感信息。 # 31 Argument of type 'AxiosResponse' is not assignable to parameter of type 'RouteVO[]' 错误信息 "Argument of type 'AxiosResponse' is not assignable to parameter of type 'RouteVO[]'" 表示你正在尝试将一个 AxiosResponse 类型的对象赋值给一个期望 RouteVO[] 类型参数的地方。这通常发生在使用 Axios 进行 HTTP 请求时,你尝试直接将 Axios 的响应对象作为数组使用,而实际上你应该使用响应对象中的数据(data)部分。 在 Axios 中,AxiosResponse 是一个泛型类型,它包含响应的多个方面,如数据(data)、状态(status)、头部(headers)等。当你声明一个 Axios 响应的类型时,你通常指定响应数据的类型,以及可能的错误类型。 例如,如果你有一个 API 调用,它返回一个 RouteVO 类型的数组,并且你使用 Axios 来执行这个调用,你应该这样做: typescript import axios from 'axios'; interface RouteVO { // ... 定义 RouteVO 的字段 ... } async function fetchRoutes(): Promise { try { const response = await axios.get('你的API地址'); return response.data; // 使用 response.data 来获取 RouteVO[] 类型的数组 } catch (error) { // 处理错误 console.error('Error fetching routes:', error); throw error; } } 在上面的代码中,axios.get 调用指定了期望的响应数据类型为 RouteVO[],并且可能有一个任意类型的错误。然后,在 fetchRoutes 函数中,我们返回 response.data 而不是整个 response 对象,因为 response.data 才是我们需要的 RouteVO[] 类型的数组。 如果你尝试直接将 response 对象(类型为 AxiosResponse)赋值给一个期望 RouteVO[] 类型的地方,TypeScript 编译器会抛出你看到的错误,因为它知道这两种类型是不兼容的。你需要确保只使用响应对象中的数据部分。 # 32 @typescript-eslint/no-explicit-any与request情况下,如何进行解决 在 TypeScript 中,当你看到 @typescript-eslint/no-explicit-any 规则报错,并且你的代码包含 request 这样的泛型调用时,这通常意味着你正在使用 any 类型,而 ESLint 规则希望你用一个更具体的类型来代替它。 在 request 的上下文中,第一个泛型参数 any 表示函数的第一个参数可以是任何类型。如果 request 函数的第一个参数确实可以是任意类型,那么你可以有几种选择来处理这个警告: **如果你确定需要使用 any**: 如果你确信在这种情况下使用 any 是合理的(虽然这通常应该是一个例外而不是常态),你可以通过添加 ESLint 规则的忽略注释来绕过这个规则: typescript // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: MenuVO[] = request(someUrl, someOptions); 或者,如果你只想忽略这一行中的规则,你可以使用 // @ts-ignore(但这会忽略所有 TypeScript 错误,不仅仅是 ESLint 规则): typescript // @ts-ignore const result: MenuVO[] = request(someUrl, someOptions); 使用更具体的类型: 最好的做法通常是避免使用 any,并尽量使用更具体的类型。如果 request 函数的第一个参数有一个明确的类型,你应该使用那个类型。例如: typescript interface RequestOptions { // 定义你的请求选项 } const result: MenuVO[] = request(someUrl, someOptions); 在这个例子中,我们定义了一个 RequestOptions 接口来代替 any。 如果 request 是第三方库的一部分: 如果 request 函数来自第三方库,并且你无法更改其泛型参数,你可以考虑在你的代码中创建一个包装函数,该函数使用更具体的类型,并调用原始的 request 函数。 typescript function myRequest(url: string, options: T): Promise { return request(url, options as any); } 在这个例子中,MySpecificType 是你定义的一个更具体的类型,它描述了 request 函数第一个参数应该具有的结构。注意,我们在这里仍然使用了 as any 断言,但这是在我们自己的包装函数内部,它应该比直接在调用点使用 any 更容易管理和理解。 配置 ESLint 规则: 如果你认为在你的项目中使用 any 是可以接受的,你可以考虑调整 ESLint 规则的配置,以允许在特定情况下使用 any。但请注意,这通常会降低类型安全性,因此应该谨慎使用。 总之,处理 @typescript-eslint/no-explicit-any 警告的最佳方式通常是避免使用 any 类型,并尽量使用更具体、更明确的类型。在确实需要使用 any 的情况下,你可以通过 ESLint 忽略注释来绕过规则,但应尽量避免这种情况。 # 33 全局样式使用 ```JS css: { // CSS 预处理器 preprocessorOptions: { //define global scss variable scss: { javascriptEnabled: true, additionalData: `@use "@/styles/variables.scss" as *;`, }, }, }, ``` # 34 检查输入大小写 ```JS ``` ```js /** 检查输入大小写 */ function checkCapslock(event: KeyboardEvent) { // 防止浏览器密码自动填充时报错 if (event instanceof KeyboardEvent) { isCapslock.value = event.getModifierState("CapsLock"); } } ``` # 35 表单Ref的类型 FormInstance ```JS import type { FormInstance } from "element-plus"; const loginFormRef = ref(); // 登录表单ref ``` # 36 请求头的header ``` static login(data: LoginData) { const formData = new FormData(); formData.append("username", data.username); formData.append("password", data.password); formData.append("captchaKey", data.captchaKey || ""); formData.append("captchaCode", data.captchaCode || ""); return request({ url: "/api/v1/auth/login", method: "post", data: formData, headers: { "Content-Type": "multipart/form-data", }, }); } ``` ``` //创建axios实例 const service = axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API, timeout: 50000, headers: { 'Content-Type': 'application/json;charset=utf-8', }, }) ``` # 37 route.query中的类型和query.redirect的类型 ``` import { LocationQuery, LocationQueryValue } from "vue-router"; ``` ``` const query: LocationQuery = route.query; const redirect = (query.redirect as LocationQueryValue) ?? "/"; ``` # 38 路由的跳转中的router来源 ``` import router from "@/router"; ``` ``` router.push({ path: redirect, query: otherQueryParams }); ``` # 39 请求的类型 ``` static getCaptcha() { return request({ url: "/api/v1/auth/captcha", method: "get", }); } ``` # 40 跨域 ``` server: { // 允许IP访问 host: "0.0.0.0", // 应用端口 (默认:3000) port: Number(env.VITE_APP_PORT), // 运行是否自动打开浏览器 open: true, proxy: { /** 代理前缀为 /dev-api 的请求 */ [env.VITE_APP_BASE_API]: { changeOrigin: true, // 接口地址 target: env.VITE_APP_API_URL, rewrite: (path) => path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""), }, }, }, ``` # 41 配置文件环境变量的获取loadEnv、defineConfig中回调函数参数的类型ConfigEnv和函数返回值UserConfig ``` export default defineConfig(({ mode }: ConfigEnv): UserConfig => { const env = loadEnv(mode, process.cwd()); return { server: { // 允许IP访问 host: "0.0.0.0", // 应用端口 (默认:3000) port: Number(env.VITE_APP_PORT), // 运行是否自动打开浏览器 open: true, proxy: { /** 代理前缀为 /dev-api 的请求 */ [env.VITE_APP_BASE_API]: { changeOrigin: true, // 接口地址 target: env.VITE_APP_API_URL, rewrite: (path) => path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""), }, }, } } }) ``` # 42 主题的置换(store中的watch) ``` import Color from "color"; ``` ``` watch( [theme, themeColor], ([newTheme, newThemeColor], [oldTheme, oldThemeColor]) => { if (newTheme !== oldTheme) { if (newTheme === ThemeEnum.DARK) { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.remove("dark"); } } if (newThemeColor !== oldThemeColor) { const rootStyle = document.documentElement.style; rootStyle.setProperty(`--el-color-primary`, newThemeColor); rootStyle.setProperty(`--el-color-primary-dark-2`, newThemeColor); for (let i = 1; i < 10; i++) { rootStyle.setProperty( `--el-color-primary-light-${i}`, `${Color(newThemeColor).alpha(1 - i * 0.1)}` ); } } }, { immediate: true, // 立即执行,确保在侦听器创建时执行一次 } ); ``` 样式 ``` html.dark .login-container { background: url("@/assets/images/login-bg-dark.jpg") no-repeat center right; } ``` # 43 组件name的作用 name: "Dashboard", // 用于 keep-alive, 必须与SFC自动推导或者显示声明的组件name一致 # 44 eslint中的rules vue(eslint-plugin-vue) https://eslint.vuejs.org/rules/#priority-a-essential-error-prevention ts([typescript-eslint) https://typescript-eslint.io/rules/ js https://eslint.nodejs.cn/docs/latest/rules/ # 45 animate.css # 46 动画 ## 46.1 ``` {{ defaultSettings.title }} ``` ``` ``` ## 46.2 翻转 ``` ``` ``` .hamburger { vertical-align: middle; cursor: pointer; // transform: scaleX(-1); } .hamburger.is-active { transform: scaleX(1); } ``` ## 46.3 ``` {{ translateRouteTitle(item.meta.title) }} {{ translateRouteTitle(item.meta.title) }} ``` ## 46.4 css中的animation ``` .github-corner:hover .octo-arm { animation: octocat-wave 560ms ease-in-out; } @keyframes octocat-wave { 0%, 100% { transform: rotate(0); } 20%, 60% { transform: rotate(-25deg); } 40%, 80% { transform: rotate(10deg); } } ``` ## 46.5 useTransition、TransitionPresets ``` interface CardProp { title: string; tagType: EpPropMergeType< StringConstructor, "primary" | "success" | "info" | "warning" | "danger", unknown >; tagText: string; count: any; dataDesc: string; iconClass: string; } ``` ``` // 卡片数量 const cardData = ref([ { title: "访客数", tagType: "success", tagText: "日", count: visitCountOutput, dataDesc: "总访客数", iconClass: "visit", }, { title: "IP数", tagType: "success", tagText: "日", count: dauCountOutput, dataDesc: "总IP数", iconClass: "ip", }, { title: "销售额", tagType: "primary", tagText: "月", count: amountOutput, dataDesc: "总IP数", iconClass: "money", }, { title: "订单量", tagType: "danger", tagText: "季", count: orderCountOutput, dataDesc: "总订单量", iconClass: "order", }, ]); ``` ``` // 访客数 const visitCount = ref(0); const visitCountOutput = useTransition(visitCount, { duration: duration, transition: TransitionPresets.easeOutExpo, }); visitCount.value = 2000; ``` ``` import { useTransition, TransitionPresets } from "@vueuse/core"; ``` ``` {{ item.title }} {{ item.tagText }} {{ Math.round(item.count) }} {{ item.dataDesc }} {{ Math.round(item.count * 15) }} ``` 移动端 ``` @media (width <= 500px) { .github-corner .octo-arm { animation: octocat-wave 560ms ease-in-out; } .github-corner:hover .octo-arm { animation: none; } } ``` # 47 vue引入图片 ``` const logo = ref(new URL(`../../../../assets/logo.png`, import.meta.url).href); ``` # 48 mix布局获取左侧的data,并跳转第一个菜单 ``` /** * 默认跳转到左侧第一个菜单 */ const goToFirstMenu = (menus: RouteRecordRaw[]) => { if (menus.length === 0) return; const [first] = menus; if (first.children && first.children.length > 0) { goToFirstMenu(first.children as RouteRecordRaw[]); } else if (first.name) { router.push({ name: first.name, }); } ``` ## 48.1 获取数组中的first ``` const [first] = menus; ``` # 49 template中如何实现国际化 ``` // translate router.meta.title, be used in breadcrumb sidebar tagsview import i18n from "@/lang/index"; export function translateRouteTitle(title: any) { // 判断是否存在国际化配置,如果没有原生返回 const hasKey = i18n.global.te("route." + title); if (hasKey) { const translatedTitle = i18n.global.t("route." + title); return translatedTitle; } return title; } ``` # 50 emit事件 hamburger.vue ``` const emit = defineEmits(["toggleClick"]); function toggleClick() { emit("toggleClick"); } ``` ``` ``` # 51 判断是否为首页 ``` function isDashboard(route: RouteLocationMatched) { const name = route && route.name; if (!name) { return false; } return ( name.toString().trim().toLocaleLowerCase() === "Dashboard".toLocaleLowerCase() ); } ``` # 52 breadcrumbs的跳转path-to-regexp ``` import { compile } from "path-to-regexp"; ``` ``` function handleLink(item: any) { const { redirect, path } = item; if (redirect) { router.push(redirect).catch((err) => { console.warn(err); }); return; } router.push(pathCompile(path)).catch((err) => { console.warn(err); }); } ``` ``` const pathCompile = (path: string) => { const { params } = currentRoute; const toPath = compile(path); return toPath(params); }; ``` # 53 全屏 ``` const { isFullscreen, toggle } = useFullscreen(); ``` # 54 全局声明 ## 54.1 页签对象TagView # 55 entries ``` function delVisitedView(view: TagView) { return new Promise((resolve) => { for (const [i, v] of visitedViews.value.entries()) { // 找到与指定视图路径匹配的视图,在已访问视图列表中删除该视图 if (v.path === view.path) { visitedViews.value.splice(i, 1); break; } } resolve([...visitedViews.value]); }); } ``` # 56 路径拼接path-browserify ``` import path from "path-browserify"; ``` ``` /** * 解析路径 * * @param routePath 路由路径 /user */ function resolvePath(routePath: string) { if (isExternal(routePath)) { return routePath; } if (isExternal(props.basePath)) { return props.basePath; } // 完整绝对路径 = 父级路径(/system) + 路由路径(/user) const fullPath = path.resolve(props.basePath, routePath); return fullPath; } ``` # 57 v-bind ``` ``` # 58 显示具有单个子路由的菜单项或没有子路由的父路由(a、router-link) 、显示具有多个子路由的父菜单项 # 59 inheritAttrs与$attrs https://blog.csdn.net/lijiahui_/article/details/122888906 ``` defineOptions({ name: "SidebarMenuItem", inheritAttrs: false, }); ``` inheritAttrs 是 Vue.js 中的一个选项,用于控制父组件的非 prop 属性如何被绑定到子组件的根元素上。 当 inheritAttrs 设置为 true(默认值)时,父组件传递给子组件的、未被识别为 prop 的属性,会自动绑定到子组件的根元素上。如果子组件的根元素本身也声明了相同的属性,那么父组件传递的属性会覆盖子组件根元素上的属性。 如果 inheritAttrs 设置为 false,那么父组件传递的非 prop 属性将不会被自动绑定到子组件的根元素上。但是,子组件内部仍然可以通过 this.$attrs 来访问这些属性,并可以选择性地将其绑定到子组件的其他元素上。 这个选项在某些情况下非常有用,特别是当你想要更细粒度地控制父组件属性如何被应用到子组件上时。例如,你可能想要将某些属性绑定到子组件内部的特定元素,而不是根元素。 需要注意的是,inheritAttrs 选项不会影响 class 和 style 的绑定,这些绑定总是会应用到子组件的根元素上。 # 60 isNest # 61 icon布局 ``` ``` ``` {{ translateRouteTitle(title) }} ``` # 62 element-plus 图标自动引入 unplugin-icons 使用 [unplugin-icons](https://github.com/antfu/unplugin-icons) 和 [unplugin-auto-import](https://github.com/antfu/unplugin-auto-import) 从 iconify 中自动导入任何图标集。 您可以参考[此模板](https://github.com/sxzz/element-plus-best-practices/blob/db2dfc983ccda5570033a0ac608a1bd9d9a7f658/vite.config.ts#L21-L58)。 https://icon-sets.iconify.design/ ``` ``` # 63 优化TagsView、布局 # 64 store解构(storeToRefs) ``` const { visitedViews } = storeToRefs(tagsViewStore); ``` # 65 鼠标中键和右键 鼠标中键:@click.middle 鼠标右键:@contextmenu.prevent ``` {{ translateRouteTitle(tag.title) }} ``` ## 65.1 获取当前组件的实例 ``` const { proxy } = getCurrentInstance()!; const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left const offsetWidth = proxy?.$el.offsetWidth; // container width ``` # 66 理清redirect作用 # 67 store结合computed、抽屉使用 ``` ``` ``` /** * 1 点击设置settingStore.settingsVisible = true 不触发set * 2 点击document隐藏,则触发set */ const settingsVisible = computed({ get() { return settingsStore.settingsVisible; }, set() { settingsStore.settingsVisible = false; }, }); ``` ``` ``` # 68 v-model 案例一 ``` ``` ``` ``` 案例二 ``` ``` ``` ``` 案例三: useVModel ``` ``` ``` const props = defineProps({ modelValue: { type: [Number], default: undefined, }, }); const emits = defineEmits(["node-click"]); const deptId = useVModel(props, "modelValue", emits); /** 部门树节点 Click */ function handleNodeClick(data: { [key: string]: any }) { console.log("data 部门树节点", data); deptId.value = data.value; emits("node-click"); } ``` # 69 watch与watchEffect 在 Vue 3 中,watch 和 watchEffect 都是用于响应式地观察和响应 Vue 组件中的数据变化,但它们之间存在一些关键的区别。以下是它们之间的主要差异: 1. 观察的数据源 watch: 你需要明确指定要观察的数据源。这可以是一个响应式引用(ref)、一个计算属性(computed)、或者是一个对象,其中包含了多个要观察的属性和对应的回调函数。 watchEffect: 它会自动收集其执行函数内部依赖的数据源。你不需要显式地指定要观察哪些数据,Vue 会自动跟踪你在 watchEffect 的回调函数中所使用的响应式数据。 2. 执行时机 watch: 在观察的数据源发生变化时执行回调函数。如果数据源没有变化,那么回调函数不会被触发。 watchEffect: 在组件渲染时立即执行一次,并自动收集依赖。之后,当依赖的数据源发生变化时,它也会重新执行。 3. 停止观察 watch: 返回的观察对象有一个 stop 方法,可以用于停止观察。 watchEffect: 返回一个停止观察的函数,调用这个函数可以停止 watchEffect 的执行。 4. 响应式副作用 watch: 通常用于观察特定数据的变化,并在变化时执行某些操作。 watchEffect: 更适合处理具有响应式副作用的场景,即当某些数据变化时,需要自动执行一系列的操作,而不需要显式地列出这些操作所依赖的数据。 5. 使用场景 watch: 当你需要明确知道哪些数据变化时需要执行特定的操作时,使用 watch 是更合适的。 watchEffect: 当你只需要响应某些数据的变化,而不关心具体是哪些数据变化时,或者当你想自动跟踪并响应多个数据的变化时,使用 watchEffect 可能更方便。 总结 watch 和 watchEffect 都是 Vue 3 中强大的响应式工具,它们提供了不同的方式来观察和响应数据的变化。选择使用哪一个取决于你的具体需求和场景。在大多数情况下,watch 提供了更多的控制和灵活性,而 watchEffect 则更适合处理具有响应式副作用的场景。 # 70 监听屏幕宽度的变化 ``` const width = useWindowSize().width; watchEffect(() => { appStore.toggleDevice( width.value < WIDTH_DESKTOP ? DeviceEnum.MOBILE : DeviceEnum.DESKTOP ); if (width.value >= WIDTH_DESKTOP) { appStore.openSideBar(); } else { appStore.closeSideBar(); } }); ``` # 71 组件 ## 71.1异步组件 ``` const chartComponent = (item: string) => { return defineAsyncComponent(() => import(`./components/${item}.vue`)); }; ``` # 72 echarts ## 72.1 echarts转化图片下载 ``` const downloadEchart = () => { // 获取画布图表地址信息 const img = new Image(); img.src = chart.value.getDataURL({ type: "png", pixelRatio: 1, backgroundColor: "#fff", }); // 当图片加载完成后,生成 URL 并下载 img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext("2d"); if (ctx) { ctx.drawImage(img, 0, 0, img.width, img.height); const link = document.createElement("a"); link.download = `业绩柱状图.png`; link.href = canvas.toDataURL("image/png", 0.9); document.body.appendChild(link); link.click(); link.remove(); } }; ``` ## 72.2 大小自适应 chart.value.resize(); ``` onMounted(() => { // 图表初始化 chart.value = markRaw( echarts.init(document.getElementById(props.id) as HTMLDivElement) ); chart.value.setOption(options); // 大小自适应 window.addEventListener("resize", () => { chart.value.resize(); }); }); ``` ## 72.3 markRaw ``` const chart = ref(""); chart.value = markRaw( echarts.init(document.getElementById(props.id) as HTMLDivElement) ); ``` 在 Vue 3 中,markRaw 是一个用于标记一个对象使其保持原始状态,避免 Vue 的响应式系统将其转换为响应式对象的工具函数。当你有一个对象,并且你确定它不需要是响应式的,或者它已经是响应式的,但你不希望 Vue 对其进行进一步的响应式处理时,你可以使用 markRaw。 这里有几个使用 markRaw 的场景: 1 避免不必要的响应式转换:当你知道某个对象不应该被 Vue 转换为响应式时,使用 markRaw 可以提高性能并避免不必要的内存占用。 2 处理第三方库或插件返回的对象:有时,第三方库或插件会返回对象,这些对象可能已经是响应式的,或者它们可能包含复杂的数据结构,你不希望 Vue 对其进行转换。 3 优化性能:对于大型对象或数据结构,转换为响应式可能需要相当多的计算和内存。如果确定某个对象不需要是响应式的,使用 markRaw 可以避免这种开销。 下面是一个简单的示例,展示了如何使用 markRaw: javascript import { ref, reactive, markRaw } from 'vue'; const rawObject = { foo: 'bar' }; const rawObjectMarked = markRaw(rawObject); // rawObjectMarked 不会被 Vue 转换为响应式对象 const reactiveObject = reactive({ baz: 'qux' }); const rawNestedObject = { nested: rawObjectMarked }; // 当 reactiveObject 嵌套 rawNestedObject 时,rawNestedObject 仍然保持原始状态 reactiveObject.nested = rawNestedObject; console.log(reactiveObject.nested.foo); // 输出 'bar' // 修改 rawNestedObject.nested.foo 不会触发 Vue 的响应式更新 需要注意的是,markRaw 只能防止 Vue 的响应式系统进一步转换对象,但它不会撤销已经存在的响应式属性。如果对象已经是响应式的,并且你使用 markRaw 标记它,那么它的响应式属性仍然会保持响应式。 最后,尽管 markRaw 可以用来避免不必要的响应式转换,但在大多数情况下,你应该让 Vue 自动处理响应式,除非你有明确的理由不这样做。 # vue3中markRaw详解 https://blog.csdn.net/weixin_39519297/article/details/137222400 ## 72.4 chart.value = markRaw( echarts.init(document.getElementById(props.id) as HTMLDivElement) )的作用 在 Vue 3 中,markRaw 的主要用途是告诉 Vue 的响应式系统,某个对象不需要被转换为响应式对象。这在处理那些已经具有复杂状态管理或不需要 Vue 的响应式特性的对象时非常有用。 在您提供的代码示例中: javascript chart.value = markRaw( echarts.init(document.getElementById(props.id) as HTMLDivElement) ) echarts.init 是用于初始化 ECharts 实例的方法,它返回一个 ECharts 实例对象。这个对象内部维护了自己的状态,并且有复杂的方法用于配置图表、渲染数据等。由于 ECharts 已经自己处理了状态的更新和渲染,因此不需要 Vue 的响应式系统来跟踪这个对象的变化。 使用 markRaw 将 ECharts 实例标记为“原始”对象有几个好处: 1 性能提升:Vue 的响应式系统需要额外的计算和内存来跟踪响应式对象的变化。对于大型对象或数据结构复杂的对象(如 ECharts 实例),这种开销可能是不必要的,并且可能会影响性能。通过 markRaw 将其标记为非响应式,可以避免这些额外的开销。 2 避免不必要的警告或错误:如果 Vue 尝试将 ECharts 实例转换为响应式对象,它可能会遇到一些问题,因为这个对象可能不是一个普通的 JavaScript 对象,而是具有特定方法和属性的复杂实例。这可能导致 Vue 发出警告或遇到错误。通过 markRaw,可以避免这些问题。 3 保持 ECharts 实例的独立性:ECharts 实例有自己的生命周期和更新机制。将其标记为非响应式可以确保 Vue 不会意外地干扰 ECharts 的正常操作。 总之,使用 markRaw 在这个场景中是一个合理的选择,因为它可以避免不必要的性能开销,同时确保 ECharts 实例能够独立于 Vue 的响应式系统工作。 # 73 下载导入模板 ``` /** 下载导入模板 */ function downloadTemplate() { UserAPI.downloadTemplate().then((response: any) => { console.log("------下载导入模板------", response); const fileData = response.data; // 文件名content-disposition: "attachment; filename=%E7%94%A8%E6%88%B7%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx" const fileName = decodeURI( response.headers["content-disposition"].split(";")[1].split("=")[1] ); // 文件类型 content-type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" const fileType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; const blob = new Blob([fileData], { type: fileType }); const downloadUrl = window.URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = downloadUrl; downloadLink.download = fileName; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); window.URL.revokeObjectURL(downloadUrl); }); } ``` responseType: "arraybuffer" ``` /** * 下载用户导入模板 * * @returns */ static downloadTemplate() { return request({ url: "/api/v1/users/template", method: "get", responseType: "arraybuffer", }); } ``` ## 73.1 window.URL.createObjectURL https://developer.mozilla.org/en-US/search?q=window.URL.createObjectURL ## 73.2 JS new Blob([fileData], { type: fileType }); 在JavaScript中,Blob 对象表示一个不可变的原始数据块,这些数据可以是文本或二进制数据。Blob 对象通常用于处理文件或数据块,特别是在处理用户上传的文件或创建下载链接时。 new Blob([fileData], { type: fileType }); 这行代码是创建一个新的 Blob 对象的语法。 fileData 是一个数组,包含要放入 Blob 对象中的数据。这些数据可以是 ArrayBuffer、ArrayBufferView(如 Uint8Array)、Blob、DOMString 等类型的对象或字符串。 { type: fileType } 是一个选项对象,用于指定 Blob 对象中数据的 MIME 类型。fileType 是一个字符串,如 "text/plain" 或 "image/jpeg",它表示 Blob 对象中数据的类型。 例如: javascript // 创建一个包含文本数据的 Blob 对象 const textBlob = new Blob(["Hello, world!"], { type: 'text/plain' }); // 创建一个包含二进制数据的 Blob 对象 const arrayBuffer = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]).buffer; const binaryBlob = new Blob([arrayBuffer], { type: 'application/octet-stream' }); 在上面的例子中,textBlob 包含一个文本字符串,而 binaryBlob 包含一个二进制数组缓冲区。通过指定 MIME 类型,我们可以告诉浏览器或其他处理 Blob 的系统如何解释或处理这些数据。 一旦你创建了 Blob 对象,你可以使用它来创建对象URL(通过 URL.createObjectURL()),然后将其用于 标签的 href 属性以创建下载链接,或者在 FileReader 对象中读取其内容等。 ## 73.3 window.URL.revokeObjectURL https://developer.mozilla.org/en-US/search?q=window.URL.revokeObjectURL ## 73.4 导出数据 ``` /** 导出用户 */ function handleExport() { UserAPI.export(queryParams).then((response: any) => { const fileData = response.data; const fileName = decodeURI( response.headers["content-disposition"].split(";")[1].split("=")[1] ); const fileType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; const blob = new Blob([fileData], { type: fileType }); const downloadUrl = window.URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = downloadUrl; downloadLink.download = fileName; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); window.URL.revokeObjectURL(downloadUrl); }); } ``` responseType: "arraybuffer" ``` /** * 导出用户 * * @param queryParams * @returns */ static export(queryParams: UserQuery) { return request({ url: "/api/v1/users/export", method: "get", params: queryParams, responseType: "arraybuffer", }); } ``` ## 73.5 文件上传(点击上传、拖拽上传) 设置 `limit` 和 `on-exceed` 可以在选中时自动替换上一个文件 ``` /** Excel文件 Exceed */ function handleFileExceed(files: any) { uploadRef.value!.clearFiles(); const file = files[0]; file.uid = genFileId(); uploadRef.value!.handleStart(file); importData.file = file; } ``` # 74 js data.path ?? ""; 在 JavaScript 中,data.path ?? "" 是一个使用了“空值合并运算符”(Nullish Coalescing Operator)?? 的表达式。这个运算符在左侧的表达式是 null 或 undefined 时返回其右侧的表达式,否则返回左侧的表达式。 具体来说,data.path ?? "" 这段代码的含义是: 尝试访问 data 对象的 path 属性。 如果 data.path 的值是 null 或 undefined,则整个表达式的值会是空字符串 ""。 如果 data.path 的值不是 null 或 undefined,则整个表达式的值会是 data.path 的实际值。 空值合并运算符 ?? 和逻辑或运算符 || 在某些情况下看起来类似,但它们的行为在处理 0、false、NaN、空字符串 "" 等“假值”(falsy values)时是不同的。|| 运算符会将这些假值视为 false,并返回其右侧的表达式,而 ?? 只在 null 或 undefined 时返回其右侧的表达式。 示例:javascript ``` let data = { path: null }; let value = data.path ?? ""; // value 将是 "" data = { path: "example" }; value = data.path ?? ""; // value 将是 "example" data = { path: 0 }; value = data.path ?? ""; // value 将是 0,因为 0 不是 null 或 undefined data = { path: false }; value = data.path ?? ""; // value 将是 false,因为 false 不是 null 或 undefined ``` 这种特性使得 ?? 运算符在需要区分 null 或 undefined 与其他假值的情况下特别有用。 # 75 js ??与?区别 在JavaScript中,?? 和 ? 是两个不同的运算符,它们各自有不同的用途和行为。 ??(空值合并运算符): 当左侧的操作数为 null 或 undefined 时,它返回其右侧的操作数。 如果左侧的操作数不是 null 或 undefined,则返回左侧的操作数本身。 它不会将其他假值(如 0、false、NaN、空字符串 "")视为“空”值。 示例: javascript ``` let a = null; let b = "default"; let value = a ?? b; // value 将会是 "default" a = "not null"; value = a ?? b; // value 将会是 "not null" a = 0; value = a ?? b; // value 将会是 0,因为 0 不是 null 或 undefined ``` ?(三元条件运算符): 这是一个条件运算符,它接受三个操作数。它的形式为 条件 ? 值1 : 值2。 如果“条件”为真,则运算符的结果为“值1”;如果“条件”为假,则结果为“值2”。 示例: javascript ``` let isRainy = true; let message = isRainy ? "Take an umbrella" : "Leave the umbrella at home"; // message 将会是 "Take an umbrella" isRainy = false; message = isRainy ? "Take an umbrella" : "Leave the umbrella at home"; // message 将会是 "Leave the umbrella at home" ``` 区别总结: ?? 是一个空值合并运算符,专门用于处理 null 和 undefined 的情况,而其他假值不会触发其右侧表达式的求值。 ? 是一个三元条件运算符,它基于一个条件的真假来决定返回哪个值。 两者在功能上是完全不同的,并且不能互换使用。选择使用哪一个取决于你的具体需求:如果你想要处理变量是否为 null 或 undefined,则使用 ??;如果你需要根据条件来返回两个不同的值,则使用 ?。 # 76 404页面跳转 ## 76.1 to.match ``` router.beforeEach(async (to, from, next) => { NProgress.start(); const hasToken = localStorage.getItem(TOKEN_KEY); if (hasToken) { if (to.path === "/login") { // 如果已登录,跳转首页 next({ path: "/" }); NProgress.done(); } else { const userStore = useUserStore(); const hasRoles = userStore.user.roles && userStore.user.roles.length > 0; if (hasRoles) { console.log("----to----------", to); // 未匹配到任何路由,跳转404 if (to.matched.length === 0) { console.log("-----from---------", from); from.name ? next({ name: from.name }) : next("/404"); } else { next(); } } else { const permissionStore = usePermissionStore(); try { const { roles } = await userStore.getUserInfo(); const accessRoutes = await permissionStore.generateRoutes(roles); accessRoutes.forEach((route: RouteRecordRaw) => { router.addRoute(route); }); next({ ...to, replace: true }); } catch (error) { // 移除 token 并跳转登录页 await userStore.resetToken(); next(`/login?redirect=${to.path}`); NProgress.done(); } } } } else { // 未登录可以访问白名单页面 if (whiteList.indexOf(to.path) !== -1) { next(); } else { next(`/login?redirect=${to.path}`); NProgress.done(); } } }); ``` ## 76.2 过滤组件 ``` const filterAsyncRoutes = (routes: RouteVO[], roles: string[]) => { const asyncRoutes: RouteRecordRaw[] = []; routes.forEach((route) => { const tmpRoute = { ...route } as RouteRecordRaw; // 深拷贝 route 对象 避免污染 if (hasPermission(roles, tmpRoute)) { // 如果是顶级目录,替换为 Layout 组件 if (tmpRoute.component?.toString() == "Layout") { tmpRoute.component = Layout; } else { // 如果是子目录,动态加载组件 const component = modules[`../../views/${tmpRoute.component}.vue`]; if (component) { tmpRoute.component = component; } else { tmpRoute.component = modules[`../../views/error-page/404.vue`]; } } if (tmpRoute.children) { tmpRoute.children = filterAsyncRoutes(route.children, roles); } asyncRoutes.push(tmpRoute); } }); return asyncRoutes; }; ``` # 77 剪切板 ``` const { copy } = useClipboard(); function handleClipboard(text: any, event: any) { // clipboard(text, event); copy(text) .then(() => { ElMessage.success("Copy successfully"); }) .catch(() => { ElMessage.warning("Copy failed"); }); } ``` # 78 组件封装 # 数据类型 ## 1 breadcrumbs中的数组类型Array ``` import { RouteLocationMatched } from "vue-router"; const breadcrumbs = ref>([]); function getBreadcrumb() { let matched = currentRoute.matched.filter( (item) => item.meta && item.meta.title ); const first = matched[0]; console.log("-----", !isDashboard(first)); if (!isDashboard(first)) { matched = [ { path: "/dashboard", meta: { title: "dashboard" } } as any, ].concat(matched); } breadcrumbs.value = matched.filter((item) => { return item.meta && item.meta.title && item.meta.breadcrumb !== false; }); } ``` ## 2 EpPropMergeType ``` import type { EpPropMergeType } from "element-plus/es/utils/vue/props/types"; ``` ``` interface CardProp { title: string; tagType: EpPropMergeType< StringConstructor, "primary" | "success" | "info" | "warning" | "danger", unknown >; tagText: string; count: any; dataDesc: string; iconClass: string; } ``` ``` const cardData = ref([ { title: "访客数", tagType: "success", tagText: "日", count: visitCountOutput, dataDesc: "总访客数", iconClass: "visit", }, { title: "IP数", tagType: "success", tagText: "日", count: dauCountOutput, dataDesc: "总IP数", iconClass: "ip", }, { title: "销售额", tagType: "primary", tagText: "月", count: amountOutput, dataDesc: "总IP数", iconClass: "money", }, { title: "订单量", tagType: "danger", tagText: "季", count: orderCountOutput, dataDesc: "总订单量", iconClass: "order", }, ]); ``` 分析: 在 Element Plus 中,`EpPropMergeType` 可能是一个内部使用的泛型工具类型,用于合并和扩展组件的属性类型定义 这个类型似乎是为了定义一个属性,它应该是: - 一个字符串(由 `StringConstructor` 指定)。 - 并且这个字符串的值必须是 `"primary"`、`"success"`、`"info"`、`"warning"` 或 `"danger"` 中的一个(由 `"primary" | "success" | "info" | "warning" | "danger"` 联合类型指定)。 - 还可能包含其他未知的类型信息(由 `unknown` 指定) ## 3 static getPage(queryParams: UserQuery) { return request>({ url: "/api/v1/users/page", method: "get", params: queryParams, }); } ## 4 request> ``` static getPage(queryParams: UserQuery) { return request>({ url: "/api/v1/users/page", method: "get", params: queryParams, }); } ``` ``` interface PageResult { /** 数据列表 */ list: T; /** 总数 */ total: number; } ``` ``` export interface UserPageVO { /** * 用户头像地址 */ avatar?: string; /** * 创建时间 */ createTime?: Date; /** * 部门名称 */ deptName?: string; /** * 用户邮箱 */ email?: string; /** * 性别 */ genderLabel?: string; /** * 用户ID */ id?: number; /** * 手机号 */ mobile?: string; /** * 用户昵称 */ nickname?: string; /** * 角色名称,多个使用英文逗号(,)分割 */ roleNames?: string; /** * 用户状态(1:启用;0:禁用) */ status?: number; /** * 用户名 */ username?: string; } ``` ## 5 PageResult ``` /** * 角色分页 */ export type RolePageResult = PageResult; ``` ``` /** * 分页响应对象 */ interface PageResult { /** 数据列表 */ list: T; /** 总数 */ total: number; } ``` ## 6 type与interface区别:泛型 ``` export type DictPageResult = PageResult; ``` # vue3 ## 1 useVModel ## 2 watchEffect()中的flush ``` watchEffect( () => { deptTreeRef.value.filter(deptName.value); }, { flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行 } ); ``` ``` ``` 分析: 在 Vue 3 中,watchEffect() 函数用于自动追踪响应式依赖,并在这些依赖发生变化时重新运行一个副作用函数。关于 flush 选项,它是 watchEffect 的一个配置选项,用于控制副作用函数在组件的哪个生命周期阶段执行。 flush 选项有以下可能的值: **'pre'**:在组件的 beforeUpdate 生命周期钩子之前同步执行副作用函数。这意味着,当依赖项发生变化时,副作用函数会在组件的 DOM 更新之前运行。这在你需要在 DOM 更新之前执行某些操作时特别有用。 **'post'**:在组件的 updated 生命周期钩子之后同步执行副作用函数。这意味着,副作用函数会在组件的 DOM 更新完成之后运行。如果你需要在 DOM 更新后访问或操作最新的 DOM 状态,这个选项会非常有用。 **'sync'**(默认值):副作用函数会立即同步执行,不会等待任何特定的生命周期钩子。这是 watchEffect 的默认行为。 flush 选项允许你更精细地控制副作用函数的执行时机,以满足特定的需求。 下面是一个简单的例子,展示了如何使用 flush 选项: javascript import { ref, watchEffect, onMounted } from 'vue'; export default { setup() { const count = ref(0); // 使用 'pre' 刷新选项 watchEffect( () => { console.log('pre flush:', count.value); // 这里可以执行一些需要在 DOM 更新之前完成的操作 }, { flush: 'pre' } ); // 使用 'post' 刷新选项 watchEffect( () => { console.log('post flush:', count.value); // 这里可以执行一些需要在 DOM 更新之后完成的操作,比如访问最新的 DOM 状态 }, { flush: 'post' } ); onMounted(() => { count.value++; // 触发副作用函数 }); return { count }; } }; 在上面的例子中,我们创建了一个响应式引用 count,并使用 watchEffect 来追踪它的变化。我们分别为 watchEffect 设置了 'pre' 和 'post' 的 flush 选项,以观察副作用函数在何时执行。当 count 的值变化时,你会看到控制台上打印出不同的日志,表明副作用函数是在哪个生命周期阶段执行的。 请注意,正确选择 flush 选项取决于你的具体需求和你希望副作用函数在组件的哪个生命周期阶段执行。大多数情况下,你可能不需要显式设置 flush 选项,因为默认的 'sync' 行为在很多情况下已经足够了。但在某些特定的场景下,使用 'pre' 或 'post' 可以帮助你更好地控制副作用函数的执行顺序。 ## 3 hooks ``` import usePage from "@/hooks/usePage"; const { handleQueryClick, } = usePage(); ``` usePage ``` import type PageContent from "@/components/PageContent/index.vue"; function usePage() { const contentRef = ref>(); // 搜索 function handleQueryClick(queryParams: IObject) { contentRef.value?.fetchPageData(queryParams, true); } } export default usePage; ``` ## 3. 1 ref>()作用 在 Vue 3 和 TypeScript 的上下文中,这行代码片段做了几件事情,我们逐一分解: import type PageContent from "@/components/PageContent/index.vue"; 这行代码使用 import type 语法来仅导入 PageContent 组件的类型定义,而不导入组件的实例或运行时逻辑。@ 符号通常是一个别名,用于指向项目的 src 目录,因此 "@/components/PageContent/index.vue" 可能是指向项目 src/components/PageContent/index.vue 文件的路径。 这样做的好处是,你可以在 TypeScript 代码中获取到组件的类型信息,以便进行类型检查和智能感知,但不会在最终的 JavaScript 打包结果中增加任何关于组件实例的额外代码。这有助于减小最终打包的大小,因为类型信息在编译过程中会被移除。 const contentRef = ref(); 这行代码创建了一个响应式引用 contentRef,其类型被指定为 InstanceType。这里涉及到几个 TypeScript 的高级概念: ref(): 这是 Vue 3 Composition API 中的一个函数,用于创建一个响应式引用。T 是这个引用的值的类型。 InstanceType: 这是一个 TypeScript 内置的工具类型,用于获取一个构造函数类型 T 创建的实例的类型。 typeof PageContent: 由于 PageContent 是通过 import type 导入的类型,typeof PageContent 会得到 PageContent 组件的构造函数类型(如果有的话)。在 Vue 中,一个 .vue 文件在被导入时,其默认导出通常是一个定义了组件选项的对象,这个对象也可以被当作一个构造函数来看待(尽管它实际上是一个对象)。 InstanceType: 综合上述,这个类型表示的是由 PageContent 组件的“构造函数”创建的实例的类型。在 Vue 的上下文中,这通常就是组件实例的类型,包括它的数据、方法、计算属性等。 因此,const contentRef = ref>(); 这行代码创建了一个响应式引用 contentRef,你可以用它来持有 PageContent 组件实例的引用。在 Vue 组件的 setup 函数或其他 Composition API 的上下文中,你可能会使用这个引用来操作或访问组件实例的属性和方法。 注意:在实际应用中,你可能还需要确保 PageContent 组件实际上是一个可以通过 InstanceType 获取类型的组件,并且这个类型是你期望的。在某些情况下,你可能需要手动为组件定义类型,或者调整你的类型定义以适应具体的用例。 ## 4 组件暴露的属性和方法 defineExpose ``` defineExpose({ fetchPageData }); ``` ## 5 如何获取组件实例 ``` import type PageContent from "@/components/PageContent/index.vue"; const contentRef = ref>(); ``` ## 6 defineProps 1 ``` defineProps({ isActive: { required: true, type: Boolean, default: false, } }); ``` 2 ``` const props = defineProps(); // 对象类型 type IObject = Record; // 定义接收的属性 export interface ISearchConfig { // 页面名称(参与组成权限标识,如sys:user:xxx) pageName: string; // 表单项 formItems: Array<{ // 组件类型(如input,select等) type?: "input" | "select" | "tree-select" | "date-picker"; // 标签文本 label: string; // 键名 prop: string; // 组件属性 attrs?: IObject; // 初始值 initialValue?: any; // 可选项(适用于select组件) options?: { label: string; value: any }[]; }>; // 是否开启展开和收缩 isExpandable?: boolean; // 默认展示的表单项数量 showNumber?: number; } interface IProps { searchConfig: ISearchConfig; } ``` ## 7 defineEmits 1 ``` const emits = defineEmits(["update:modelValue"]); function handleChange(val?: string | number | undefined) { emits("update:modelValue", val); } ``` 2 ``` const emit = defineEmits(["toggleClick"]); function toggleClick() { emit("toggleClick"); } ``` 3 ``` // 搜索表单数据 const queryParams = reactive({}); // 对象类型 type IObject = Record; ``` ``` const emit = defineEmits<{ queryClick: [queryParams: IObject]; resetClick: [queryParams: IObject]; }>(); // 重置操作 function handleReset() { queryFormRef.value?.resetFields(); emit("resetClick", queryParams); } // 查询操作 function handleQuery() { emit("queryClick", queryParams); } ``` 4 ``` const emit = defineEmits<{ addClick: [] }>(); emit("addClick"); ``` ## 8 table?: Omit, "data"> 在 Vue 3 的 TypeScript 代码中,table?: Omit, "data"> 这一行代码是一个类型注解,用于定义一个可选的属性 table,这个属性的类型是基于 TableProps 类型但去除了 "data" 属性。 下面逐一解释这个类型注解的各个部分: **table?**: table 是属性的名称。 ? 表示这个属性是可选的,即对象可以包含这个属性,也可以不包含。 **Omit**: Omit 是一个 TypeScript 内置的工具类型,用于从类型 T 中排除一些属性,这些属性的名称被包含在类型 K 中。 T 是原始类型,这里为 TableProps。 K 是要排除的属性名类型,这里是一个字符串字面量 "data"。 **TableProps**: 这是一个泛型类型,TableProps 可能是某个库或组件中定义的类型,用于描述表格的属性。 是传递给 TableProps 的泛型参数。在这个例子中,它表示表格的数据可以是任何类型。 **Omit, "data">**: 这个类型表示 TableProps 中除了 "data" 属性之外的所有属性。 综合起来,table?: Omit, "data"> 表示一个可选的属性 table,它的类型与 TableProps 相同,但不包含 "data" 属性。这通常用于组件的属性定义,当你想要使用一个组件的几乎所有属性,但不想使用其中的某个属性(比如 "data")时,这个类型就非常有用。 在实际应用中,可能是因为你已经有自己的方式处理或提供数据,因此不需要 TableProps 类型中定义的 "data" 属性。 ### 9 Record ``` type IObject = Record; 等同于 type IObject = { [x: string]: any; } ``` # element-plus ## 1 ref ``` const queryFormRef = ref(ElForm); // 查询表单 ``` ``` import type { UploadInstance } from "element-plus"; const uploadRef = ref(); // 上传组件 ``` ``` const menuRef = ref(ElTree); ``` ``` import type { FormInstance } from "element-plus"; const queryFormRef = ref(); ``` ## 2 genFileId 它可能被用于生成唯一的文件标识符 ``` import { genFileId } from "element-plus"; file.uid = genFileId(); ``` # ## 3 ElMessageBox.prompt ## 4 el-form中的inline ``` ``` ## 5 分页组件上+v-model:current-page="currentPage"作用 ``` ``` ## 6 check-strictly https://blog.51cto.com/u_16213699/10588986 ## 7 element-plus的图标 ``` import * as ElementPlusIconsVue from "@element-plus/icons-vue"; const epIcons: string[] = Object.keys(ElementPlusIconsVue); // Element Plus图标集合 ``` ## 8 el-tree、el-tree-select el-tree-select ``` ``` ## 9 组件props ### 9.1 TableProps类型(table组件属性) ``` Omit, "data">; ``` ### 9.2 dialog组件属性 ``` import type { DialogProps, DrawerProps, FormProps, FormItemRule, } from "element-plus"; // dialog组件属性 export type IDialog = Partial>; ``` ### 9.3 drawer组件属性 ``` import type { DialogProps, DrawerProps, FormProps, FormItemRule, } from "element-plus"; // drawer组件属性 export type IDrawer = Partial>; ``` ### 9.4 form组件属性 ``` import type { DialogProps, DrawerProps, FormProps, FormItemRule, } from "element-plus"; // form组件属性 export type IForm = Partial>; ``` ## 10 Partial `Partial` 是一个内置的工具类型,它用于将一个类型的所有属性都变为可选的 ``` export type IDialog = Partial>; ``` ## 11 验证规则 ### 11.1 FormItemRule ``` import type { FormItemRule } from "element-plus"; // 表单项 export type IFormItems = Array<{ // 组件类型(如input,select,radio,custom等,默认input) type?: | "input" | "select" | "radio" | "checkbox" | "tree-select" | "date-picker" | "input-number" | "custom"; // 组件属性 attrs?: IObject; // 组件可选项(适用于select,radio,checkbox组件) options?: Array<{ label: string; value: any; disabled?: boolean; [key: string]: any; }>; // 插槽名(适用于组件类型为custom) slotName?: string; // 标签文本 label: string; // 标签提示 tips?: string; // 键名 prop: string; // 验证规则 rules?: FormItemRule[]; // 初始值 initialValue?: any; // 是否隐藏 hidden?: boolean; // 监听函数 watch?: (newValue: any, oldValue: any, data: T) => void; // 计算属性函数 computed?: (data: T) => any; // 监听收集函数 watchEffect?: (data: T) => void; }>; ``` # 聊天 ## 1 文档 https://jmesnil.net/stomp-websocket/doc/ ## 2 介绍使用 sockjs-client/dist/sockjs.min.js、stompjs ``` import SockJS from "sockjs-client/dist/sockjs.min.js"; import Stomp from "stompjs"; var headers = { login: 'mylogin', passcode: 'mypasscode', // additional header 'client-id': 'my-client-id' }; var connect_callback = function() { // called back after the client is connected and authenticated to the STOMP server }; var error_callback = function(error) { // display the error's message header: alert(error.headers.message); }; let client: Stomp.Client; client.connect(headers, connectCallback, errorCallback); ``` # TS ## 1 type与interface PageSearch/index.vue ``` export interface ISearchConfig { // 页面名称(参与组成权限标识,如sys:user:xxx) pageName: string; // 表单项 formItems: Array<{ // 组件类型(如input,select等) type?: "input" | "select" | "tree-select" | "date-picker"; // 标签文本 label: string; // 键名 prop: string; // 组件属性 attrs?: IObject; // 初始值 initialValue?: any; // 可选项(适用于select组件) options?: { label: string; value: any }[]; }>; // 是否开启展开和收缩 isExpandable?: boolean; // 默认展示的表单项数量 showNumber?: number; } ``` ``` import type { ISearchConfig } from "@/components/PageSearch/index.vue"; const searchConfig: ISearchConfig = { pageName: "sys:user", formItems: [ { type: "input", label: "关键字", prop: "keywords", attrs: { placeholder: "用户名/昵称/手机号", clearable: true, style: { width: "200px", }, }, }, { type: "tree-select", label: "部门", prop: "deptId", attrs: { placeholder: "请选择", data: [ { value: 1, label: "有来技术", children: [ { value: 2, label: "研发部门", }, { value: 3, label: "测试部门", }, ], }, ], filterable: true, "check-strictly": true, "render-after-expand": false, clearable: true, style: { width: "150px", }, }, }, { type: "select", label: "状态", prop: "status", attrs: { placeholder: "全部", clearable: true, style: { width: "100px", }, }, options: [ { label: "启用", value: 1 }, { label: "禁用", value: 0 }, ], }, { type: "date-picker", label: "创建时间", prop: "createAt", attrs: { type: "daterange", "range-separator": "~", "start-placeholder": "开始时间", "end-placeholder": "截止时间", "value-format": "YYYY-MM-DD", style: { width: "240px", }, }, }, ], }; export default searchConfig; ``` ## 2 Record(attrs) ``` // 对象类型 type IObject = Record; ``` ``` // 定义接收的属性 export interface ISearchConfig { // 页面名称(参与组成权限标识,如sys:user:xxx) pageName: string; // 表单项 formItems: Array<{ // 组件类型(如input,select等) type?: "input" | "select" | "tree-select" | "date-picker"; // 标签文本 label: string; // 键名 prop: string; // 组件属性 attrs?: IObject; // 初始值 initialValue?: any; // 可选项(适用于select组件) options?: { label: string; value: any }[]; }>; // 是否开启展开和收缩 isExpandable?: boolean; // 默认展示的表单项数量 showNumber?: number; } ``` IObject ``` { type: "input", label: "关键字", prop: "keywords", attrs: { placeholder: "用户名/昵称/手机号", clearable: true, style: { width: "200px", } } } ```