# Vue3-setup-disha-admin-ts **Repository Path**: zhao-jingtao-l/vue3-setup-disha-admin-ts ## Basic Information - **Project Name**: Vue3-setup-disha-admin-ts - **Description**: 基于Vue3组合式API的 后台管理系统 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-02-24 - **Last Updated**: 2025-06-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: vue3, element, Element-UI, Axios, windicss ## README # **罗刹地后台管理系统项目细节** --- > 如果想使用 elemnet-plus 的框架的提示话 可以在 Vscode 下载 > > Element Plus Snippets ## 一、项目接口地址 [http://dishaxy.dishait.cn/shopadminapi#97bf2828-c669-4b2d-8af1-82f5be238722]: ```http http://dishaxy.dishait.cn/shopadminapi#97bf2828-c669-4b2d-8af1-82f5be238722 ```` ## 二、配置 404 路由 > 配置路由表 ```ts import { createRouter, createWebHistory } from "vue-router"; // 导入路由信息 import routes from "./router"; const router = createRouter({ // 使用histort路由模式 history: createWebHistory(import.meta.env.BASE_URL), // 路由对象 routes, }); export default router; ```` ```ts // 匹配任意路由 { path: '/not-found', name: 'not-found', component: () => import('@/components/Not-Fount/Not-Fount.vue'), // 设置一个meta信息 有路由信息 添加false // 没有路由信息添加true meta: { hidFooter: true } }, { // 匹配所有内容 path: '/:pathMatch(.*)*', // 重定向到'找不到'路由组件 redirect: '/not-found' }, ``` ## 三、使用 windicss > 官网链接 ```http https://cn.windicss.org/integrations/vite.html ``` > 安装 ```shell npm i -D vite-plugin-windicss windicss ``` > 然后,在你的 Vite 配置中添加插件: ```ts import WindiCSS from "vite-plugin-windicss"; export default { plugins: [WindiCSS()], }; ``` > 最后,在你的 Vite 入口文件中导入 `virtual:windi.css`: ```ts import "virtual:windi.css"; ``` ### 1. 使用 windciss **结合@apply 实现样式抽离** ```html 1. 2. @apply bg-indigo-500; ``` ## 四、登录页响应式布局兼容移动端 ```html 欢迎光临 欢迎回来 账号密码登录 登 录 ``` ## 五、反向代理配置跨域问题 > 在 vite.config.ts 中 ```ts // 反向代理解决跨域问题 server: { proxy: { '/api': { target: 'http://ceshi13.dishait.cn', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), } } }, ``` ## 六、引入 Cookie 存储 Token > 1. 引入 npm i @vueuse/integrations ```shell npm i @vueuse/integrations ``` > 2. 引入 npm i universal-cookie ```shell npm i universal-cookie ``` > 3. 在组件中使用 ```ts // 引入 import { useCookies } from "@vueuse/integrations/useCookies"; // 使用 useCookies const cookies = useCookies(); ``` ## 七、**引入 vuex 状态管理用户信息** ```ts /* 管理员仓库 */ import type { MyMessageInfo } from "@/config/Manager"; export default { // 开启独立命名空间 namespaced: true, state: { // 管理员信息 messageInfo: {}, }, mutations: { // 记录用户信息 修改管理员信息 setMessageInfo(state: any, payload: any) { state.messageInfo = payload; }, }, getters: {}, actions: {}, }; ``` ## 八、**全局 loading 进度条实现** > 在 npmjs.com 中找到 nprogress > > 注意: 如果使用 ts 的情况下 不下载@types/nprogress 会提示错误 : 找不到 nprogress 模块 ```shell npm i nprogress npm i --save-dev @types/nprogress ``` > 在 main.ts 中 ```ts // 引入 nprogress 样式表 import "nprogress/nprogress.css"; ``` > 封装显示和隐藏的方法 ```ts import nprogress from "nprogress"; // 显示全屏 Loading export const showFullLoading = () => { nprogress.start(); }; // 隐藏全屏 Loading export const HiddenFullLoading = () => { nprogress.done(); }; ``` ## 九、**动态页面标题实现** ### 第一步: 配置 meta 信息 ```ts // 后台首页 { path: "/home", name: "home", component: AdminHome, meta: { title: "罗刹地-后台首页" } }, ``` ### 第二步:在 **permission.ts**前置守卫钩子 中 ```ts /* 设置页面标题 */ let title: any = to.meta.title ? to.meta.title : ""; (document as HTMLDocument).title = title; ``` ## 十、项目的全屏实现(vueuse.org 官网) > 搜索 useFullscreen > > 1. 安装 npm i @vueuse/core ```shell npm i @vueuse/core ``` > 2. 使用的时候 ```ts import { useFullscreen } from "@vueuse/core"; // isFullscreen: 是否全屏状态 toggle: 切换全屏 const { isFullscreen, enter, exit, toggle } = useFullscreen(); ``` > 3. 组件模板中使用的时候 ```vue ``` ## 十一、动态路由的实现(超级重点) ### 1. 定义一个默认的路由 这个路由是所有用户共享的 ```ts // 默认路由,所有用户共享 const routes = [ { path: "/", name: "admin", component: Admin, }, { path: "/login", component: AdminLogin, meta: { title: "登录页", }, }, // 匹配任意路由 { path: "/:pathMatch(.*)*", name: "not-found", component: () => import("@/components/Not-Fount/Not-Fount.vue"), meta: { title: "不是小罗的范畴", }, }, { path: "/:pathMatch(.*)*", redirect: "/not-found", }, ]; ``` ### 2. 书写动态路由 用于匹配菜单动态添加路由 ```ts // 动态路由 用于匹配菜单动态添加路由 const trendsRouter = [ // 子路由的后台首页主控台 { path: "/", name: "/", component: AdminHome, meta: { title: "罗刹地-后台首页", }, }, // 商品管理 { path: "/goods/list", name: "/goods/list", component: () => import("@/pages/Goods/List.vue"), meta: { title: "罗刹地-商品管理", }, }, // 分类管理 { path: "/category/list", name: "/category/list", component: category, meta: { title: "罗刹地-分类管理", }, }, // 规格管理 { path: "/skus/list", name: "skus/list", component: () => import("@/pages/Skus/SkusList.vue"), meta: { title: "罗刹地-规格管理", }, }, // 优惠券管理 { path: "/coupon/list", name: "/coupon/list", component: () => import("@/pages/Coupon/CouponList.vue"), meta: { title: "罗刹地-优惠券管理", }, }, ]; ``` ### 3. 导入路由信息 ```ts // 导入路由信息 export const router = createRouter({ history: createWebHistory(), routes, }); ``` ### 4. 添加动态路由的方法 利用递归的思想 ```ts // @ts-ignore export function addRoutes(menus) { // 是否有新的路由 let hasNewRoutes = false; // @ts-ignore const findAndAddRoutesByMenus = (arr) => { // @ts-ignore arr.forEach((e) => { let item = trendsRouter.find((o) => o.path == e.frontpath); if (item && !router.hasRoute(item.path)) { router.addRoute("admin", item); hasNewRoutes = true; } if (e.child && e.child.length > 0) { findAndAddRoutesByMenus(e.child); } }); }; findAndAddRoutesByMenus(menus); return hasNewRoutes; } ``` ### 5. **解决添加动态路由的时候 刷新找不到路由的问题** > 在添加动态路由方法的时候 定义一变量 > > let hasNewRoutes = false > > 具体查看 4. 中的方式 #### (重点)设置全局路由前置守卫的时候 ```ts // 全局前置守卫钩子 let hasGetInfo = false; router.beforeEach(async (to, from, next) => { // 显示 loading showFullLoading(); // 获取到 token const token = getToken(); // 如果没有登录强制回到登录页 if (!token && to.path !== "/login") { messageShow("检测到您还未登录 请先登录", "error"); return next({ path: "/login" }); } // 防止重复登录 if (token && to.path === "/login") { messageShow("您已登录 无需再次登录", "warning"); return next({ path: from.path }); } /* 如果用户登录了 自动获取用户信息 并存储在Vuex中 */ let hasNewRoutes = false; if (token && !hasGetInfo) { const res = await store.dispatch("manager/getManagerInfo"); hasGetInfo = true; // 动态添加路由 hasNewRoutes = addRoutes(res.menus); } /* 设置页面标题 */ let title: any = to.meta.title ? to.meta.title : ""; (document as HTMLDocument).title = title; hasNewRoutes ? next(to.fullPath) : next(); }); // 全局后置守卫 router.afterEach(() => HiddenFullLoading()); ``` ## 十二、**标签导航组件实现** ### 1. 使用 tab ```vue Action 1 ``` ### 2. 选中选项中的 path ```ts // 选中选项卡的 path const ActiveTab: Ref = ref($router.path); // tab 标签的数据列表 const TabList = ref([ { title: "后台首页", path: "/", }, ]); ``` ### 3. 添加导航标签 ```ts /* 添加标签导航 */ const addTab = (tab: MyTabs) => { // 表示没有找到 相对应的 let noTab = TabList.value.findIndex((t) => t.path === tab.path) === -1; if (noTab) { TabList.value.push(tab); } cookies.set("tabList", TabList.value); }; ``` ### 4. 监听路由变化 ```ts /* 监听路由的变化 */ onBeforeRouteUpdate((to, from) => { // 点击路由之后 标签处于激活状态 ActiveTab.value = to.path; // 添加路由 addTab({ title: (to.meta.title as string).split("-")[1], path: to.path }); }); ``` #### 5. 初始化标签导航列表 ```ts /* 初始化标签导航列表 */ const initTabList = () => { let tabList = cookies.get("tabList"); if (tabList) { TabList.value = tabList; } }; initTabList(); ``` #### 6. tab 改变的时候触发的逻辑 ```ts /* tab 改变的时候触发的函数 */ const TabChangeHandler = (tab: any) => { console.log("tab对应的路径: ", tab); ActiveTab.value = tab; $router.push({ path: tab }); }; ``` #### 7. tab 删除之后自动跳转到下一个或者上一个 路径 ```ts /* 删除(关闭)标签的处理函数 */ const removeTab = (targetName: TabPaneName) => { // targetName 就是你要删除掉的 path 的值 // 拿到当前激活的path值 let active = ActiveTab.value; // 拿到导航标签栏的数组 const tabs = TabList.value; // 如果 当前激活的 path 的值是和目标的这个path 是一样的 if (active === targetName) { tabs.forEach((item, index: number) => { // 拿到的当前的 path路径和 目标的路径是一致的 if (item.path === targetName) { // 拿到目标索引的 下一个索引或者 上一个索引 const nextIndex = tabs[index + 1] || tabs[index - 1]; if (nextIndex) { // 把当前激活的值设置为 nextIndex.path active = nextIndex.path; } } }); } // 修改当前的值 也就是下一次激活的值 去跳转的值 ActiveTab.value = active; // 去过滤 TabList 数组 TabList.value = TabList.value.filter((item) => item.path !== targetName); // 最后更新存储的 cookie cookies.set("tabList", TabList.value); }; ``` ## 十三、**keep-alive 页面缓存**(重点) > 是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。 > 默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态——当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。 ### 1. 拿到点击之后切换的不同的组件 > 在路由出口的时候 把 Component 解构出来 v-slot="{ Component }" ```vue ``` ### 2. 使用 keep-alive 包裹 > max 就是超过 20 个组件之后 再次点击第一次组件的时候 就不会缓存了 就会被销毁 ```vue ``` ## 十四、**使用 transition 配置全局过渡动画**(淡入淡出)(Vue 提供的内置组件) ### 1. 先找到主页面布局文件 admin.vue > 包裹住 keep-alive ```vue ``` ### 2. 在 transitions 组件上添加 name 属性 ```vue ``` ### 3. 书写 css 样式(离开的动画没有走完 新的页面就已经出来了 有 BUG) ```scss /* 进入之前 */ .fade-enter-from { opacity: 0; } /* 进入之后 */ .fade-enter-to { opacity: 1; } /* 离开之前的动画 */ .fade-leave-from { opacity: 1; } /* 离开之后的动画 */ .fade-leave-to { opacity: 0; } /* 动画的时长 */ .fade-enter-active, .fade-leave-active { transition: all 0.3s; } ``` ### 4. 希望进入动画的时候延迟一下 等离开的动画走完才进入 ```scss /* 进入页面的时长延迟一下 */ .fade-enter-active { transition-delay: 0.3s; } ``` ### 5. 使用 Animate.css 去编写动画(了解一下) > 官方链接 https://animate.style/ ```scss /* 进入之前 */ .fade-enter-from { opacity: 0; -webkit-transform: translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0); } /* 进入之后 */ .fade-enter-to { opacity: 1; -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } /* 离开之前的动画 */ .fade-leave-from { opacity: 1; } /* 离开之后的动画 */ .fade-leave-to { opacity: 0; -webkit-transform: translate3d(0, 100%, 0); transform: translate3d(0, 100%, 0); } /* 动画的时长 */ .fade-enter-active, .fade-leave-active { transition: all 0.3s; } /* 进入页面的时长延迟一下 */ .fade-enter-active { transition-delay: 0.3s; } ``` ## 十五、主控台(后台首页的开发) ### 1. 先拿到数据 ```ts /* 后台首页的统计 */ const panels: Ref> = ref([]); const StatisticsHandler = async () => { const res = await Statistics1Api(); if (res.status === 200) { panels.value = res.data.data.panels; } console.log("后台统计1的数据:", panels.value); }; ``` ### 2. 在页面结构中渲染数据 ```vue {{ item.title }} {{ item.unit }} {{ item.value }} {{ item.subTitle }} {{ item.subValue }} ``` ## 十六、**骨架屏优化体验** > 在需要等待加载内容的位置设置一个骨架屏,某些场景下比 Loading 的视觉效果更好。 ```vue ``` ## 十七、**数字滚动动画实现** > 需要去使用一个第三方动画库 gsap ```shell npm install gsap -S ``` ### 1. 先封装一个 组件 CountTo.Vue ```vue {{ number.num }} ``` ### 2. 使用的时候 ```vue ``` ## 十八、**echarts图表组件开发和交互** > 前往 echarts.apache官方文档 ```http https://echarts.apache.org/zh/download.html ``` ```shell npm install echarts ``` ### 1. 封装 echarts 组件 AdminHomeEcharts.vue ```vue 订单统计 {{ item.text }} ``` ## 十九、**店铺和交易提示组件开发和交互** ### 1. 封装 AdminHomeCart.vue 组件 ```vue {{ title }} {{ tip }} {{ item.value }} {{ item.label }} ``` ### 2. 使用组件的时候 ```vue ``` ## 二十、**v-permission指令按钮级权限控制 自定义指令(重点)** ### 1. 在登录的时候把所有的接口数据给存储在 Vuex中 ```ts state: { // 用户拥有的所有接口权限的别名 ruleNames: [] } // 同步的去修改state中的数据 mutations:{ setruleNamesInfo(state: any, ruleNames: any) { state.ruleNames = ruleNames } } ``` ### 2. 使用自定义指令去筛选用户的权限菜单 > 如果vuex中的ruleNames有 下面的接口的话 就把组件给留下,否则就会把这个组件移除掉 ```vue ``` ### 3. 在 src 目录下新建一个 permission.ts的文件 ```ts /* v-permission指令按钮级权限控制 */ export default { // @ts-ignore install(app){ console.log(app); } } ``` > 在 main.ts中引用 ```ts // 导入插件 import permission from './directives/permission' // 使用插件 app.use(permission) ``` ### 4. 使用自定义指令的时候 > 在 permission.ts 中 ```ts /* v-permission指令按钮级权限控制 */ export default { // @ts-ignore install(app) { // 全局自定义指令 app.directive("permission", { // 使用 mounted 钩子函数 // @ts-ignore // el 就是使用自定义指令的元素对象 binding为一些配置项 mounted(el, binding) { // 拿到自定义执行传入的值 binding.value /* 拿到自定义执行传入的值跟 vuex 中的接口信息作比较 有的话就把 el留下来 如果没有的话就直接移除即可 */ } }) } } ``` > 组件中使用的时候 ```vue ``` ### 5. 自定义指定完成 > 在 permission.ts 中 ```ts /* v-permission指令按钮级权限控制 */ import store from "@/store/index"; /* 定义一个函数方法来判断是否有函数别名 */ // @ts-ignore const hasPermission = (value, el = false) => { // Array.isArray(value)判断是不是数组 if (!Array.isArray(value)) { throw new Error(`需要配置权限, 例如 v-permission="['getStatistics3,GET']"`) } // findIndex()方法返回数组中通过测试的第一个元素的索引 // 查到了有包含这个别名 有这个权限 const hasAuth = value.findIndex(key => store.state.manager.ruleNames.includes(key)) !== -1 // 首先你有el元素 并且你没有这个权限的话 就需要把el移除掉 if (el && !hasAuth) { // @ts-ignore el.parentNode && el.parentNode.removeChild(el) } return hasAuth } export default { // @ts-ignore install(app) { // 全局自定义指令 app.directive("permission", { // 使用 mounted 钩子函数 // @ts-ignore // el 就是使用自定义指令的元素对象 binding为一些配置项 mounted(el, binding) { // 拿到自定义执行传入的值 binding.value /* 拿到自定义执行传入的值跟 vuex 中的接口信息作比较 有的话就把 el留下来 如果没有的话就直接移除即可 */ hasPermission(binding.value, el) } }) } } ``` > 模拟中使用的时候 用一个vuex数组中压根不存在的数据 这样的话会把 el 铲除掉 ```vue ``` > 但是会抛出一个错误 ```ts Uncaught TypeError: Cannot read properties of null (reading 'resize') at ResizeObserver. (AdminHomeEcharts.vue:99:44) ``` ### 6. 在你使用自定义组件的 eacharts 的组件中 > 此错误的意思就是 > > 无法读取null(读取“resize”) > 所以解决方式就是 利用 ?. 就可以了 ```ts useResizeObserver(el, (entries) => myChart?.resize()) ```