【Vue】后台管理系统
O 项目说明
1.脚手架
- vite
- vue-cli ==》 webpack
2.vite脚手架使用
官网:https://vitejs.cn/
Vue3 vite官网:https://cn.vitejs.dev/
Vite下一代的前端工具链,为开发者提供急速响应
# 安装
$ cnpm i vite -g
$ vite -v
vite/4.0.3 darwin-x64 node-v16.13.1
windows注意处理 ****/vite.psl文件
Vite(法语意为 “快速的”,发音 /vit/
,发音同 “veet”)是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:
- 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。
- 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。
2.1 如何搭建vue3项目
- 使用npm
# 如果选择npm创建项目再执行
$ npm create vite@latest
- 使用yarn,如果电脑没有安装yarn
cnpm i yarn -g
$ yarn create vite
- 使用pnpm:如果电脑尚未安装 pnpm
cnpm i pnpm -g
$ pnpm create vite
对比 | yarn | npm | pnpm |
---|---|---|---|
初始化 | yarn init | npm init | 利用硬链接和符号连接来避免复制所有本地缓存资源文件 |
安装依赖 | yarn install 或者yarn | npm install 或 npm i | pnpm install |
新增依赖 | yarn add vant | npm i vant -S | pnpm i vant |
删除依赖 | yarn remove vant | npm uninstall vant -S | |
删除devDependencies 依赖 | npm uninstall vant -D | ||
更新依赖 | yarn upgrade | npm update | pnpm update |
全局安装或者删除 | yarn global remove vue-cli | npm uninstall vue-cli -g | |
同时下载多个 | yarn add axios vue-axios | npm i axis vue-axios -S |
1.创建项目
$ npm init vue@latest
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cSvR3gWm-1672888918411)(assets/image-20221226093812541.png)]
|- vue3-admin-app # 项目名称|- node_modules # 项目依赖包|- publicfavicon.ico # 网页图标|- src # 写代码的主场|- assets # 资源文件base.css # 基础样式logo.svg # logomain.css # 项目样式|- components # 自定义组件|- icons # 图标组件HelloWorld.vue # 自定义组件TheWelcome.vue # 自定义组件welcomeItem.vue # 自定义组件|-router # 路由文件夹index.ts # 路由配置|- stores # 状态管理器文件夹counter.ts # 状态管理器模块|- views # 项目页面组件AboutView.vue # 页面HomeView.vue # 页面App.vue # 项目跟组件main.ts # 项目入口文件.eslintrc.cjs # 代码格式化说明.gitignore # git上传忽略文件.prettierrc.json # 格式化配置env.d.ts # 环境配置声明文件 - ts 中index.html # 页面模版package.json # 项目依赖说明以及运行命令README.md # 说明文档tsconfig.config.json # ts的配置文件说明 - 本项目部分tsconfig.json # ts的配置文件说明 - 公共部分vite.config.ts # vite的配置文件
1.1 Vue3 单文件组件 SFC
一个 Vue 单文件组件 (SFC),通常使用 *.vue
作为文件扩展名,它是一种使用了类似 HTML 语法的自定义文件格式,用于定义 Vue 组件。一个 Vue 单文件组件在语法上是兼容 HTML 的。
每一个 *.vue
文件都由三种顶层语言块构成:<template>
、<script>
和 <style>
<template><div class="example">{{ msg }}</div>
</template><script>
export default {data() {return {msg: 'Hello world!'}}
}
</script><style>
.example {color: red;
}
</style>
打开src/mian.ts
发现在引入 App.vue
中画红线,说明项目中没有.vue
的声明文件,需要自行补充一下
// env.d.ts
/// <reference types="vite/client" />// 简单版本
// declare module '*.vue'// 推荐
declare module '*.vue' {// 引入vue模块中ts的方法import type { DefineComponent } from 'vue'// 定义vue组件以及类型注解const component: DefineComponent<{}, {}, any>export default component
}
再次打开 src/main.ts
发现红线丢失
复制src文件夹,保留App.vue
以及main.ts
基本内容
// src/main.ts
import { createApp } from 'vue'import App from './App.vue'const app = createApp(App)app.mount('#app')
如何使用选项式API
<!-- src/App.vue -->
<template><div class="example">{{ msg }} - {{ count }}<button @click="count++">加1</button></div>
</template><script lang="ts">import { defineComponent} from 'vue'export default defineComponent({data () {return {msg: 'hello sfc',count: 10}}})
</script>
<!-- 如果样式只针对当前组件是有效的,只要给style 添加 scoped 属性即可 -->
<style>.example {color: #f66}
</style>
如果使用组合式API
<!-- src/App.vue -->
<template><div class="example">{{ msg }} - {{ count }}<button @click="count++">加1</button></div>
</template><script lang="ts">import { defineComponent, ref } from 'vue'export default defineComponent({setup () {const msg = ref('hello sfc!!!!')const count = ref(10)return {msg, count}}})
</script>
<!-- 如果样式只针对当前组件是有效的,只要给style 添加 scoped 属性即可 -->
<style>.example {color: #f66}
</style>
- 组合式API简写形式
<!-- src/App.vue -->
<template><div class="example">{{ msg }} - {{ count }}<button @click="count++">加1</button></div>
</template><!-- 采用简写形式 -->
<script lang="ts" setup>import { ref } from 'vue'const msg = ref('hello sfc!!!!')const count = ref(10)</script>
<!-- 如果样式只针对当前组件是有效的,只要给style 添加 scoped 属性即可 -->
<style>.example {color: #f66}
</style>
预习:https://cn.vuejs.org/guide/typescript/composition-api.html
2.准备工作
?样式处理 css、sass、less 、stylus
sass、less、stylus 为 css 预处理器
.contianer {} .container .box {} .container .box .header {} .container .box .content {} .container .box .footer {}
// scss .contianer {width: 100%.box {width: 100%.header {width: 100%}.content {width: 100% }.footer {width: 100%}} }
// .less .contianer {width: 100%.box {width: 100%.header {width: 100%}.content {width: 100% }.footer {width: 100%}} }
// stylus .containerwidth 100%.boxwidth 100%.headerwidth 100%.contentwidth 100%.footerwidth 100%
?? 本项目用什么,建议大家先选择UI组件库(element-plus / ant-design-vue)
样式 css - sass - 可以重新创建项目
# 需要安装 $ cnpm i sass -D
使用重置样式表
# 需要安装 $ cnpm i normalize.css -D
// src/main.ts import { createApp } from 'vue' import { createPinia } from 'pinia'import App from './App.vue' import router from './router'// 重置样式表 import 'normalize.css/normalize.css'// import './assets/main.css' // 不使用默认样式const app = createApp(App)app.use(createPinia()) app.use(router)app.mount('#app')
路由 vue-router
# 创建项目时已经选择了,此处不需要安装 $ cnpm i vue-router -S
状态管理 pinia / vuex
# 创建项目时已经选择了,此处不需要安装 $ cnpm i pinia -S
数据请求方案 axios / fetch
# 需要安装 $ cnpm i axios -S
组件库 element plus
# 需要安装(如果安装失败, 更新cnpm) # npm install -g cnpm --registry=https://registry.npmmirror.com $ cnpm i element-plus -S
Element-plus 默认使用英文,如果需要使用中文包,如下操作
// src/main.ts import { createApp } from 'vue' import { createPinia } from 'pinia'// 引入UI库 ++++++ import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import zhCn from 'element-plus/dist/locale/zh-cn.mjs'import App from './App.vue' import router from './router'// 重置样式表 import 'normalize.css/normalize.css'// import './assets/main.css' // 不使用默认样式const app = createApp(App)// 配置UI库 ++++++ app.use(ElementPlus, { locale: zhCn }) app.use(createPinia()) app.use(router)app.mount('#app')
// env.d.ts /// <reference types="vite/client" />// declare module '*.vue' declare module '*.vue' {import type { DefineComponent, defineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component }declare module 'element-plus/dist/locale/zh-cn.mjs'
本地存储 localStorage / store2 /
# 安装 $ cnpm i store2 -S
数据可视化 ECharts
# 安装 $ cnpm i echarts -S
3.创建主布局文件
https://element-plus.gitee.io/zh-CN/component/container.html#%E5%B8%B8%E8%A7%81%E9%A1%B5%E9%9D%A2%E5%B8%83%E5%B1%80
<!-- src/App.vue -->
<script lang="ts" setup>
</script><template><div class="common-layout"><el-container><el-aside width="200px">Aside</el-aside><el-container><el-header>Header</el-header><el-main>Main</el-main><el-footer>Footer</el-footer></el-container></el-container></div>
</template><style lang="scss">
/* 审查元素从而设定样式 */
html, body, #app, .common-layout, .el-container {height: 100%;
}
.common-layout {.el-container {background-color: #efefef;.el-aside {background-color: #001529;}.el-container {.el-header {background-color: #fff;}.el-main {background-color: #fff;margin: 16px;}.el-footer {background-color: #fff;}}}
}
</style>
4.对于整个系统的设计思路
4.1 系统的默认设置
https://panjiachen.gitee.io/vue-element-admin/#/login?redirect=%2Fdashboard
// src/settings.ts
// 参考项目链接
// https://panjiachen.gitee.io/vue-element-admin/#/login?redirect=%2Fdashboard
export default {title: '嗨购管理系统',// 主题等的设置showSetting: true,// 快捷导航的提示tagsView: true,// 头部是否固定fixedHeader: true,// 左侧菜单logo显示sideBarLogo: true
}
4.2 设置页面的标题
// src/utils/get-page-title.ts
import defaultSettings from '../settings'const title = defaultSettings.title || '嗨购后台管理系统'// 系统首页 - 嗨购后台管理系统
// 添加轮播图 - 嗨购后台管理系统
export default function getPageTitle (pageTitle: string) {if (pageTitle) {// 当前页面标识 + 应用的标题return `${ pageTitle } - ${ title }`} else {return `${ title }`}
}
页面标题的修改需要配合路由使用
5.设置路由
5.1 设置基本布局组件
<!-- src/layout/index.vue -->
<!-- 后台管理系统两种布局:登录布局 + 其他布局 -->
<script lang="ts" setup>
</script><template><div class="common-layout"><el-container><el-aside width="200px">Aside</el-aside><el-container><el-header>Header</el-header><el-main>Main</el-main><el-footer>Footer</el-footer></el-container></el-container></div>
</template>
<!-- src/App.vue -->
<!-- 组合式API中如何使用 组件 -->
<script lang="ts" setup>import Layout from './layout/index.vue'
</script><template><Layout />
</template><style lang="scss">
/* 审查元素从而设定样式 */
html, body, #app, .common-layout, .el-container {height: 100%;
}
.common-layout {.el-container {background-color: #efefef;.el-aside {background-color: #001529;}.el-container {.el-header {background-color: #fff;}.el-main {background-color: #fff;margin: 16px;}.el-footer {background-color: #fff;}}}
}
</style>
5.2 设置基本页面
5.2.1 系统首页
<!-- src/views/home/index.vue -->
<script lang="ts" setup></script><template><div>系统首页</div>
</template>
5.2.2 轮播图管理
轮播图列表
<!-- src/views/banner/list.vue --> <script lang="ts" setup> </script><template><div>轮播图列表</div> </template>
<!-- src/views/banner/components/home.vue --> <script lang="ts" setup> </script><template><div>首页轮播图列表</div> </template>
<!-- src/views/banner/components/kind.vue --> <script lang="ts" setup> </script><template><div>分类轮播图列表</div> </template>
添加轮播图
<!-- src/views/banner/add.vue --> <script lang="ts" setup> </script><template><div>添加轮播图</div> </template>
5.2.3 产品管理
<!-- src/views/pro/list.vue -->
<script lang="ts" setup>
</script><template><div>产品列表</div>
</template>
<!-- src/views/pro/search.vue -->
<script lang="ts" setup>
</script><template><div>筛选列表</div>
</template>
5.2.4 账户管理
<!-- src/views/account/admin.vue -->
<script lang="ts" setup>
</script><template><div>管理员列表</div>
</template>
<!-- src/views/account/user.vue -->
<script lang="ts" setup>
</script><template><div>用户列表</div>
</template>
5.2.5 登录页面
<!-- src/views/login/index.vue -->
<script lang="ts" setup>
</script><template><div>登录页面</div>
</template>
5.3 设置路由模块
https://router.vuejs.org/zh/introduction.html
左侧菜单渲染结构(这些页面应该都是主界面结构)
|- 系统首页 # 只有一级路由
|- 轮播图管理 # 三级路由|- 轮播图列表|- 首页轮播图|- 分类页轮播图|- 添加轮播图
|- 产品管理 # 二级路由|- 产品列表|- 筛选列表
|- 账户管理 # 二级路由|- 管理员列表|- 用户列表
真实项目开发时,可能 轮播图 产品 账户是不同的人开发以及维护的,所以建议将这几个路由的配置分离开来
然后再统一整合管理即可
5.3.1 账户管理路由
// src/router/modules/account.ts
// @符号代表 src 目录结构
import Layout from '@/layout/index.vue'
import User from '@/views/account/user.vue'
import Admin from '@/views/account/Admin.vue'
export default {path: '/account', // 地址栏显示的路径redirect: '/account/user', // 当用户输入 /account 路由时,自动跳转到 /account/user 路由component: Layout, // 账户管理使用主界面布局结构name: 'account', // 给路由起名字 - 命名路由 - 具有唯一性 - 后期会使用meta: {title: '账户管理', // 左侧菜单栏显示的名字icon: 'User' // 左侧菜单栏显示的图标},children: [ // 代表账户管理下有二级路由{path: '/account/user',component: User,meta: {title: '用户列表'}},{path: '/account/admin',component: Admin,meta: {title: '管理员列表'}}]
}
5.3.2 轮播图路由
// src/router/modules/banner.ts
import Layout from '@/layout/index.vue'
import BannerAdd from '@/views/banner/add.vue'
import BannerList from '@/views/banner/list.vue'
import BannerHomeList from '@/views/banner/components/home.vue'
import BannerKindList from '@/views/banner/components/kind.vue'
export default {path: '/banner',name: 'banner',redirect: '/banner/list',component: Layout,meta: {title: '轮播图管理',icon: 'PictureFilled'},children: [{path: '/banner/list',component: BannerList,redirect: '/banner/list/home',meta: {title: '轮播图列表'},children: [{path: '/banner/list/home',component: BannerHomeList,meta: {title: '首页轮播图'},},{path: '/banner/list/kind',component: BannerKindList,meta: {title: '分类页轮播图'},}]},{path: '/banner/add',component: BannerAdd,meta: {title: '添加轮播图'}}]
}
5.3.3 产品管理路由
// src/router/modules/pro.ts
import Layout from '@/layout/index.vue'
import ProList from '@/views/pro/list.vue'
import ProSearch from '@/views/pro/search.vue'
export default {path: '/pro',redirect: '/pro/list',component: Layout,meta: {title: '产品管理',icon: 'Fries'},children: [{path: '/pro/list',component: ProList,meta: {title: '产品列表'}},{path: '/pro/search',component: ProSearch,meta: {title: '筛选列表'}}]
}
5.3.4 整合路由
// src/router/index.tsimport { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
// 1.引入自定义分模块的路由
import bannerRoutes from './modules/banner'
import proRoutes from './modules/pro'
import accountRoutes from './modules/account'// 2.引入主界面布局,生成 首页和登录的路由 -- 常规路由 -- 任何人都可以访问路由
import Layout from '@/layout/index.vue'
import Home from '@/views/home/index.vue'
import Login from '@/views/login/index.vue'// 3.定义常规路由-任何人可以访问的路由
export const constantRoutes: RouteRecordRaw[] = [{ // 地址栏输入 /login, 整个页面展示登录页面path: '/login',component: Login},{path: '/',redirect: '/home',component: Layout,children: [{path: '/home',component: Home,meta: {title: '系统首页',icon: 'HomeFilled'}}]}
]// 4.整合模块路由
export const asyncRoutes = [bannerRoutes,proRoutes,accountRoutes
]// 5.合并路由
const router = createRouter({// createWebHistory http://localhost:5173/home// createWebHashHistory http://localhost:5173/#/homehistory: createWebHistory(import.meta.env.BASE_URL),// routes: constantRoutes.concat(asyncRoutes)routes: [...constantRoutes, ...asyncRoutes]
})export default router
5.3.5 入口文件配置路由
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'// 引入UI库
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'import App from './App.vue'
import router from './router'// 重置样式表
import 'normalize.css/normalize.css'// import './assets/main.css' // 不使用默认样式const app = createApp(App)// 配置UI库
app.use(ElementPlus, { locale: zhCn })
app.use(createPinia())
app.use(router)app.mount('#app')
5.3.6 App.vue使用路由
<!-- src/App.vue -->
<!-- 组合式API中如何使用 组件 -->
<script lang="ts" setup>
</script><template><!-- 路由映射的组件 /login Login/pro/list Layout--><RouterView/>
</template><style lang="scss">
/* 审查元素从而设定样式 */
html, body, #app, .common-layout, .el-container {height: 100%;
}
.common-layout {.el-container {background-color: #efefef;.el-aside {background-color: #001529;}.el-container {.el-header {background-color: #fff;}.el-main {background-color: #fff;margin: 16px;}.el-footer {background-color: #fff;}}}
}
</style>
5.3.7 主界面设置路由视图
<!-- src/layout/index.vue -->
<!-- 后台管理系统两种布局:登录布局 + 其他布局 -->
<script lang="ts" setup>
</script><template><div class="common-layout"><el-container><el-aside width="200px">Aside</el-aside><el-container><el-header>Header</el-header><el-main><!-- 内容区域变化 --><RouterView /></el-main><el-footer>Footer</el-footer></el-container></el-container></div>
</template>
5.3.8 轮播图列表视图
<!-- src/views/banner/list.vue -->
<script lang="ts" setup></script><template><div>轮播图列表</div><RouterView />
</template>
6.渲染左侧菜单栏
6.1 重新设置路由表
页面展示靠路由,但是左侧菜单并不见得所有的路由都得出现
将不需要出现在左侧菜单栏的数据 做一个标识hidden: true
扩展了类型注解
// src/router/index.ts
// type RouteRecordRaw 定义路由规则的变量的类型注解
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
// 1.引入自定义分模块的路由
import bannerRoutes from './modules/banner'
import proRoutes from './modules/pro'
import accountRoutes from './modules/account'// 2.引入主界面布局,生成 首页和登录的路由 -- 常规路由 -- 任何人都可以访问路由
import Layout from '@/layout/index.vue'
import Home from '@/views/home/index.vue'
import Login from '@/views/login/index.vue'// 扩展 类型注解
type MyRouteRecordRaw = RouteRecordRaw & {hidden?: boolean
}// 3.定义常规路由-任何人可以访问的路由
export const constantRoutes: MyRouteRecordRaw[] = [{ // 地址栏输入 /login, 整个页面展示登录页面path: '/login',component: Login,hidden: true},{path: '/',redirect: '/home',component: Layout,children: [{path: '/home',component: Home,meta: {title: '系统首页',icon: 'HomeFilled'}}]}
]// 4.整合模块路由
export const asyncRoutes: MyRouteRecordRaw[] = [bannerRoutes,proRoutes,accountRoutes
]// 5.合并路由
const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: constantRoutes.concat(asyncRoutes)
})export default router
// src/router/modules/banner.ts
import Layout from '@/layout/index.vue'
import BannerAdd from '@/views/banner/add.vue'
import BannerList from '@/views/banner/list.vue'
import BannerHomeList from '@/views/banner/components/home.vue'
import BannerKindList from '@/views/banner/components/kind.vue'
export default {path: '/banner',name: 'banner',redirect: '/banner/list',component: Layout,meta: {title: '轮播图管理',icon: 'PictureFilled'},children: [{path: '/banner/list',component: BannerList,redirect: '/banner/list/home',meta: {title: '轮播图列表'},children: [{path: '/banner/list/home',component: BannerHomeList,meta: {title: '首页轮播图'},},{path: '/banner/list/kind',component: BannerKindList,meta: {title: '分类页轮播图'},}]},{path: '/banner/add',component: BannerAdd,meta: {title: '添加轮播图'},hidden: true}]
}
6.2 构建左侧菜单的组件
<!-- src/layout/components/Sidebar/index.vue -->
<script lang="ts" setup></script>
<template><el-aside width="200px"><div class="logo"></div><div>左侧菜单</div></el-aside>
</template>
<!-- src/layout/index.vue -->
<!-- 后台管理系统两种布局:登录布局 + 其他布局 -->
<script lang="ts" setup>
import SideBar from './components/Sidebar/index.vue'
</script><template><div class="common-layout"><el-container><!-- <el-aside width="200px">Aside</el-aside> --><SideBar /><el-container><el-header>Header</el-header><el-main><!-- 内容区域变化 --><RouterView /></el-main><el-footer>Footer</el-footer></el-container></el-container></div>
</template>
左侧菜单对应的每个数据的菜单项
<!-- src/layout/components/Sidebar/SidebarItem.vue -->
<script lang="ts" setup></script><template><div>左侧菜单选项</div>
</template>
<!-- src/layout/components/Sidebar/index.vue -->
<script lang="ts" setup>import SidebarItem from './SidebarItem.vue'// 整合路由import { constantRoutes, asyncRoutes } from '@/router'const menuList = constantRoutes.concat(asyncRoutes)console.log(menuList)
</script>
<template><el-aside width="200px"><div class="logo"></div><!-- 菜单组件https://element-plus.gitee.io/zh-CN/component/menu.html#%E4%BE%A7%E6%A0%8F--><!-- <div>左侧菜单</div> --><el-menu><!-- 路由中的每一个选项都需要单独去渲染菜单项有的不需要出现在左侧的菜单栏有的只有一个菜单项有的有二级菜单和三级菜单建议把每个菜单项单独封装成一个组件 SidebarItem登录 系统首页 产品管理 账户管理 轮播图管理将每一项数据 item 传输到子组件,子组件接收数据,并且渲染--><SidebarItem v-for="item of menuList":key="item.path":item = "item"></SidebarItem></el-menu></el-aside>
</template>
渲染子组件
分析数据结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-itlMnTal-1672888918413)(assets/image-20221227160845689.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p8CRy9Iq-1672888918414)(assets/image-20221227160905511.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LQBUyTgw-1672888918414)(assets/image-20221227161017853.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ryAgzwzg-1672888918415)(assets/image-20221227161042917.png)]
6.3 渲染左侧菜单的数据
说明:需要使用图标
$ cnpm install @element-plus/icons-vue
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'// 引入UI库
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'// 引入图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'import App from './App.vue'
import router from './router'// 重置样式表
import 'normalize.css/normalize.css'// import './assets/main.css' // 不使用默认样式const app = createApp(App)
// 配置图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component)
}// 配置UI库
app.use(ElementPlus, { locale: zhCn })
app.use(createPinia())
app.use(router)app.mount('#app')
<!-- src/layout/components/Sidebar/SidebarItem.vue -->
<script lang="ts" setup>// 1.接收父组件所传递的数据 item 并且加以验证// https://cn.vuejs.org/guide/typescript/composition-api.html#typing-component-propsconst props = defineProps({// item 为父组件传递给子组件的标识item: { type: Object, required: true }})
</script>
<!-- 遇到 类似登录的路由含有 hidden:true 不希望出现左侧菜单栏-->
<template><!-- 过滤了 含有 hidden:true 的数据 --><div v-if="!item.hidden"><!-- 系统首页的路由 children 的长度只为1,认为只需要写一个一级路由即可 index属性代表唯一标志 --><el-menu-item :index="item.path" v-if="item.children && item.children.length === 1"><el-icon><!-- 动态组件 --><component :is="item.children[0].meta.icon"></component></el-icon><span>{{ item.children[0].meta.title }}</span></el-menu-item><!-- 路由选项既没有 children 也没有 hidden 时候{path: '/setting',component: Setting,meta: {title: '设置',icon: 'Setting'}},{ // 二级菜单子菜单path: '/account/user',component: User,meta: {title: '用户列表'}},--><el-menu-item :index="item.path" v-else-if="!item.children"><el-icon><!-- 动态组件 --><component :is="item.meta.icon"></component></el-icon><span>{{ item.meta.title }}</span></el-menu-item><!-- 剩余的包含有多级菜单的数据函数 - 函数递归组件 - 组件递归组合式API的组件递归,自己调用自己的组件,必须确保组件的名字和组件的文件名字保持一致假设组件文件名为 TestCom.vue ,那么递归调用时 <test-com></text-com>递归目的:自动渲染多级菜单--><el-sub-menu :index="item.path" v-else><!-- 插槽实现一级菜单 --><template #title><el-icon><!-- 动态组件 --><component :is="item.meta.icon"></component></el-icon><span>{{ item.meta.title }}</span></template><!-- 递归组件 --><sidebar-item v-for="itm of item.children" :key="item.path" :item="itm"></sidebar-item></el-sub-menu></div>
</template>
6.4 点击左侧菜单切换路由
<!-- src/layout/components/Sidebar/SidebarItem.vue -->
<script lang="ts" setup>import { useRouter } from 'vue-router';const props = defineProps({item: { type: Object, required: true }})// https://router.vuejs.org/zh/guide/advanced/composition-api.html// router.push() 从A push 到B,从B push 到C,C可以返回到B,B可以返回到A// router.replace() 从A push 到B,从B replace 到C, C返回的实际上是 A// router.back() 返回// router.go(num) num为正,前进几步,num为负,后退几步// 选项式API this.$router.push() .replace()const router = useRouter()const changeUrl = (path: string) => {console.log(path)router.push(path) // js 跳转页面 ---- 编程式导航跳转}
</script>
<template><div v-if="!item.hidden"><el-menu-item @click="changeUrl(item.path)" :index="item.path" v-if="item.children && item.children.length === 1"><el-icon><component :is="item.children[0].meta.icon"></component></el-icon><span>{{ item.children[0].meta.title }}</span></el-menu-item><el-menu-item @click="changeUrl(item.path)" :index="item.path" v-else-if="!item.children"><el-icon><component :is="item.meta.icon"></component></el-icon><span>{{ item.meta.title }}</span></el-menu-item><el-sub-menu :index="item.path" v-else><template #title><el-icon><component :is="item.meta.icon"></component></el-icon><span>{{ item.meta.title }}</span></template><sidebar-item v-for="itm of item.children" :key="item.path" :item="itm"></sidebar-item></el-sub-menu></div>
</template>
6.5 只展开一个以及刷新选中
<!-- src/layout/components/Sidebar/index.vue -->
<script lang="ts" setup>import SidebarItem from './SidebarItem.vue'// 整合路由import { constantRoutes, asyncRoutes } from '@/router'import { useRoute } from 'vue-router';const menuList = constantRoutes.concat(asyncRoutes)console.log(menuList)// 获取路由地址// 选项式API this.$routeconst route = useRoute()console.log(route)
</script>
<template><el-aside width="200px"><div class="logo"></div><!-- 菜单组件https://element-plus.gitee.io/zh-CN/component/menu.html#%E4%BE%A7%E6%A0%8F--><!-- <div>左侧菜单</div> --><el-menu:unique-opened="true":default-active="route.path"background-color="#001529"text-color="#fff":collapse-transition="false"><!-- 路由中的每一个选项都需要单独去渲染菜单项有的不需要出现在左侧的菜单栏有的只有一个菜单项有的有二级菜单和三级菜单建议把每个菜单项单独封装成一个组件 SidebarItem登录 系统首页 产品管理 账户管理 轮播图管理将每一项数据 item 传输到子组件,子组件接收数据,并且渲染--><SidebarItem v-for="item of menuList":key="item.path":item = "item"></SidebarItem></el-menu></el-aside>
</template>
6.6 logo
<!-- src/layout/components/logo/index.vue -->
<script lang="ts" setup>import { computed } from 'vue'import logo from '@/assets/logo.svg'import settings from '@/settings'const title = computed(() => settings.title)
</script><template><div class="logo"><el-image style="width: 40px; height: 40px" :src="logo" fit="fill" /><span>{{ title }}</span></div>
</template><style lang="scss">
.logo {width: 100%;height: 60px;overflow: hidden;padding: 10px;box-sizing: border-box;color: #fff;display: flex;align-items: center;
}
</style>
<!-- src/layout/components/Sidebar/index.vue -->
<script lang="ts" setup>import SidebarItem from './SidebarItem.vue'import Logo from './../logo/index.vue'// 整合路由import { constantRoutes, asyncRoutes } from '@/router'import { useRoute } from 'vue-router';const menuList = constantRoutes.concat(asyncRoutes)console.log(menuList)// 获取路由地址// 选项式API this.$routeconst route = useRoute()console.log(route) // route.path.value
</script>
<template><el-aside width="200px"><!-- <div class="logo"></div> --><Logo /><!-- 菜单组件https://element-plus.gitee.io/zh-CN/component/menu.html#%E4%BE%A7%E6%A0%8F--><!-- <div>左侧菜单</div> --><el-menu:unique-opened="true":default-active="route.path"><!-- 路由中的每一个选项都需要单独去渲染菜单项有的不需要出现在左侧的菜单栏有的只有一个菜单项有的有二级菜单和三级菜单建议把每个菜单项单独封装成一个组件 SidebarItem登录 系统首页 产品管理 账户管理 轮播图管理将每一项数据 item 传输到子组件,子组件接收数据,并且渲染--><SidebarItem v-for="item of menuList":key="item.path":item = "item"></SidebarItem></el-menu></el-aside>
</template>
6.7 左侧菜单的收缩
logo组件接收变量,控制文件的显示
<!-- src/layout/components/logo/index.vue -->
<script lang="ts" setup>import { computed } from 'vue'import logo from '@/assets/logo.svg'import settings from '@/settings'const props = defineProps({collapse: { type: Boolean, required: true }})const title = computed(() => settings.title)
</script><template><div class="logo"><el-image style="width: 40px; height: 40px" :src="logo" fit="fill" /><span v-if="!collapse">{{ title }}</span></div>
</template><style lang="scss">
.logo {width: 100%;height: 60px;overflow: hidden;padding: 10px;box-sizing: border-box;color: #fff;display: flex;align-items: center;
}
</style>
<!-- src/layout/components/Sidebar/index.vue -->
<script lang="ts" setup>import { ref } from 'vue'import SidebarItem from './SidebarItem.vue'import Logo from './../logo/index.vue'// 整合路由import { constantRoutes, asyncRoutes } from '@/router'import { useRoute } from 'vue-router';const menuList = constantRoutes.concat(asyncRoutes)console.log(menuList)// 获取路由地址// 选项式API this.$routeconst route = useRoute()console.log(route) // route.path.valueconst collapse = ref(false)
</script>
<template><el-aside :width="collapse ? '50px' : '200px'"><!-- <div class="logo"></div> --><Logo :collapse="collapse"/><!-- 菜单组件https://element-plus.gitee.io/zh-CN/component/menu.html#%E4%BE%A7%E6%A0%8F--><!-- <div>左侧菜单</div> --><el-menu:collapse="collapse":unique-opened="true":default-active="route.path"><!-- 路由中的每一个选项都需要单独去渲染菜单项有的不需要出现在左侧的菜单栏有的只有一个菜单项有的有二级菜单和三级菜单建议把每个菜单项单独封装成一个组件 SidebarItem登录 系统首页 产品管理 账户管理 轮播图管理将每一项数据 item 传输到子组件,子组件接收数据,并且渲染--><SidebarItem v-for="item of menuList":key="item.path":item = "item"></SidebarItem></el-menu><div class="changeIcon" :style="{ width: collapse ? '50px' : '200px'}"><el-icon color="#fff" v-if="collapse" @click="collapse = !collapse"><ArrowRight /></el-icon><el-icon color="#fff" v-else @click="collapse=!collapse"><ArrowLeft /></el-icon></div></el-aside>
</template><style lang="scss">
.changeIcon {position: absolute;bottom: 10px;display: flex;justify-content: center;
}
</style>
6.8 面包屑
<!-- src/layout/components/breadcrumb/index.vue -->
<script lang="ts" setup></script><template><div>面包屑</div>
</template>
// src/router/index.ts
// type RouteRecordRaw 定义路由规则的变量的类型注解
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
// 1.引入自定义分模块的路由
import bannerRoutes from './modules/banner'
import proRoutes from './modules/pro'
import accountRoutes from './modules/account'// 2.引入主界面布局,生成 首页和登录的路由 -- 常规路由 -- 任何人都可以访问路由
import Layout from '@/layout/index.vue'
import Home from '@/views/home/index.vue'
import Login from '@/views/login/index.vue'// 扩展 类型注解(interface) 暴露出去,供需要的使用 ++++++++++++++++++++++++++++++++++++++++
// export declare 暴露类型
export declare type MyRouteRecordRaw = RouteRecordRaw & {hidden?: boolean
}// 3.定义常规路由-任何人可以访问的路由
export const constantRoutes: MyRouteRecordRaw[] = [{ // 地址栏输入 /login, 整个页面展示登录页面path: '/login',component: Login,hidden: true},// {// path: '/setting',// component: Setting,// meta: {// title: '设置',// icon: 'Setting'// }// },{path: '/',redirect: '/home',component: Layout,children: [{path: '/home',component: Home,meta: {title: '系统首页',icon: 'HomeFilled'}}]}
]// 4.整合模块路由
export const asyncRoutes: MyRouteRecordRaw[] = [bannerRoutes,proRoutes,accountRoutes
]// 5.合并路由
const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: constantRoutes.concat(asyncRoutes)
})export default router
获取需要的对象数据
<!-- src/layout/components/breadcrumb/index.vue -->
<script lang="ts" setup>// 准备数据// const breadcrumbNameMap = {// '/home': '系统首页',// '/banner': '轮播图管理',// '/banner/list': '轮播图列表',// '/banner/list/home': '首页轮播图',// '/banner/list/kind': '分类轮播图',// '/banner/add': '添加轮播图',// '/pro': '产品管理',// '/pro/list': '产品列表',// '/pro/search': '筛选列表',// '/account': '账户管理',// '/account/user': '用户列表',// '/account/admin': '管理员列表' // }import { constantRoutes, asyncRoutes, type MyRouteRecordRaw } from '@/router'const menuList = [...constantRoutes, ...asyncRoutes]let breadcrumbNameMap: Record<string, string> = {}function getBreadcrumbNameMap (menuList: MyRouteRecordRaw[]) {menuList.forEach(item => {if (item.path !== '/login') {if (item.children) {if (item.children.length === 1) {// 如果遇到 ** 可能未定义 使用 !解决// 因为路由meta 的属性的类型注解 不明确,此处上面定义为 string,使用类型推断为string即可breadcrumbNameMap[item.children[0].path] = item.children[0].meta!.title as string} else {breadcrumbNameMap[item.path] = item.meta!.title as stringgetBreadcrumbNameMap(item.children)}} else {breadcrumbNameMap[item.path] = item.meta!.title as string}}})}getBreadcrumbNameMap(menuList)console.log(breadcrumbNameMap) // 需要准备的数据// /banner/list/kind // breadcrumbNameMap['/banner'] breadcrumbNameMap['/banner/list'] breadcrumbNameMap['/banenr/list/kind']// 轮播图管理 轮播图列表 分类轮播图
</script><template><div>面包屑</div>
</template>
根据地址栏获取路由
<!-- src/layout/components/breadcrumb/index.vue -->
<script lang="ts" setup>// 准备数据// const breadcrumbNameMap = {// '/home': '系统首页',// '/banner': '轮播图管理',// '/banner/list': '轮播图列表',// '/banner/list/home': '首页轮播图',// '/banner/list/kind': '分类轮播图',// '/banner/add': '添加轮播图',// '/pro': '产品管理',// '/pro/list': '产品列表',// '/pro/search': '筛选列表',// '/account': '账户管理',// '/account/user': '用户列表',// '/account/admin': '管理员列表' // }import { constantRoutes, asyncRoutes, type MyRouteRecordRaw } from '@/router'import { watchEffect, ref } from 'vue';import { useRoute } from 'vue-router';const menuList = [...constantRoutes, ...asyncRoutes]let breadcrumbNameMap: Record<string, string> = {}function getBreadcrumbNameMap (menuList: MyRouteRecordRaw[]) {menuList.forEach(item => {if (item.path !== '/login') {if (item.children) {if (item.children.length === 1) {// 如果遇到 ** 可能未定义 使用 !解决// 因为路由meta 的属性的类型注解 不明确,此处上面定义为 string,使用类型推断为string即可breadcrumbNameMap[item.children[0].path] = item.children[0].meta!.title as string} else {breadcrumbNameMap[item.path] = item.meta!.title as stringgetBreadcrumbNameMap(item.children)}} else {breadcrumbNameMap[item.path] = item.meta!.title as string}}})}getBreadcrumbNameMap(menuList)// console.log(breadcrumbNameMap) // 需要准备的数据// /banner/list/kind // breadcrumbNameMap['/banner'] breadcrumbNameMap['/banner/list'] breadcrumbNameMap['/banenr/list/kind']// 轮播图管理 轮播图列表 分类轮播图const route = useRoute()// const url = route.path// console.log(url) // /banner/list/kind function getData () {const arr = route.path.split('/') // ['', 'banner', 'list', 'kind']arr.shift() // ['banner', 'list', 'kind']// ['/banner', '/banner/list', '/banner/list/kind']const newArr: string[] = []arr.map((_, index) => {// join('/') 拼接时使用 / 拼接,默认是 ,const newUrl = '/' + arr.slice(0, index + 1).join('/')console.log(newUrl)newArr.push(newUrl)})// console.log(newArr)return newArr}let breadcrumbArr = ref<string[]>([])watchEffect(() => { // 路由变化时 重新获取面包屑 ---- 保证数据的双向绑定console.log(11111)breadcrumbArr.value = getData() // 修改使用 value 保证响应式console.log('66', breadcrumbArr)})
</script><template><el-breadcrumb separator="/"><el-breadcrumb-item :to="{ path: '/' }">系统首页</el-breadcrumb-item><el-breadcrumb-item v-for="(item, index) of breadcrumbArr" :key="index"><a :href="item">{{ breadcrumbNameMap[item] }}</a></el-breadcrumb-item></el-breadcrumb>
</template>
垂直居中显示面包屑
<!-- src/layout/index.vue -->
<!-- 后台管理系统两种布局:登录布局 + 其他布局 -->
<script lang="ts" setup>
import SideBar from './components/Sidebar/index.vue'
import Breadcrumb from './components/breadcrumb/index.vue'
</script><template><div class="common-layout"><el-container><!-- <el-aside width="200px">Aside</el-aside> --><SideBar /><el-container><el-header :style="{ display: 'flex', alignItems: 'center'}"><Breadcrumb /></el-header><el-main><!-- 内容区域变化 --><RouterView /></el-main><el-footer>Footer</el-footer></el-container></el-container></div>
</template>
7.封装数据请求
axios https://www.axios-http.cn/docs/intro
// restful api
// get
// post
// put 更新
// patch 更新
// delete 删除// get
fetch('url?a=1&b=2').then(res => res.json()).then(res => { console.log(res) })
axios.get('url?a=1&b=2').then(res => { console.log(res) })
axios.get('url', { params: { a: 1, b: 2} }).then(res => { console.log(res) })
axios({url: 'url?a=1&b=2',method: 'get'
}).then(res => { console.log(res) })
axios({url: 'url',method: 'get',params: { a: 1, b: 2}
}).then(res => { console.log(res) })// post
axios.post('url', { a: 1, b: 2 }).then(res => { console.log(res) })
axios({url: 'url',method: 'post',data: { a: 1, b: 2}
}).then(res => { console.log(res) })// axios 拦截器
// 请求拦截器
// 响应拦截器
// 添加请求拦截器
axios.interceptors.request.use(function (config) {// 在发送请求之前做些什么// 显示loading动画效果return config;}, function (error) {// 对请求错误做些什么return Promise.reject(error);});// 添加响应拦截器
axios.interceptors.response.use(function (response) {// 2xx 范围内的状态码都会触发该函数。// 对响应数据做点什么// loading动画消失return response;}, function (error) {// 超出 2xx 范围的状态码都会触发该函数。// 对响应错误做点什么return Promise.reject(error);});
一般项目中都需要重新定义axios,需要对axios进行配置
// 创建实例时配置默认值
const instance = axios.create({baseURL: 'https://api.example.com'
});// 创建实例后修改默认值
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;
// https://api.example.com/api/pro/list
instance.get('/api/pro/list')
接口文档:http://121.89.205.189:3000/admindoc/
// src/utils/request.ts
// 1.引入axios
import axios from 'axios'
// 因为router 是单独暴露的,此处可以直接引入使用
import router from '@/router'
// 2.自定义axios
// 接口文档: http://121.89.205.189:3000/admindoc/
const ins = axios.create({baseURL: 'http://121.89.205.189:3000/admin'
})// 3.配置拦截器
// 请求拦截器
ins.interceptors.request.use((config) => {// 请求之前干什么// 后台管理系统所有的页面都需要从服务器获取数据// 思考:要数据就必须是登录才可以// 请求数据时需要将 是否 登录的标识传递给 服务器,服务器判断登录标识 是否可用,// 如果可以使用,返回你要的数据,如果不可以使用,告诉用户,登录失效,用户重新登录// 登录标识:token 登录以后后端返回给前端,前端一般会将其存入到本地存储// 如何将 token 传递给服务器,一般从本地存储提取,然后在 头信息(请求头) 中传递config.headers!.token = localStorage.getItem('token') || ''return config
}, (error) => {return Promise.reject(error)
})// 响应拦截器
ins.interceptors.response.use((response) => {// 判断登录标识 是否有效,如果无效跳转至登录页面,如果有效,不做操作if (response.data.code === '10119') {// 没有传递token 或者 token过期// 跳转到登录页面 重新登录router.push('/login')} return response
}, (error) => {return Promise.reject(error)
})// 暴露自定义的 axios
export default ins
所有的数据请求都要先登录,登录状态在非常多的页面都需要使用
- 多个视图需要依赖同一个状态
- 来自不同视图的行为需要变更同一个状态
- 首选状态管理器
8.pinia状态管理器
https://pinia.vuejs.org/zh/
8.1 入口注册pinia
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'// 引入UI库
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'// 引入图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'import App from './App.vue'
import router from './router'// 重置样式表
import 'normalize.css/normalize.css'// import './assets/main.css' // 不使用默认样式const app = createApp(App)
// 配置图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component)
}// 配置UI库
app.use(ElementPlus, { locale: zhCn })
app.use(createPinia())
app.use(router)app.mount('#app')
8.2 定义状态管理器模块
// src/store/counter.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'// 选项式API
// export const useCounterStore = defineStore('counter', {// state () { // 初始化数据 -- data
// return {// count: 99
// }
// },
// getters: { // 计算属性
// doubleCount: state => { // state 即为 当前状态管理器中的 所有的状态
// return state.count * 2
// }
// },
// actions: { // 事件 ---- methods
// increment () {// this.count += 10
// },
// decrement () {// this.count -= 10
// }
// }// })// 组合式API
export const useCounterStore = defineStore('counter', () => {// 初始化const count = ref(0)// 计算属性const doubleCount = computed(() => count.value * 2)// 方法function increment() {count.value += 10}function decrement() {count.value -= 10}// 返回˙状态和方法return { count, doubleCount, increment, decrement }
})
8.3 组件中使用
8.3.1 模版中使用对象渲染
<!-- src/views/home/index.vue -->
<script lang="ts" setup>import { computed } from 'vue'import { useCounterStore } from '@/stores/counter'const counter = useCounterStore()// 提取模版中需要使用的数据const count = computed(() => counter.count)const doubleCount = computed(() => counter.doubleCount)const reduce = () => {counter.decrement()}const add = () => {counter.increment()}
</script><template><div>系统首页</div><button @click="reduce">-10</button> {{ count }} - {{ counter.count }} - {{ counter.doubleCount }} - {{ doubleCount }}<button @click="add">+10</button>
</template>
<!-- src/views/account/user.vue -->
<script lang="ts" setup>import { useCounterStore } from '@/stores/counter'const counter = useCounterStore()const reduce = () => {counter.decrement()}const add = () => {counter.increment()}
</script><template><div>用户列表</div><button @click="reduce">-10</button> {{ counter.count }} - {{ counter.doubleCount }}<button @click="add">+10</button>
</template>
9登录功能实现
9.1 构建登录页面
<!-- src/views/login/index.vue -->
<script lang="ts" setup>import { ref, computed } from 'vue';const adminname = ref('')const password = ref('')// 按钮是否可点const flag = computed(() => { return adminname.value === '' || password.value === ''})
</script><template><div class="loginBox"><div class="loginForm"><div class="loginTitle">系统登录</div><div class="loginInput"><el-input v-model="adminname" placeholder="管理员账户" clearable prefix-icon="User"/></div><div class="loginInput"><el-input type="password" v-model="password" clearable placeholder="管理员密码" show-password prefix-icon="Lock"/></div><el-button type="primary" :disabled="flag" class="loginBtn">登录</el-button><span>默认账户名:admin 默认密码:123456</span></div></div>
</template><style lang="scss">.loginBox {width: 100%;height: 100%;background-color: #2b2b2b;display: flex;justify-content: center;align-items: center;color: #fff;.loginForm {width: 500px;min-height: 300px;/* background-color: #fff; */.loginTitle {text-align: center;font-size: 24px;font-weight: bold;}.loginInput {margin: 20px 0;.el-input {height: 40px;.el-input__wrapper {background-color: #2b2b2b;.el-input__inner {background-color: #2b2b2b;}}}}.loginBtn {width: 100%;height: 36px;margin-bottom: 20px;}}}
</style>
9.2 封装管理员登录的数据请求
// src/api/admin.ts
import request from '@/utils/request'// interface IAdminLoginParams {// adminname: string
// password: string
// }type TAdminLoginParams = {adminname: stringpassword: string
}export function adminLogin (params: TAdminLoginParams) {return request.post('/admin/login', params)
}
9.3 登录页面调用登录接口
<!-- src/views/login/index.vue -->
<script lang="ts" setup>import { ref, computed } from 'vue';import { ElMessage } from 'element-plus'import { adminLogin } from '@/api/admin'import { useRouter } from 'vue-router';const adminname = ref('')const password = ref('')// 按钮是否可点const flag = computed(() => { return adminname.value === '' || password.value === ''})const router = useRouter()const login = () => {adminLogin({adminname: adminname.value,password: password.value}).then(res => {console.log(res)if (res.data.code === '10005') {console.log('账户未注册')ElMessage.error('账户未注册.')} else if (res.data.code === '10003') {ElMessage.warning('密码错误')} else {ElMessage.success('登录成功')// 将登陆信息保存到本地localStorage.setItem('adminname', res.data.data.adminname)localStorage.setItem('checkedkeys', res.data.data.checkedkeys)localStorage.setItem('role', res.data.data.role)localStorage.setItem('token', res.data.data.token)router.replace('/')}})}
</script><template><div class="loginBox"><div class="loginForm"><div class="loginTitle">系统登录</div><div class="loginInput"><el-input v-model="adminname" placeholder="管理员账户" clearable prefix-icon="User"/></div><div class="loginInput"><el-input type="password" v-model="password" clearable placeholder="管理员密码" show-password prefix-icon="Lock"/></div><el-button type="primary" @click="login" :disabled="flag" class="loginBtn">登录</el-button><span>默认账户名:admin 默认密码:123456</span></div></div>
</template><style lang="scss">.loginBox {width: 100%;height: 100%;background-color: #2b2b2b;display: flex;justify-content: center;align-items: center;color: #fff;.loginForm {width: 500px;min-height: 300px;/* background-color: #fff; */.loginTitle {text-align: center;font-size: 24px;font-weight: bold;}.loginInput {margin: 20px 0;.el-input {height: 40px;.el-input__wrapper {background-color: #2b2b2b;.el-input__inner {background-color: #2b2b2b;}}}}.loginBtn {width: 100%;height: 36px;margin-bottom: 20px;}}}
</style>
9.4 退出登录
<!-- src/layout/index.vue -->
<!-- 后台管理系统两种布局:登录布局 + 其他布局 -->
<script lang="ts" setup>import SideBar from './components/Sidebar/index.vue'import Breadcrumb from './components/breadcrumb/index.vue'import { ArrowDown } from '@element-plus/icons-vue'import { useRouter } from 'vue-router';const router = useRouter()const adminname = localStorage.getItem('adminname')const logout = () => {localStorage.removeItem('adminname')localStorage.removeItem('token')localStorage.removeItem('checkedkeys')localStorage.removeItem('role')router.push('/login')}
</script><template><div class="common-layout"><el-container><!-- <el-aside width="200px">Aside</el-aside> --><SideBar /><el-container><el-header :style="{ display: 'flex', alignItems: 'center'}"><Breadcrumb /><el-dropdown :style="{ position: 'absolute', right: '16px'}"><span class="el-dropdown-link">欢迎您,{{ adminname }}<el-icon class="el-icon--right"><arrow-down /></el-icon></span><template #dropdown><el-dropdown-menu><el-dropdown-item>个人中心</el-dropdown-item><el-dropdown-item divided @click="logout">退出</el-dropdown-item></el-dropdown-menu></template></el-dropdown></el-header><el-main><!-- 内容区域变化 --><RouterView /></el-main><el-footer>Footer</el-footer></el-container></el-container></div>
</template>
9.5 登录信息状态管理
// src/store/admins.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'// 组合式API
export const useAdminStore = defineStore('admin', () => {// 初始化const adminname = ref(localStorage.getItem('adminname') || '')const token = ref(localStorage.getItem('token') || '')const changeAdminname = (val: string) => {adminname.value = val}const changeToken = (val: string) => {token.value = val}// 返回˙状态和方法return { adminname, token, changeAdminname, changeToken }
})
9.6 登录页面修改 状态以及布局页面使用状态
<!-- src/views/login/index.vue -->
<script lang="ts" setup>import { ref, computed } from 'vue';import { ElMessage } from 'element-plus'import { adminLogin } from '@/api/admin'import { useRouter } from 'vue-router';import { useAdminStore } from '@/stores/admins';const adminname = ref('admin')const password = ref('123456')const admin = useAdminStore()// 按钮是否可点const flag = computed(() => { return adminname.value === '' || password.value === ''})const router = useRouter()const login = () => {// 正则验证adminLogin({adminname: adminname.value,password: password.value}).then(res => {// console.log(res)if (res.data.code === '10005') {console.log('账户未注册')ElMessage.error('账户未注册.')} else if (res.data.code === '10003') {ElMessage.warning('密码错误')} else {ElMessage.success('登录成功')// 将登陆信息保存到本地localStorage.setItem('adminname', res.data.data.adminname)localStorage.setItem('checkedkeys', res.data.data.checkedkeys)localStorage.setItem('role', res.data.data.role)localStorage.setItem('token', res.data.data.token)// 可以将数据保存到状态管理器admin.changeAdminname(res.data.data.adminname)admin.changeToken(res.data.data.token)router.replace('/')}})}
</script><template><div class="loginBox"><div class="loginForm"><div class="loginTitle">系统登录</div><div class="loginInput"><el-input v-model="adminname" placeholder="管理员账户" clearable prefix-icon="User"/></div><div class="loginInput"><el-input type="password" v-model="password" clearable placeholder="管理员密码" show-password prefix-icon="Lock"/></div><el-button type="primary" @click="login" :disabled="flag" class="loginBtn">登录</el-button><span>默认账户名:admin 默认密码:123456</span></div></div>
</template><style lang="scss">.loginBox {width: 100%;height: 100%;background-color: #2b2b2b;display: flex;justify-content: center;align-items: center;color: #fff;.loginForm {width: 500px;min-height: 300px;/* background-color: #fff; */.loginTitle {text-align: center;font-size: 24px;font-weight: bold;}.loginInput {margin: 20px 0;.el-input {height: 40px;.el-input__wrapper {background-color: #2b2b2b;.el-input__inner {background-color: #2b2b2b;}}}}.loginBtn {width: 100%;height: 36px;margin-bottom: 20px;}}}
</style>
<!-- src/layout/index.vue -->
<!-- 后台管理系统两种布局:登录布局 + 其他布局 -->
<script lang="ts" setup>import SideBar from './components/Sidebar/index.vue'import Breadcrumb from './components/breadcrumb/index.vue'import { ArrowDown } from '@element-plus/icons-vue'import { useRouter } from 'vue-router';import { useAdminStore } from '@/stores/admins'import { computed } from 'vue';const router = useRouter()// const adminname = localStorage.getItem('adminname')const admin = useAdminStore()const adminname = computed(() => admin.adminname)const logout = () => {localStorage.removeItem('adminname')localStorage.removeItem('token')localStorage.removeItem('checkedkeys')localStorage.removeItem('role')router.push('/login')}
</script><template><div class="common-layout"><el-container><!-- <el-aside width="200px">Aside</el-aside> --><SideBar /><el-container><el-header :style="{ display: 'flex', alignItems: 'center'}"><Breadcrumb /><el-dropdown :style="{ position: 'absolute', right: '16px'}"><span class="el-dropdown-link">欢迎您1,{{ adminname }}<el-icon class="el-icon--right"><arrow-down /></el-icon></span><template #dropdown><el-dropdown-menu><el-dropdown-item>个人中心</el-dropdown-item><el-dropdown-item divided @click="logout">退出</el-dropdown-item></el-dropdown-menu></template></el-dropdown></el-header><el-main><!-- 内容区域变化 --><RouterView /></el-main><el-footer>Footer</el-footer></el-container></el-container></div>
</template>
10 管理员的操作
10.1 渲染管理员列表
封装获取管理员信息的接口
// src/api/admin.ts
import request from '@/utils/request'// interface IAdminLoginParams {// adminname: string
// password: string
// }type TAdminLoginParams = {adminname: stringpassword: string
}export function adminLogin (params: TAdminLoginParams) {return request.post('/admin/login', params)
}export function getAdminList () {return request.get('/admin/list')
}
<!-- src/views/account/Admin.vue -->
<script lang="ts" setup>import { ref, onMounted } from 'vue';import { getAdminList } from '@/api/admin'interface IAdminInfo {adminid: stringadminname: stringrole: number}const adminList = ref<IAdminInfo[]>([])onMounted(() => {getAdminList().then(res => {console.log('admin', res.data)adminList.value = res.data.data})})
</script><template><el-table :data="adminList" style="width: 100%"><el-table-column label="序号"><template #default="scope"><!-- scope.$index 代表索引值 --><span>{{ scope.$index + 1 }}</span></template></el-table-column><el-table-column prop="adminname" label="管理员账户" width="180" /><el-table-column prop="role" label="角色" width="180" ><template #default="scope"><!-- scope.row 代表一条数据 --><el-tag v-if="scope.row.role === 2" class="ml-2" type="success">超级管理员</el-tag><el-tag v-else class="ml-2" type="info">管理员</el-tag></template></el-table-column><el-table-column label="操作"><template #default="scope"><el-button size="small">编辑</el-button><el-buttonsize="small"type="danger">删除</el-button></template></el-table-column></el-table>
</template>
10.2 分页实现
<!-- src/views/account/Admin.vue -->
<script lang="ts" setup>import { ref, onMounted, computed } from 'vue';import { getAdminList } from '@/api/admin'interface IAdminInfo {adminid: stringadminname: stringrole: number}// 将展示数据adminList 变为了 计算属性的数据// 将依据原始数据 originalList 以及 页码currentPage 和每页显示个数pageSize 一起计算出来显示的数据adminList// const adminList = ref<IAdminInfo[]>([])const currentPage = ref(1)const pageSize = ref(10)const originalList = ref<IAdminInfo[]>([])const changeCurrentPage = (val: number) => { // 页码改变函数currentPage.value = val}const changeSize = (val: number) => { // 每页显示个数改变的函数pageSize.value = val}const adminList = computed(() => {// 截取数组的某一些项// 深拷贝数据作为备份const arr = JSON.parse(JSON.stringify(originalList.value))const newArr = arr.splice((currentPage.value - 1) * pageSize.value, pageSize.value)// console.log(originalList.value)return newArr})onMounted(() => {getAdminList().then(res => {// console.log('admin', res.data)originalList.value = res.data.data})})</script><template><el-table :data="adminList" style="width: 100%"><el-table-column label="序号"><template #default="scope"><!-- scope.$index 代表索引值 --><span>{{ scope.$index + 1 }}</span></template></el-table-column><el-table-column prop="adminname" label="管理员账户" width="180" /><el-table-column prop="role" label="角色" width="180" ><template #default="scope"><!-- scope.row 代表一条数据 --><el-tag v-if="scope.row.role === 2" class="ml-2" type="success">超级管理员</el-tag><el-tag v-else class="ml-2" type="info">管理员</el-tag></template></el-table-column><el-table-column label="操作"><template #default="scope"><el-button size="small">编辑</el-button><el-buttonsize="small"type="danger">删除</el-button></template></el-table-column></el-table><el-pagination background layout="sizes, prev, pager, next" :page-sizes="[5, 10, 20, 30]":total="originalList.length" @current-change="changeCurrentPage"@size-change="changeSize"/>
</template>
filter处理数据
const adminList = computed(() => {const newArr = originalList.value.filter((item, index) => {return index >= (currentPage.value - 1) * pageSize.value && index < (currentPage.value - 1) * pageSize.value + pageSize.value})return newArr})
10.3 删除管理员
// src/api/admin.ts
import request from '@/utils/request'// interface IAdminLoginParams {// adminname: string
// password: string
// }type TAdminLoginParams = {adminname: stringpassword: string
}export function adminLogin (params: TAdminLoginParams) {return request.post('/admin/login', params)
}export function getAdminList () {return request.get('/admin/list')
}export function deleteAdmin (params: { adminid: string }) {return request.post('/admin/delete', params)
}
<!-- src/views/account/Admin.vue -->
<script lang="ts" setup>import { ref, onMounted, computed } from 'vue';import { getAdminList, deleteAdmin } from '@/api/admin'interface IAdminInfo {adminid: stringadminname: stringrole: number}// 将展示数据adminList 变为了 计算属性的数据// 将依据原始数据 originalList 以及 页码currentPage 和每页显示个数pageSize 一起计算出来显示的数据adminList// const adminList = ref<IAdminInfo[]>([])const currentPage = ref(1)const pageSize = ref(10)const originalList = ref<IAdminInfo[]>([])const changeCurrentPage = (val: number) => { // 页码改变函数currentPage.value = val}const changeSize = (val: number) => { // 每页显示个数改变的函数pageSize.value = val}const adminList = computed(() => {// 截取数组的某一些项// 深拷贝数据作为备份// const arr = JSON.parse(JSON.stringify(originalList.value))// const newArr = arr.splice((currentPage.value - 1) * pageSize.value, pageSize.value)const newArr = originalList.value.filter((item, index) => {return index >= (currentPage.value - 1) * pageSize.value && index < (currentPage.value - 1) * pageSize.value + pageSize.value})// console.log(originalList.value)return newArr})const getAdminListData = () => {getAdminList().then(res => {// console.log('admin', res.data)originalList.value = res.data.data})}onMounted(() => {getAdminListData()})const removeItem = (adminid: string) => {deleteAdmin({ adminid }).then(() => {getAdminListData()})}
</script><template><el-table :data="adminList" style="width: 100%"><el-table-column label="序号"><template #default="scope"><!-- scope.$index 代表索引值 --><span>{{ (currentPage - 1) * pageSize + scope.$index + 1 }}</span></template></el-table-column><el-table-column prop="adminname" label="管理员账户" width="180" /><el-table-column prop="role" label="角色" width="180" ><template #default="scope"><!-- scope.row 代表一条数据 --><el-tag v-if="scope.row.role === 2" class="ml-2" type="success">超级管理员</el-tag><el-tag v-else class="ml-2" type="info">管理员</el-tag></template></el-table-column><el-table-column label="操作"><template #default="scope"><el-button size="small">编辑</el-button><el-popconfirm title="确定删除吗?" @confirm="removeItem(scope.row.adminid)"><template #reference><el-buttonsize="small"type="danger">删除</el-button></template></el-popconfirm></template></el-table-column></el-table><el-pagination background layout="sizes, prev, pager, next" :page-sizes="[5, 10, 20, 30]":total="originalList.length" @current-change="changeCurrentPage"@size-change="changeSize"/>
</template>
10.4 添加管理员
10.4.1 封装添加管理员接口
// src/api/admin.ts
import request from '@/utils/request'// interface IAdminLoginParams {// adminname: string
// password: string
// }type TAdminLoginParams = {adminname: stringpassword: string
}export function adminLogin (params: TAdminLoginParams) {return request.post('/admin/login', params)
}export function getAdminList () {return request.get('/admin/list')
}export function deleteAdmin (params: { adminid: string }) {return request.post('/admin/delete', params)
}interface IAdminAddParams {adminname: stringpassword: stringrole: numbercheckedKeys: any // 因为开放,索引设置为any
}
export function addAdmin (params: IAdminAddParams) {return request.post('/admin/add', params)
}
10.4.2 封装获取树形控件数据方法
// src/utils/get-routes.tsimport type { MyRouteRecordRaw } from "@/router";interface Tree {id: stringlabel: stringchildren?: Tree[]
}export function getRoutes (menuList: MyRouteRecordRaw[]) {const arr: Tree[] = []menuList.forEach((item) => {let obj: Tree = {id: '',label: ''}if (item.meta) {if (item.children) { // 动态那些数据obj = {id: item.path,label: String(item.meta.title),children: getRoutes(item.children)}} else {obj = {id: item.path,label: String(item.meta.title)}}} else {if (item.children) { // 系统首页obj = {id: item.children[0].path,label: String(item.children[0].meta!.title)}}}// arr.push(obj)obj.id !== '' && arr.push(obj)});return arr
}// getRoutes(menuList)
10.3.4 添加管理员
<!-- src/views/account/Admin.vue -->
<script lang="ts" setup>import { ref, onMounted, computed } from 'vue';import { getAdminList, deleteAdmin, addAdmin } from '@/api/admin'import { constantRoutes, asyncRoutes } from '@/router'import { getRoutes } from '@/utils/get-routes'interface IAdminInfo {adminid: stringadminname: stringrole: number}// 将展示数据adminList 变为了 计算属性的数据// 将依据原始数据 originalList 以及 页码currentPage 和每页显示个数pageSize 一起计算出来显示的数据adminList// const adminList = ref<IAdminInfo[]>([])const currentPage = ref(1)const pageSize = ref(10)const originalList = ref<IAdminInfo[]>([])const changeCurrentPage = (val: number) => { // 页码改变函数currentPage.value = val}const changeSize = (val: number) => { // 每页显示个数改变的函数pageSize.value = val}const adminList = computed(() => {// 截取数组的某一些项// 深拷贝数据作为备份// const arr = JSON.parse(JSON.stringify(originalList.value))// const newArr = arr.splice((currentPage.value - 1) * pageSize.value, pageSize.value)const newArr = originalList.value.filter((item, index) => {return index >= (currentPage.value - 1) * pageSize.value && index < (currentPage.value - 1) * pageSize.value + pageSize.value})// console.log(originalList.value)return newArr})const getAdminListData = () => {getAdminList().then(res => {// console.log('admin', res.data)originalList.value = res.data.data})}onMounted(() => {getAdminListData()})const removeItem = (adminid: string) => {deleteAdmin({ adminid }).then(() => {getAdminListData()})}// 添加管理员const drawer = ref(false)const adminname = ref('')const password = ref('')const role = ref(1)const checkedKeys = ref<string[]>([])const menuList = [...constantRoutes, ...asyncRoutes]const treeData = getRoutes(menuList)const onTreeCheck = (_: any, obj: { halfCheckedKeys: string[]; checkedKeys: string[]}) => {checkedKeys.value = [...obj.halfCheckedKeys, ...obj.checkedKeys].sort()}const addAdminFn = () => {addAdmin({adminname: adminname.value,password: password.value,role: role.value,checkedKeys: checkedKeys.value}).then(() => {// 重置表单状态adminname.value = ''password.value = ''role.value = 1checkedKeys.value = []// 关闭抽屉效果drawer.value = falsegetAdminListData()})}const closeDrawer = () => {// 重置表单状态adminname.value = ''password.value = ''role.value = 1checkedKeys.value = []// 关闭抽屉效果drawer.value = false}
</script><template><el-button type="primary" @click="drawer=true">添加管理员</el-button><el-table :data="adminList" style="width: 100%"><el-table-column label="序号"><template #default="scope"><!-- scope.$index 代表索引值 --><span>{{ (currentPage - 1) * pageSize + scope.$index + 1 }}</span></template></el-table-column><el-table-column prop="adminname" label="管理员账户" width="180" /><el-table-column prop="role" label="角色" width="180" ><template #default="scope"><!-- scope.row 代表一条数据 --><el-tag v-if="scope.row.role === 2" class="ml-2" type="success">超级管理员</el-tag><el-tag v-else class="ml-2" type="info">管理员</el-tag></template></el-table-column><el-table-column label="操作"><template #default="scope"><el-button size="small">编辑</el-button><el-popconfirm title="确定删除吗?" @confirm="removeItem(scope.row.adminid)"><template #reference><el-buttonsize="small"type="danger">删除</el-button></template></el-popconfirm></template></el-table-column></el-table><el-pagination background layout="sizes, prev, pager, next" :page-sizes="[5, 10, 20, 30]":total="originalList.length" @current-change="changeCurrentPage"@size-change="changeSize"/><!-- 添加管理员 抽屉效果 --><el-drawer v-model="drawer" title="I am the title" :with-header="false" @close="closeDrawer"><h1>添加管理员</h1><el-input v-model="adminname" placeholder="管理员账户" clearable/><el-input v-model="password" placeholder="密码" clearable /><el-select v-model="role" placeholder="管理员角色" ><el-optionlabel="管理员":value="1"/><el-optionlabel="超级管理员":value="2"/></el-select><el-treeref="treeRef":data="treeData"show-checkboxnode-key="id"@check="onTreeCheck"/><el-button type="primary" @click="addAdminFn">添加</el-button></el-drawer>
</template>
10.5 编辑管理员
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Py3yNhxo-1672888918417)(assets/image-20221230153508095.png)]
// src/api/admin.ts
import request from '@/utils/request'// interface IAdminLoginParams {// adminname: string
// password: string
// }type TAdminLoginParams = {adminname: stringpassword: string
}export function adminLogin (params: TAdminLoginParams) {return request.post('/admin/login', params)
}export function getAdminList () {return request.get('/admin/list')
}export function deleteAdmin (params: { adminid: string }) {return request.post('/admin/delete', params)
}interface IAdminAddParams {adminname: stringpassword: stringrole: numbercheckedKeys: any // 因为开放,索引设置为any
}
export function addAdmin (params: IAdminAddParams) {return request.post('/admin/add', params)
}interface IAdminUpdateParams {adminname: stringrole: numbercheckedKeys: any // 因为开放,索引设置为any
}
export function updateAdmin (params: IAdminUpdateParams) {return request.post('/admin/update', params)
}t
<!-- src/views/account/Admin.vue -->
<script lang="ts" setup>import { ref, onMounted, computed } from 'vue';import { getAdminList, deleteAdmin, addAdmin, updateAdmin } from '@/api/admin'import { constantRoutes, asyncRoutes } from '@/router'import { ElTree } from 'element-plus'import { getRoutes } from '@/utils/get-routes'interface IAdminInfo {adminid: stringadminname: stringrole: number}// 将展示数据adminList 变为了 计算属性的数据// 将依据原始数据 originalList 以及 页码currentPage 和每页显示个数pageSize 一起计算出来显示的数据adminList// const adminList = ref<IAdminInfo[]>([])const currentPage = ref(1)const pageSize = ref(10)const originalList = ref<IAdminInfo[]>([])const changeCurrentPage = (val: number) => { // 页码改变函数currentPage.value = val}const changeSize = (val: number) => { // 每页显示个数改变的函数pageSize.value = val}const adminList = computed(() => {// 截取数组的某一些项// 深拷贝数据作为备份// const arr = JSON.parse(JSON.stringify(originalList.value))// const newArr = arr.splice((currentPage.value - 1) * pageSize.value, pageSize.value)const newArr = originalList.value.filter((item, index) => {return index >= (currentPage.value - 1) * pageSize.value && index < (currentPage.value - 1) * pageSize.value + pageSize.value})// console.log(originalList.value)return newArr})const getAdminListData = () => {getAdminList().then(res => {console.log('admin', res.data)originalList.value = res.data.data})}onMounted(() => {getAdminListData()})const removeItem = (adminid: string) => {deleteAdmin({ adminid }).then(() => {getAdminListData()})}// 添加管理员const drawer = ref(false)const adminname = ref('')const password = ref('')const role = ref(1)const checkedKeys = ref<string[]>([])const menuList = [...constantRoutes, ...asyncRoutes]const treeData = getRoutes(menuList)const onTreeCheck = (_: any, obj: { halfCheckedKeys: string[]; checkedKeys: string[]}) => {checkedKeys.value = [...obj.halfCheckedKeys, ...obj.checkedKeys].sort()}const addAdminFn = () => {addAdmin({adminname: adminname.value,password: password.value,role: role.value,checkedKeys: checkedKeys.value}).then(() => {// 重置表单状态adminname.value = ''password.value = ''role.value = 1checkedKeys.value = []// 关闭抽屉效果drawer.value = falsegetAdminListData()})}const closeDrawer = () => {// 重置表单状态adminname.value = ''role.value = 1checkedKeys.value = []// 关闭抽屉效果drawer.value = false}// 修改管理员const treeUpdateRef = ref<InstanceType<typeof ElTree>>()const dialog = ref(false)const updateAdminInfo = (item: any) => {dialog.value = trueadminname.value = item.adminnamerole.value = item.role// console.log(item)// checkedKeys.value = item.checkedKeys// 添加延时器确保DOM加载完毕, 否则显示 setCheckedKeys 未定义setTimeout(() => {treeUpdateRef.value!.setCheckedKeys(item.checkedKeys, false)}, 0)}const updateAdminInfoFn = () => {// console.log('111', {// adminname: adminname.value,// role: role.value,// checkedKeys: checkedKeys.value// })updateAdmin({adminname: adminname.value,role: role.value,checkedKeys: checkedKeys.value}).then((res) => {// console.log('22222', res.data)// 重置表单状态adminname.value = ''role.value = 1checkedKeys.value = []// 关闭抽屉效果dialog.value = falsegetAdminListData()})}
</script><template><el-button type="primary" @click="drawer=true">添加管理员</el-button><el-table :data="adminList" style="width: 100%"><el-table-column label="序号"><template #default="scope"><!-- scope.$index 代表索引值 --><span>{{ (currentPage - 1) * pageSize + scope.$index + 1 }}</span></template></el-table-column><el-table-column prop="adminname" label="管理员账户" width="180" /><el-table-column prop="role" label="角色" width="180" ><template #default="scope"><!-- scope.row 代表一条数据 --><el-tag v-if="scope.row.role === 2" class="ml-2" type="success">超级管理员</el-tag><el-tag v-else class="ml-2" type="info">管理员</el-tag></template></el-table-column><el-table-column label="操作"><template #default="scope"><el-button size="small" @click="updateAdminInfo(scope.row)">编辑</el-button><el-popconfirm title="确定删除吗?" @confirm="removeItem(scope.row.adminid)"><template #reference><el-buttonsize="small"type="danger">删除</el-button></template></el-popconfirm></template></el-table-column></el-table><el-pagination background layout="sizes, prev, pager, next" :page-sizes="[5, 10, 20, 30]":total="originalList.length" @current-change="changeCurrentPage"@size-change="changeSize"/><!-- 添加管理员 抽屉效果 --><el-drawer v-model="drawer" title="I am the title" :with-header="false" @close="closeDrawer"><h1>添加管理员</h1><el-input v-model="adminname" placeholder="管理员账户" clearable/><el-input v-model="password" placeholder="密码" clearable /><el-select v-model="role" placeholder="管理员角色" ><el-optionlabel="管理员":value="1"/><el-optionlabel="超级管理员":value="2"/></el-select><el-treeref="treeAddRef":data="treeData"show-checkboxnode-key="id"@check="onTreeCheck"/><el-button type="primary" @click="addAdminFn">添加</el-button></el-drawer><!-- 编辑管理员 --><el-dialog v-model="dialog" title="编辑管理员信息"><el-input v-model="adminname" readonly placeholder="管理员账户" clearable/><el-select v-model="role" placeholder="管理员角色" ><el-optionlabel="管理员":value="1"/><el-optionlabel="超级管理员":value="2"/></el-select><el-treeref="treeUpdateRef":data="treeData"show-checkboxnode-key="id"@check="onTreeCheck"/><template #footer><span class="dialog-footer"><el-button type="primary" @click="updateAdminInfoFn">修改</el-button></span></template></el-dialog>
</template>
11.左侧菜单栏动态渲染
11.1 处理左侧菜单数据
此算法建议直接拿来使用
// src/utils/get-menu-list.ts
import type { MyRouteRecordRaw } from "@/router";export function getMenuList (menus: MyRouteRecordRaw[], checkedkeys: string[]) {if (checkedkeys.length === 0) { // 如果没有设置权限,那么即将拥有所有的权限checkedkeys = ['/home', '/banner', '/banner/list', '/banner/list/home', '/banner/list/kind', '/banner/add', '/pro', '/pro/list', '/pro/search', '/account', '/account/user', '/account/admin']}let result = getResult(menus)function getResult(menus: MyRouteRecordRaw[]) {let arr: MyRouteRecordRaw[]= []menus.forEach(item => {if (item.meta) {if (checkedkeys.includes(item.path)) {arr.push({...item})if (item.children) {arr[arr.length - 1].children = getResult(item.children)}}} else { // 系统首页arr.push(item)}});return arr }return result
}
11.2 动态渲染左侧菜单
<!-- src/layout/components/Sidebar/index.vue -->
<script lang="ts" setup>import { ref } from 'vue'import SidebarItem from './SidebarItem.vue'import Logo from './../logo/index.vue'import { getMenuList } from '@/utils/get-menu-list'// 整合路由import { constantRoutes, asyncRoutes } from '@/router'import { useRoute } from 'vue-router';// /banner,/banner/add,/banner/list,/banner/list/home,/banner/list/kind,/homeconst checkedkeys = localStorage.getItem('adminname') !== 'admin' ? localStorage.getItem('checkedkeys')!.split(',') : []console.log('checkedkeys', checkedkeys)// const menuList = constantRoutes.concat(asyncRoutes) // 所有数据const menuList = getMenuList(constantRoutes.concat(asyncRoutes), checkedkeys) // 真实数据// console.log(menuList)// 获取路由地址// 选项式API this.$routeconst route = useRoute()// console.log(route) // route.path.valueconst collapse = ref(false)
</script>
<template><el-aside :width="collapse ? '50px' : '200px'"><!-- <div class="logo"></div> --><Logo :collapse="collapse"/><!-- 菜单组件https://element-plus.gitee.io/zh-CN/component/menu.html#%E4%BE%A7%E6%A0%8F--><!-- <div>左侧菜单</div> --><el-menu:collapse="collapse":unique-opened="true":default-active="route.path"background-color="#001529"text-color="#fff":collapse-transition="false"><!-- 路由中的每一个选项都需要单独去渲染菜单项有的不需要出现在左侧的菜单栏有的只有一个菜单项有的有二级菜单和三级菜单建议把每个菜单项单独封装成一个组件 SidebarItem登录 系统首页 产品管理 账户管理 轮播图管理将每一项数据 item 传输到子组件,子组件接收数据,并且渲染--><SidebarItem v-for="item of menuList":key="item.path":item = "item"></SidebarItem></el-menu><div class="changeIcon" :style="{ width: collapse ? '50px' : '200px'}"><el-icon color="#fff" v-if="collapse" @click="collapse = !collapse"><ArrowRight /></el-icon><el-icon color="#fff" v-else @click="collapse=!collapse"><ArrowLeft /></el-icon></div></el-aside>
</template><style lang="scss">
.changeIcon {position: absolute;bottom: 10px;display: flex;justify-content: center;
}
</style>
ckbox
node-key=“id”
@check=“onTreeCheck”
/>
<template #footer>
<el-button type=“primary” @click=“updateAdminInfoFn”>
修改
# 11.左侧菜单栏动态渲染## 11.1 处理左侧菜单数据此算法建议直接拿来使用```ts
// src/utils/get-menu-list.ts
import type { MyRouteRecordRaw } from "@/router";export function getMenuList (menus: MyRouteRecordRaw[], checkedkeys: string[]) {if (checkedkeys.length === 0) { // 如果没有设置权限,那么即将拥有所有的权限checkedkeys = ['/home', '/banner', '/banner/list', '/banner/list/home', '/banner/list/kind', '/banner/add', '/pro', '/pro/list', '/pro/search', '/account', '/account/user', '/account/admin']}let result = getResult(menus)function getResult(menus: MyRouteRecordRaw[]) {let arr: MyRouteRecordRaw[]= []menus.forEach(item => {if (item.meta) {if (checkedkeys.includes(item.path)) {arr.push({...item})if (item.children) {arr[arr.length - 1].children = getResult(item.children)}}} else { // 系统首页arr.push(item)}});return arr }return result
}
11.2 动态渲染左侧菜单
<!-- src/layout/components/Sidebar/index.vue -->
<script lang="ts" setup>import { ref } from 'vue'import SidebarItem from './SidebarItem.vue'import Logo from './../logo/index.vue'import { getMenuList } from '@/utils/get-menu-list'// 整合路由import { constantRoutes, asyncRoutes } from '@/router'import { useRoute } from 'vue-router';// /banner,/banner/add,/banner/list,/banner/list/home,/banner/list/kind,/homeconst checkedkeys = localStorage.getItem('adminname') !== 'admin' ? localStorage.getItem('checkedkeys')!.split(',') : []console.log('checkedkeys', checkedkeys)// const menuList = constantRoutes.concat(asyncRoutes) // 所有数据const menuList = getMenuList(constantRoutes.concat(asyncRoutes), checkedkeys) // 真实数据// console.log(menuList)// 获取路由地址// 选项式API this.$routeconst route = useRoute()// console.log(route) // route.path.valueconst collapse = ref(false)
</script>
<template><el-aside :width="collapse ? '50px' : '200px'"><!-- <div class="logo"></div> --><Logo :collapse="collapse"/><!-- 菜单组件https://element-plus.gitee.io/zh-CN/component/menu.html#%E4%BE%A7%E6%A0%8F--><!-- <div>左侧菜单</div> --><el-menu:collapse="collapse":unique-opened="true":default-active="route.path"background-color="#001529"text-color="#fff":collapse-transition="false"><!-- 路由中的每一个选项都需要单独去渲染菜单项有的不需要出现在左侧的菜单栏有的只有一个菜单项有的有二级菜单和三级菜单建议把每个菜单项单独封装成一个组件 SidebarItem登录 系统首页 产品管理 账户管理 轮播图管理将每一项数据 item 传输到子组件,子组件接收数据,并且渲染--><SidebarItem v-for="item of menuList":key="item.path":item = "item"></SidebarItem></el-menu><div class="changeIcon" :style="{ width: collapse ? '50px' : '200px'}"><el-icon color="#fff" v-if="collapse" @click="collapse = !collapse"><ArrowRight /></el-icon><el-icon color="#fff" v-else @click="collapse=!collapse"><ArrowLeft /></el-icon></div></el-aside>
</template><style lang="scss">
.changeIcon {position: absolute;bottom: 10px;display: flex;justify-content: center;
}
</style>
【Vue】后台管理系统相关推荐
- VUE后台管理系统权限管理
VUE后台管理系统权限管理(面试路由守卫) 1.背景 后台管理系统中总会遇到权限分配的问题:这也是一道vue的很经典的面试问题 2.解决思路 权限管理无非前端或者后台解决 先说一下前端解决的思路:在设 ...
- 12款vue后台管理系统(可下载)
vue官方的后台管理仪表盘框架是:vue-admin,除此之外还有很多其他优秀的后台管理集成框架,正所谓前人栽树,后人乘凉.特此记录一下,方面以后要用. 参考下载地址:12款优秀的vue后台管理系统模 ...
- 哔哩哔哩Allen前端vue后台管理系统笔记
哔哩哔哩Allen前端vue后台管理系统笔记 Element ui 引入 全局引入 按需引入 嵌套路由 左侧菜单栏的样式 Container布局,左侧菜单栏commonAside组件 commonAs ...
- 正确姿势开发vue后台管理系统
项目地址 vue-admin-webapp 欢迎star,fork 前言 相信许多人和我一样刚接触 vue 时看文档都很枯燥,看完 vue,还有 vueRouter .vuex .vue-cli.es ...
- vue后台管理系统搭建
vue后台管理系统搭建 前提 安装node.js 控制台安装yarn:npm install -g yarn 查看yarn版本:yarn --version 注:-g表示的是全局安装 淘宝镜像安装:n ...
- 看看人家 SpringBoot + vue后台管理系统,多么优雅...
如何写一个优雅的项目?为了让更多人学习前后端分离项目,特意录制了一个基于Spring security + Jwt + Vue的前后端分离后台管理系统VueAdmin,手把手完整教学,另外还写了两篇完 ...
- vue后台管理系统实践方案总结(一)
项目概述 基本业务概述 根据不同的应用场景,电商系统一般都提供了PC端.移动APP.移动Web.微信小程序等多种终端访问方式 管理系统功能 电商后台管理系统用于管理账号.商品分类.商品信息.订单.数据 ...
- vue后台管理系统打包上线到node
项目准备 1. 配置 alias 别名 使用vue-cli开发项目,最大特色是组件化.组件中频繁引用其他组件或插件.我们可以把一些常用的路径定义成简短的名字.方便开发中使用. //加载path模块 c ...
- Vue后台管理系统模板介绍
介绍:Vue.js 是一个目前比较流行的前端框架,今天给大家推荐几款基于 Vue 的后端管理的框架.目前比较流行和 Vue 搭配的 UI组件 有Element-UI.iview.Bootstrap-V ...
- 一套通用的VUE后台管理系统方案(vite+Vue3+ts)
通用后台管理系统整体架构方案(Vue) 项目创建,脚手架的选择(vite or vue-cli) vue-cli基于webpack封装,生态非常强大,可配置性也非常高,几乎能够满足前端工程化的所有要求 ...
最新文章
- linux数据库什么意思,Linux系统中的数据库命令是什么
- VMware ESXi客户端连接控制台时提示“VMRC控制台连接已断开...正在尝试重新连接“的解决方法
- 验证码生成java_JAVA-验证码生成
- 三层架构实现增删的简单实例
- C#基础--类/接口/成员修饰符,多态、重载、重写,静态和非静态
- 在Linux中挂载Windows端共享权限设定方法和出现报错的解决办法
- 获取占用fd最大的前20个进程
- visual studio 2017 连接 SQL Server
- Getphonenumber获得电话号码的例子
- postgre管理员 无法访问表_PostgreSQL常见问题处理方法
- 如何在页面显示json数据
- 注意!SQLite被曝漏洞,Chrome 火狐等数千应用或受影响
- RPM、SRPM和YUM,linux包rpm包管理工具
- Java基础SE.03.Java面向对象
- PCB线路板进行热设计的方法都有哪些?
- pytorch——梯度计算
- 广告公司到底干什么的?欣奥诚分享
- 关于技术人员创业的几点建议
- hdu6112今夕何夕
- 一个测试工程师的职业生涯感悟