视频网址:https://www.bilibili.com/video/BV1QU4y1E7qo/?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.click&vd_source=05357abb73955e1d5683092c544c1376
刚学完vue,感觉对于很多东西还不是很理解,但也没有时间去再复习,所以打算直接上手一个实战项目来边写边练,之前学的时候由于有别人现成的笔记,导致我很多时候都会有图轻松的想法,一直没有自己动手做过笔记,我也发现这样的做法导致我对学的东西一学就忘,很难在脑中留下深刻的印象,所以我觉得这次做项目要自己纯手敲笔记,保证每一个需求都在完成之后留下当时的想法。2023-12.20
当我们要创建一个项目的时候,里面有很多诸如api、store等需要按照固定规定完成的文件夹,如果交由程序员自己一点一点创建,过于繁琐,于是我们引入脚手架(vue-cli)的概念
官网:https://cli.vuejs.org/zh/guide/
启动脚手架之前需要先安装node,node可以理解成是一种环境
官网:https://nodejs.org/en
首先需要安装vue-cli,这里直接使用命令行的方式
npm install -g @vue/cli
# OR
yarn global add @vue/cli
安装完可以在命令行里输入vue -V
,来检测是否成功安装
安装完之后就可以在你想要创建项目的文件夹中打开cmd,输入vue create 项目名
来创建项目
创建完后可以使用npm run server
来启动项目
官网:https://element.eleme.cn/#/zh-CN
element-ui可以快速的帮助我们生成页面上的样式以及布局
在刚才创建好的项目中安装elemeng-ui
命令行输入:npm i element-ui -S
即可
安装完毕后可以在package.json文件中查看是否安装完成
如图便是安装完成了
①完整引入
在 main.js 中写入以下内容:
import Vue from 'vue';
//引入element-ui
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';
Vue.use(ElementUI); //全局引入
new Vue({
el: '#app',
render: h => h(App)
});
以上代码便完成了 Element 的引入。需要注意的是,样式文件需要单独引入。
②按需引入
借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积
的目的。
首先,安装 babel-plugin-component:npm install babel-plugin-component -D
然后,将 .babelrc 修改为:
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
这里有可能会报个错误,说es2015不存在,如果发生了上述报错,将es2015修改成@babel/preset-env
即可
接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:
import Vue from 'vue';
import { Button, Select } from 'element-ui'; //ES6解构语法
import App from './App.vue';
//按需引入
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为
* Vue.use(Button)
* Vue.use(Select)
*/
new Vue({
el: '#app',
render: h => h(App)
});
一般项目中还是使用按需引入较多
众所周知,我们使用vue就是为了实现单页应用,而想要在单页应用中实现路由跳转,这就需要我们用到vue-router
官网:https://router.vuejs.org/zh/
现在普遍vue-router是4.0的版本,但本项目需要用到3.0,因此安装时需要加点手段
一般情况下可以直接在命令行输入:npm install vue-router
但这样会直接安装成4.0,所以改成:npm install vue-router@3
即可安装3.0的版本
想要安装具体版本,可直接在@后输入具体的版本号
比如我想安装3.6.5的vue-router,输入npm install [email protected]
即可
至于想要查看vue-router3.0最新的版本是多少,可以直接上npm上进行搜索
安装完毕后,我们需要对router进行配置
一般情况下我们会在src目录下创建一个router
文件夹,在文件夹下创建index.js
来进行配置
如果在一个模块化工程中使用它,必须要通过 Vue.use()
明确地安装路由功能:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
如果使用全局的 script 标签,则无须如此 (手动安装)。
在src根目录下创建一个views文件夹,文件夹下就专门放路由组件
在路由配置文件中引入他们
//1.引入路由
import Home from '../views/Home.vue'
import User from '../views/User.vue'
//2.将组件和路由进行映射
const routes = [
{ path: '/home', component: Home },
{ path: '/user', component: User }
]
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})
// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
export default router
记得要通过 router 配置参数注入路由,从而让整个应用都有路由功能
进入main.js
引入router
//引入router
import router from './router';
最后挂载即可
new Vue({
render: h => h(App),
router
}).$mount('#app')
但这个时候你发现你去url上添加路由的路径,页面依然不会发生变化
这是因为缺少了最重要的路由出口的配置,这时候需要我们回到App.vue里添加上这样一段代码
这样我就可以实现单页面不同路由下显示不同的页面了,nice!
如图所示,我们的项目主要由左侧的导航栏、顶部的header和中间的主体组成
而我们通过点击左侧的导航栏会导致中间主体的内容出现变化,可导航按和header的内容是不变的,如果我们每一个页面都要重新写一遍导航栏和header的话就显得太笨了,因此我们引入了嵌套路由
的概念
嵌套路由官网介绍:https://v3.router.vuejs.org/zh/guide/essentials/nested-routes.html
/user/foo/profile /user/foo/posts
+------------------+ +-----------------+
| User | | User |
| +--------------+ | | +-------------+ |
| | Profile | | +------------> | | Posts | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
就如上述所示,在不同的路由下面,对于User部分是他们公用的,而变化的只有内部的区域,对于这样的情况我们进行怎样的代码的编写呢?
首先我们需要先创建一个主路由,和相应的子路由,就以上述的User,Profile和Posts举例,创建路由的过程参考Vue-router的第二点配置。
假设我们现在创建好相应的路由了,在main.js入口文件配置的时候我们需要更改一点东西
要在嵌套的出口中渲染组件,需要在 VueRouter
的参数中使用 children
配置:
//1.引入路由
import User from '../XXX/XXX.vue'
import Profile from '../XXX/XXX.vue'
import Posts from '../XXX/XXX.vue'
//2.将组件和路由进行映射
const routes = [
{
path: '/user', //主路由
component: User,
children: [ //子路由
{ path: 'profile', component: ProFile },
{ path: 'Posts', component: Posts }
]
},
]
要注意,以 /
开头的嵌套路径会被当作根路径。 这让你充分的使用嵌套组件而无须设置嵌套的路径。
你会发现,children
配置就是像 routes
配置一样的路由配置数组,所以呢,你可以嵌套多层路由。
但这时你会发现,你去查看子路由的网址时依然没有子路由的画面
这是因为你只配置了主路由的出口路由,并没有配置子路由的出口路由
所以你现在还需要再Main,vue里配置子路由的出口路由
我是主路由
本项目采用Container布局
Container布局详解:https://element.eleme.cn/#/zh-CN/component/container
本项目主要由Aside,Header,Main三部分组成
Aside
Header
Main
从上图可以看到,Aside左侧菜单栏主要由一个标题,和几个一级菜单以及二级菜单组成,这里我们可以直接使用element-ui里的NavMenu 导航菜单组件帮助完成
NavMenu 导航菜单详解:https://element.eleme.cn/#/zh-CN/component/menu
那么现在我们有了现成的代码,我们应该把代码写在哪里呢?
按理来说我们已经做好了整体的布局了,我们应该直接把上述代码copy进Aside的部分就可以完成
但在前端有一个很重要的思想叫组件化的思想,组件化的思想包含两种,一种是组件的拆分,一种是组件的封装
对于我们左侧菜单栏这样的一个功能,从页面的角度来看它其实是一个比较单一的功能,对于一个比较单一的功能我们需要把他拆分成一个单独的组件,这样也便于我们以后的修改和维护
所以我们可以在Components下新建一个CommonAside.vue
的文件来
然后再在Main.vue中引入,并且在上面的使用
//使用
Header
。。。
考虑到我们要实现的菜单栏存在一级菜单也存在二级菜单,并且我们后续还要实现不同等级的用户所能使用的功能不同,因此我们在做一二级菜单之前先把,相应的数据进行一个分类,考虑到现实中可能会有很多数据,而一个一个手动分开费时又费力,所以这里我们要使用之前学过的computed计算属性
computed计算属性详解:https://cn.vuejs.org/guide/essentials/computed.html
通过分析数据我们发现,一级菜单和二级菜单的区别是二级菜单有一个children属性又来存他的子菜单,所以我们接可以依靠是否有children属性来判断数据到底是一级还是二级
computed: {
//无子菜单
noChildren() {
return this.menuData.filter(item => !item.children)
},
//有子菜单
hasChildren() {
return this.menuData.filter(item => item.children)
},
},
filter()是过滤函数,传入一个数组,返回一个符合要求的数组
详解:https://blog.csdn.net/dyk11111/article/details/131242109?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170332421416800226548167%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=170332421416800226548167&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-4-131242109-null-null.142v96pc_search_result_base4&utm_term=filter%28%29&spm=1018.2226.3001.4187
区分好数据之后,我们只需要在需要在需要放一级菜单的地方使用v-for循环遍历nochildren数组即可
<el-menu-item v-for="item in noChildren" :index="item.name" :key="item.name">
<i :class="`el-icon-${item.icon}`"></i>
<span slot="title">{{ item.label }}</span>
</el-menu-item>
在这里简单提示几个注意点
①key属性不能忘记,key是需要添加用于标记我们元素的唯一性,所以里面的内容需要一个固定且不变的值
②index的作用与key类似,其功能是用来让组件内部来确定用户点击的到底是哪一项导航栏,因此其属性也必须是唯一的
③在i标签这里存放的是我们一级菜单的icon组件,里面直接使用的是element-ui现成的icon,需要注意的是在class属性里使用到了ES6的模板字符串(双引号内还要套一个``,ESC下面的按键)的语法实现了字符串的拼接
二级菜单与一级菜单类似,只不过二级菜单有两个v-for循环而已
{{ item.label }}
{{ subItem.label }}
需要注意一下第二次循环遍历的是item.children
既然要实现路由跳转,那么首先要先把路由准备好,除了前面我们准备好了的home和user以外,我们还要根据data里的路由数据创建好页面,并注册好路由。
思路:
①现在views文件夹中创建对应的页面
②引入路由
//1.引入路由
import Home from '../views/Home.vue'
import User from '../views/User.vue'
import Main from '../views/Main.vue'
import Mall from '../views/Mall.vue'
import PageOne from '../views/PageOne.vue'
import PageTwo from '../views/PageTwo.vue'
③将组件和路由进行映射
//2.将组件和路由进行映射
const routes = [
{
path: '/',
component: Main,
children: [
{ path: 'home', component: Home }, //首页
{ path: 'user', component: User }, //用户管理
{ path: 'mall', component: Mall }, //商品管理
{ path: 'page1', component: PageOne },
{ path: 'page2', component: PageTwo }
]
},
]
此时就算路由注册完毕
/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
//1.引入路由
import Home from '../views/Home.vue'
import User from '../views/User.vue'
import Main from '../views/Main.vue'
import Mall from '../views/Mall.vue'
import PageOne from '../views/PageOne.vue'
import PageTwo from '../views/PageTwo.vue'
//2.将组件和路由进行映射
const routes = [
{
path: '/',
component: Main,
children: [
{ path: 'home', component: Home }, //首页
{ path: 'user', component: User }, //用户管理
{ path: 'mall', component: Mall }, //商品管理
{ path: 'page1', component: PageOne },
{ path: 'page2', component: PageTwo }
]
},
]
//创建router实例
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})
// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
export default router
现在万事俱备,只差最后实现点击跳转,既然是要点击对应的路由,那么我们肯定要在具体点击的地方绑定一个点击事件,在这里我们给他起名clickMenu()函数,与此同时我们还要向其中传入点击的路由对象
{{ item.label }}
。。。
{{subItem.label}}
clickMenu()
clickMenu(item) {
this.$router.push(item.path)
}
因为我们之前在路由配置文件中实现了创建和挂载实例,因此我们在函数里可以直接获取router
这里需要讲一下router实例方法中的的push
详细参考:https://v3.router.vuejs.org/zh/guide/essentials/navigation.html
这里采用了编程式导航的概念,在 Vue 实例内部,你可以通过 $router
访问路由实例。因此你可以调用 this.$router.push()
括号内填写对应的路由路径
这里还要实现一个小功能,即一进入页面自动跳转到首页的功能,只需要在配置文件中加入redirect后面接上要跳转的路径即可
//2.将组件和路由进行映射
const routes = [
{
path: '/',
component: Main,
redirect: '/home', //重定向到首页
children: [
{ path: 'home', component: Home }, //首页
{ path: 'user', component: User }, //用户管理
{ path: 'mall', component: Mall }, //商品管理
{ path: 'page1', component: PageOne },
{ path: 'page2', component: PageTwo }
]
},
]
但这里还有一个小毛病,就是当你点击两次首页的时候,页面会报错,至于如何解决,留到下一部分在解决~
虽然这个报错不会影响菜单栏的正常使用,但作为一个合格的程序员还是要解决好每一次报错的。
其实之所以会产生这样的情况,是由于router限制了我们不能进行重复的跳转,既然如此,那么我们只需要在跳转的时候进行一个简单的判断,即当前路径与你要跳转的路径是否相同
,不同就跳转,反之不跳。但还要注意一下,在上一节我们为了能一进入页面就直接显示主页,进行了重定向,在这时你如果再点击主页也一样会报错,所以这种单独的情况我们要单独分析
clickMenu(item) {
if (this.$route.path !== item.path
&& !(this.$route.path === '/home' && (item.path === '/'))) {
this.$router.push(item.path)
}
}
注:
this.$route表示当前页面的路由
this.$router表示整个路由实例
Header同样也是一个独立的部分,所以我们也将其独立出来,作为一个单独的组件来使用
简单说一下思路,毕竟基本上还是用element-ui现成的组件来直接使用
只是在布局上使用了flex布局,并且使用的是对齐方式是space-between
CommonHeader.vue
首页
个人中心
退出
vuex详解:https://v3.vuex.vuejs.org/zh/
vuex的工作原理可以去看我之前的一篇笔记:https://blog.csdn.net/Kiwi23333/article/details/135240764
别人的一篇我觉得写的很好的详解:Vuex详解,一文彻底搞懂Vuex
简单分析一下业务逻辑,我们需要点击Header组件上的一个按钮实现左侧菜单栏的缩放,然后Header和Aside是不同的组件,想要通过Header上的按钮去控制Aside的样式,需要我们用到vuex。
这里简单介绍一下什么是Vuex:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
Vuex的安装和引入我们这里简单介绍一下:
首先在src根目录下创建store文件夹,其中存放vuex模块
先创建一个index.js来初始化vuex
/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import tab from './tab.js' //引入模块
Vue.use(Vuex)
//创建Vuex实例
export default new Vuex.Store({
modules: { //Vuex模块化
tab
}
})
初始化好了之后还要去main.js里引入和挂载一下,让全局可以使用$store
main.js
...
//引入store
import store from './store';
...
new Vue({
render: h => h(App),
router,
store, //挂载
}).$mount('#app')
这样Vuex的初始化就算完成了
下面就是完成需求的时候了
首先我们要知道菜单栏之所以能实现缩放,主要靠的是isCollapse,其为布尔值,false不缩放,true缩放
知道这个,那我们只需要再点击按钮的时候实现切换isCollapse取反即可实现菜单栏的缩放功能
说干就干
首先先去仓库store里创建一个tab.js,里面存放isCollapse,和实现isCollapse取反的函数
tab.js
export default {
state: {
isCollapse: false //控制菜单的展开还是收起
},
mutations: {
//修改菜单展开还是收起的方法
collapseMenu(state) {
state.isCollapse = !state.isCollapse
}
}
}
接着去按钮处注册一个点击事件,点击事件实现了提交mutations
CommonHeader.vue
<template>
<div class="header-container">
<div class="l-content">
<el-button
@click="handleMenu" //注册点击事件
icon="el-icon-menu"
size="mini"
></el-button>
<!-- 面包屑 -->
<span class="text">首页</span>
</div>
<div class="r-content">
<el-dropdown>
<span class="el-dropdown-link">
<img class="user" src="../assets/images/user.png" alt="" />
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item>退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
handleMenu() {
this.$store.commit('collapseMenu') //提交mutations
}
},
}
</script>
<style lang="less" scoped>
...
</style>
与此同时,我们的CommonAside组件还需要获取到isCollapse的值,这里要在计算属性中实现
<template>
<div>
<el-menu
。。。
:collapse="isCollapse" //配置缩放
。。。
>
。。。
</template>
<style lang="less" scoped>
。。。
</style>
<script>
export default {
data() {
return {
。。。
};
},
methods: {
。。。
},
computed: {
。。。
isCollapse() { //获取state
return this.$store.state.tab.isCollapse
}
},
}
</script>
这样我们就实现了点击按钮控制左侧菜单栏缩放的功能
我感觉我自己vuex学的不是很好,很多地方讲的不是很清楚,只能勉强让自己明白业务逻辑,具体实现也是依葫芦画瓢。
如上图所示,我们发现我们确实实现了左侧菜单的折叠,但一是很明显这个标题的存在方式并不美观,二是header按理来说应该自动延伸到左侧
第一个问题的解决方式很简单,只需要用一个三目标表达式,判断一下当前导航栏是收起还是正常,正常就显示正常的标题,收起就显示缩写
<h3>{{ isCollapse ? "后台" : "通用后台管理系统" }}</h3>
第二个问题就更简单了,我们点击f12分析一下样式,发现是标签里设置了宽度,把原本定死的宽度改成auto自适应即可
<el-aside width="auto">
观察一下上方的完成版界面,我们会发现在Home组件下,其基本的布局被分为了左和右两个主要板块,并且虽然我这里没有贴图演示,但其实他还能实现不同大小窗口下的组件自适应,同时还有一个鼠标移动到相应组件时会有一个灰色的阴影的细节
具体实现:
对于这个功能,我们就要使用到element-ui中的layout组件
详情参考:https://element.eleme.cn/#/zh-CN/component/layout
主要是通过其中的:span
属性来分配一行里面每个组件间的权重(总共24),我们可以看到右侧的组件相当于是左侧的两倍,那么我们就可以给左侧8的权重,右侧16的权重,这样就会形成一个相对1:2的格局;
同时为了实现鼠标移动到模块上会有阴影的功能,我们要借用element-ui里的card组件
详情参考:https://element.eleme.cn/#/zh-CN/component/card
左侧的上部分主要可以看作是一个上下结构,中间有一个分割线将其上下分开,其中上部分还分为了一个左右结构,可以用flex布局解决,下部分可以**在
中包含一个**实现同一行显示
Admin
超级管理员
最后一次登录时间: 2024-1-13
最后一次登录地点: 江苏扬州
购买统计部分本质上是一个表格,由一定的行和列组成,因此我们可以使用element-ui中的Table表格组件来进行设计
详情参考:https://element.eleme.cn/#/zh-CN/component/table
因为我们这里主要是对数据进行展示,所以我们直接使用一个简单的基础表格即可
右侧上部的订单部分,主要还是对flex布局的考察
具体可以参考:https://blog.csdn.net/weixin_48998573/article/details/131240067?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170522299116800227429755%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=170522299116800227429755&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-131240067-null-null.142v99pc_search_result_base4&utm_term=flex%E5%B8%83%E5%B1%80&spm=1018.2226.3001.4187
¥ {{ item.value }}
{{ item.name }}
④右侧中间和下方图表布局
基本布局如下
引入echarts
<script src="https://cdn.staticfile.org/echarts/4.3.0/echarts.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ECharts</title>
<!-- 引入刚刚下载的 ECharts 文件 -->
<script src="echarts.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var option = {
title: {
text: 'ECharts 入门示例'
},
//提示框
tooltip: {},
//图例
legend: {
data: ['销量']
},
//x轴
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
//y轴
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</body>
</html>
<el-card style="height: 280px">
<!-- 折线图 -->
<div ref="echarts1" style="height: 280px"></div>
</el-card>
<div class="graph">
<el-card>
<!-- 柱状图 -->
<div ref="echarts2" style="height: 260px"></div>
</el-card>
<el-card>
<!-- 饼状图 -->、
<div ref="echarts3" style="height: 220px"></div>
</el-card>
</div>
mounted() {
getData().then(({ data }) => {
const { tableData, userData, videoData } = data.data
this.tableData = tableData
//折线图
// 基于准备好的dom,初始化echarts实例
const echarts1 = echarts.init(this.$refs.echarts1)
// 指定图表的配置项和数据
var echarts1Option = {}
// 处理数据xAxis
const { orderData } = data.data
//es6的key属性
const xAxis = Object.keys(orderData.data[0])
const xAxisData = {
data: xAxis
}
echarts1Option.xAxis = xAxisData
echarts1Option.yAxis = {}
echarts1Option.legend = xAxisData
echarts1Option.series = []
xAxis.forEach(key => {
echarts1Option.series.push({
name: key,
data: orderData.data.map(item => item[key]),
type: 'line'
})
})
console.log(echarts1Option);
// 使用刚指定的配置项和数据显示图表。
echarts1.setOption(echarts1Option)
//柱状图
// 基于准备好的dom,初始化echarts实例
const echarts2 = echarts.init(this.$refs.echarts2)
// 指定图表的配置项和数据
var echarts2Option = {
legend: {
// 图例文字颜色
textStyle: {
color: "#333",
},
},
grid: {
left: "20%",
},
// 提示框
tooltip: {
trigger: "axis",
},
xAxis: {
type: "category", // 类目轴
data: userData.map(item => item.date),
axisLine: {
lineStyle: {
color: "#17b3a3",
},
},
axisLabel: {
interval: 0,
color: "#333",
},
},
yAxis: [
{
type: "value",
axisLine: {
lineStyle: {
color: "#17b3a3",
},
},
},
],
color: ["#2ec7c9", "#b6a2de"],
series: [
{
name: '新增用户',
data: userData.map(item => item.new),
type: 'bar'
},
{
name: '活跃用户',
data: userData.map(item => item.active),
type: 'bar'
}
],
}
echarts2.setOption(echarts2Option)
//饼状图
// 基于准备好的dom,初始化echarts实例
const echarts3 = echarts.init(this.$refs.echarts3)
var echarts3Option = {
tooltip: {
trigger: "item",
},
color: [
"#0f78f4",
"#dd536b",
"#9462e5",
"#a6a6a6",
"#e1bb22",
"#39c362",
"#3ed1cf",
],
series: [
{
data: videoData,
type: 'pie'
}
],
}
echarts3.setOption(echarts3Option)
})
什么是面包屑?:https://blog.csdn.net/m0_72383454/article/details/127466581?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170545601616800180638292%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=170545601616800180638292&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_click~default-1-127466581-null-null.142v99pc_search_result_base4&utm_term=%E9%9D%A2%E5%8C%85%E5%B1%91&spm=1018.2226.3001.4187
首先我们从数据部分和交互部分来分析一下这个功能
①首先从数据层面来看,默认有一个首页
②当我们在其他组件点击的时候,header部分的面包屑会增加
③当我们选择已有的菜单的时候,这个数据不会重复的添加
还记得我们之前专门在CommonHeader.vue中专门存了一个放面包屑的地方吗,在之前我们是将其直接写死的,现在我们就可以对其进行布局
具体的布局我们依然使用element-ui中现成的布局:https://element.eleme.cn/#/zh-CN/component/breadcrumb#breadcrumb-mian-bao-xie
面包屑是在head部分组件里,Tag标签虽然不再head部分组件里,但是它在整个管理后台系统中是会一直存在的,所以需要在Main.vue中。
这两块功能的实现,主要依赖Element-ui两个样式 Breadcrumb 面包屑 + Tag 标签
整个大致逻辑是这样的,首先是面包屑 首页 一定要存在的,接下来 侧边组件 点击某菜单,把这个数据存到vuex中,然后 头部组件 来获取vuex中这个数据并展示。
我们先来看点击,点击要实现点击对应的tag后跳转到对应的页面,同时当删除一个tag之后,要分两种情况,如果删除的不是目前正在高亮的tag,则无事发生,但如果删除的是目前正在高亮的tag,则要把页面跳转到最后一个tag,并将其高亮
首先来设置一下点击事件
<el-tag
v-for="item in tags"
:key="item.path"
:closable="item.name !== 'home'"
:effect="$route.name === item.name ? 'dark' : 'plain'"
//点击事件
@click="ClickMenu(item)"
>
{{ item.label }}
</el-tag>
methods: {
//点击tag跳转的功能
ClickMenu(item) {
//跳转到对应路由的页面
console.log(item);
this.$router.push({ name: item.name })
}
}
接下来来看看删除
删除主要要使用tag组件中的close属性
要注意一下的是,这里注册点击时间的时候不仅仅要传入数据,还要传入当前tag的索引,用于删除跳转到最后一个tag
删除的逻辑相对复杂,我们先梳理一下
删除本质上删除的是store中的数据,所以我们要在mutation中定义一个closeTag()方法,来删除store中指定的数据
为了把数据传递给closeTag函数,我们要使用到辅助函数的方法
//辅助函数传递数据
...mapMutations(['closeTag']),
然后使用findIndex来获得当前数据的索引,再用splice删除该索引的tag
除了删除数据还不够,我们还要实现删除数据后高亮并跳转的功能,具体分三种情况
//删除store中指定的数据
closeTag(state, item) {
console.log(item, 'item');
//tabList中的名字与我们传递进来的数据的名字一样则存储下标
const index = state.tabList.findIndex(val => val.name === item.name)
state.tabList.splice(index, 1)
//删除之后跳转的逻辑
//情况一:如果点击删除的标签与我们当前路由的路径不同的话,此时不做任何操作
if (item.name !== this.$route.name) {
return
}
//情况二:表示删除的是最后一项,往前一个tag跳转
if (index === length) {
this.$router.push({
name: this.tags[index - 1].name
})
} else {
//情况三:删除的是中间的一个tag,往后一个tag跳转
this.$router.push({
name: this.tags[index].name
})
}
}
页面的编写相对简单,只需要调用element-ui中对应的组件即可,代码如下可供参考
<template>
<div class="manage">
<el-dialog title="提示" :visible.sync="dialogVisible" width="50%">
<!-- 用户的表单信息 -->
<el-form :inline="true" ref="form" :model="form" label-width="80px">
<el-form-item label="姓名">
<el-input placeholder="请输入姓名" v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="年龄">
<el-input placeholder="请输入年龄" v-model="form.age"></el-input>
</el-form-item>
<el-form-item label="性别">
<el-select v-model="form.sex" placeholder="请选择性别">
<el-option label="男" value="1"></el-option>
<el-option label="女" value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item label="出生日期">
<el-date-picker
v-model="form.birth"
type="date"
placeholder="选择日期"
>
</el-date-picker>
</el-form-item>
<el-form-item label="地址">
<el-input placeholder="请输入地址" v-model="form.addr"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="dialogVisible = false"
>确 定</el-button
>
</span>
</el-dialog>
<div class="manage-header">
<el-button @click="dialogVisible = true" type="primary">+ 新增</el-button>
</div>
</div>
</template>
<script>
export default {
name: 'AppUser',
data() {
return {
dialogVisible: false,
form: {
name: '',
age: '',
sex: '',
birth: '',
addr: ''
}
}
},
}
</script>
在上一节我们已经完成了form表单的填写,现在我们需要实现获取用户输入的数据,并将其添加在页面里,注意在这里我们要做一个必填的校验,防止用户没有输入信息就点击确认
注意element-ui为我们提供了表单验证的功能
简单来说就是我们需要在之前的代码中,给加上一个rules,再给上加入一个prop
举例:
<el-form
:rules="rules"
:inline="true"
ref="form"
:model="form"
label-width="80px"
>
<el-form-item label="姓名" prop="name">
<el-input placeholder="请输入姓名" v-model="form.name"></el-input>
</el-form-item>
......
</el-form>
data(){
...
rules: {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
],
...
}
}
下面我们要实现点击确认按钮能获得你填入表单的数据
首先给确认按钮绑定一个点击事件submit
事件中首先要判断一下是否通过了表单验证,这里要用到validate
submit() {
this.$refs.form.validate((valid) => {
if (valid) {
//后续对表单数据的处理
console.log(this.form, 'form');
//清空表单数据
this.$refs.form.resetFields()
//关闭弹窗
this.dialogVisible = false
}
})
},
同时我们还要实现在点击取消或者关闭窗口时,保证下次点击新增按钮时,表单中不会有上一次填写的数据,使用的是resetFields()方法
这里要注意一下,这两者可以使用同一个函数,但点击右上角的×关闭窗口要使用中的:before-close属性来实现,而点击取消只需要绑定一个点击事件
<el-dialog
title="提示"
:visible.sync="dialogVisible"
width="50%"
:before-close="handleClose"
>
...
</el-dialog>
...
<el-button @click="handleClose">取 消</el-button>
//弹窗关闭的时候
handleClose() {
console.log(this.$refs.form);
//清空表单数据
this.$refs.form.resetFields()
this.dialogVisible = false
}
由于没有后端,本项目采用mock来模拟后端数据,mock的相关代码在这里不做过多赘述,其主要内容也与后端有关。总之通过一些列引入与调用,最后在User.vue里获取到列表的数据即可
mounted() {
//获取的列表的数据
getUser().then(({ data }) => {
console.log(data);
//把后端的数据传给tableData
this.tableData = data.list
})
},
光获得到列表数据还不够,我们还需要将其在用户页中显示出来才行
回到我们之前没有具体编写的组件里,把里面的label和prop属性里的值和后端数据的值对应上
需要注意的是这里对于性别的处理,后端对于性别的判断是用1来表示男,0来表示女,因此在这里要用到自定义列表的知识,即在中加入,里面做一个三目表达式来判断这个data是0还是1
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column prop="name" label="姓名"> </el-table-column>
<el-table-column prop="sex" label="性别">
<template slot-scope="scope">
<span style="margin-left: 10px">{{
scope.row.sex == 1 ? "男" : "女"
}}</span>
</template>
</el-table-column>
<el-table-column prop="age" label="年龄"> </el-table-column>
<el-table-column prop="birth" label="出生日期"> </el-table-column>
<el-table-column prop="addr" label="地址"> </el-table-column>
</el-table>
首先来完成新增的逻辑,之前我们已经做好了新增的按钮以及新增那些内容的界面,对于新增功能而言只需要做到点击确认调用对应的接口把想要新增的数据显示在界面上即可,
但要注意的是我们还有一个编辑功能,对于编辑功能而言,其操作的页面其实是和新增复用的,只不过区别是点击新增出现的是空白的表单,而点击编辑显示的是对应的数据,所以我们需要通过一个状态来区分用户点击的到底是新增还是提交。在这里我们可以在data里定义一个数据名为modalType,规定当其数值为0时表示新增弹窗,1时为编辑弹窗,然后在确定的点击事件里加入一个对于modalType的判断,如果为0调用新增接口,如果为1调用编辑接口
submit() {
this.$refs.form.validate((valid) => {
if (valid) {
if (this.modalType === 0) {
//调用新增接口
addUser(this.form).then(() => {
//重新获取列表的接口
this.getList()
})
} else {
//调用编辑接口
editUser(this.form).then(() => {
//重新获取列表的接口
this.getList()
})
}
//后续对表单数据的处理
console.log(this.form, 'form');
//清空表单数据
this.$refs.form.resetFields()
//关闭弹窗
this.dialogVisible = false
}
})
新增按钮点击事件
handleAdd() {
this.dialogVisible = true
this.modalType = 0
}
下面来看编辑按钮的点击事件
注意一下这里点击编辑按钮之后,对于的表单的性别一栏显示的是1或0,而不是男和女,之所以会出现这样的情况是因为后台给的数据就是这样的,要想显示对应的男和女要在上面的value属性前加:
<el-form-item label="性别" prop="sex">
<el-select v-model="form.sex" placeholder="请选择性别">
<el-option label="男" :value="1"></el-option>
<el-option label="女" :value="0"></el-option>
</el-select>
</el-form-item>
handleEdit(row) {
this.modalType = 1
this.dialogVisible = true
//注意需要对当前数据进行深拷贝,否则会报错
this.form = JSON.parse(JSON.stringify(row))
},
深拷贝这块我记不太清了,有空要再去看一下
最后我们来看一下删除事件
首先点击删除时会弹出一个窗口询问你是否删除,这里的弹窗效果我们使用element-ui的MessageBox 弹框中的第二个确认消息
具体可看:https://element.eleme.cn/#/zh-CN/component/message-box#dan-du-yin-yong
handleDelete(row) {
this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delUser({ id: row.id }).then(() => {
this.$message({
type: 'success',
message: '删除成功!'
});
//重新获取列表的接口
this.getList()
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
要注意一下调用删除接口的时候传入的是当前这个数据的id,并且要用对象的形式传入
在写分页功能之前,我们要先调整一下用户管理页的样式
这里要用到element-ui的height属性
给整个父组件一个height=“90%”,再给渲染数据的一个height="90%"的属性,就实现了滚动条的功能
下面我们正式看分页功能
分页要用到element-ui中的Pagination组件
详见:https://element.eleme.cn/#/zh-CN/component/pagination#events
主要是要注意以下total属性和page-size属性,page-size表示一页显示多少条数据,total表示总共有多少条数据,所以页数自然就等于total除以page-size
首先去data里定义一个total,初始值设为0,然后去获取列表的getList方法中把后端总共的数据总数赋给total
data(){
return {
...
total: 0 //当前的总数据条数
}
}
getList() {
//获取的列表的数据
getUser().then(({ data }) => {
console.log(data);
this.tableData = data.list
this.total = data.count || 0
})
},
接下里处理一下换页的功能
<!-- 分页功能 -->
<div>
<el-pagination
layout="prev, pager, next"
:total="total"
@current-change="handlePage"
>
</el-pagination>
</div>
//换页功能,val代表选择的具体页码
handlePage(val) {
console.log(val);
this.pageData.page = val
//重新获取列表的接口
this.getList()
},
<!-- form搜索区域 -->
<el-form :model="userForm" :inline="true">
<el-form-item>
<el-input placeholder="请输入名称" v-model="userForm.name"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
</el-form-item>
</el-form>
//列表的搜索
handleSearch() {
this.getList()
},
设置路由什么的在这里就不做过多赘述,只不过要注意一下登录界面的路由和主路由是平级的,不算做主路由的子路由
Login.vue
系统登录
登录
登录权限主要依靠token和cookie来实现
Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
简单来说就是需要一个令牌,来确定当前登录的用户是否有权限,根据不同的权限来显示不同的功能页面,如果没有则任然停留在登录页面
Cookie是用来存储登陆的token的
那么如何实现登录权限的判断,就要用到导航守卫
主要就是通道next方法,如果token正确那就直接next跳转到主页,反之next(false)中断跳转
路由守卫在main.js文件中设置
。。。
//添加前置路由守卫
router.beforeEach((to, from, next) => {//判断token存不存在
const token = Cookie.get('token')
//token不存在,证明当前用户没有登陆,应该跳转至登录页
if (!token && to.name !== 'login') {
next({ name: 'login' })
} else if (token && to.name === 'login') {
//token存在,说明用户已经登陆了,跳转至首页
next({ name: 'home' })
} else {
next()
}
})
。。。
登录跳转事件
//登录
submit() {
//token信息
const token = Mock.Random.guid()
//token信息存入cookie,用于不同页面间的通信
Cookie.set('token', token)
//跳转到首页
this.$router.push('/home')
}
先在mock.js中定义接口
//mock.js定义接口
。。。
import peimission from "./mockServerData/peimission";
//定义mock请求拦截
。。。
//用户列表的数据
。。。
mock.mock(/api\/permission\/getMenu/, 'post', peimission.getMenu)
再在index.js中定义API
//index.js定义API
import http from '../utils/request'
。。。
export const getMenu = (data) => {
return http.post('permission/getMenu', data)
}
最后去Login.vue中来实现功能
主要看submit方法
<template>
<el-form
ref="form"
label-width="70px"
:model="form"
:rules="rules"
:inline="true"
class="login_container"
>
<h3 class="login_title">系统登录</h3>
<el-form-item label="账号:" prop="username">
<el-input v-model="form.username" placeholder="请输入账号"></el-input>
</el-form-item>
<el-form-item label="密码:" prop="password">
<el-input
type="password"
v-model="form.password"
placeholder="请输入密码"
></el-input>
</el-form-item>
<el-form-item>
<el-button @click="submit" type="primary">登录</el-button>
</el-form-item>
</el-form>
</template>
<script>
// import Mock from 'mockjs';
import Cookie from 'js-cookie'
import { getMenu } from '../api'
export default {
name: 'AppLogin',
data() {
return {
form: {
username: '',
password: ''
},
rules: {
username: [
{ required: true, message: '请输入账号', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
]
}
}
},
methods: {
//登录
submit() {
// //token信息
// const token = Mock.Random.guid()
// 校验form表单
this.$refs.form.validate((valid) => {
if (valid) {
getMenu(this.form).then(({ data }) => {
console.log(data);
if (data.code === 20000) {
//token信息存入cookie,用于不同页面间的通信
Cookie.set('token', data.data.token)
//跳转到首页
this.$router.push('/home')
} else {
this.$message.error(data.data.message)
}
})
}
})
}
},
}
</script>
<style lang="less" scoped>
.login_container {
width: 350px;
border: 1px solid #eaeaea;
margin: 180px auto;
padding: 35px 35px 15px 35px;
background-color: #fff;
border-radius: 15px;
box-shadow: 0 0 25px #cac6c6;
box-sizing: border-box;
.el-input {
width: 198px;
}
.login_title {
text-align: center;
color: #505485;
margin: 0px auto 40px auto;
}
.el-button {
margin-left: 105px;
margin-top: 10px;
}
}
</style>
在实现菜单权限管理的时候,我们要考虑以下三个问题:
1.不同的账号登录,会有不同的菜单权限
2.通过url输入地址来显示页面(动态路由)
3.对于菜单的数据在不同页面之间的数据通信
说是话这块没太看懂,等后面面试准备项目的时候重看一边吧,到时候再把笔记补上