Vue全家桶 - 电商后台管理系统项目开发实录(详)
目录
- 项目概述
- 1.1 电商项目基本业务概述
- 1.2 电商后台管理系统的功能
- 1.3 电商后台管理系统的开发模式(前、后端分离)
- 项目初始化
- 2.1 前端项目初始化步骤
- 码云相关操作
- 2.2 后台项目的环境安装配置
- 登录 / 退出 功能
- 3.1 登录概述
- 3.2 登录 - token 原理分析
- 3.3 实现登录功能
- 3.4 实现退出功能
- 处理 ESLint 警告
- 主页布局
- 4.1 后台首页基本布局
- 4.2 顶部布局,侧边栏布局
- 4.2.1. 顶部布局
- 4.2.2 侧边栏菜单布局
- 4.3 通过接口获取菜单数据
- 4.4 动态渲染菜单数据并进行路由控制
- 4.5 设置激活子菜单样式
- 4.6 制作侧边菜单栏的伸缩(展开/折叠)功能
- 4.7 在后台首页添加子级路由
- 4.7.1 主页 - 实现首页路由的重定向
- 用户管理
- 5.1 用户管理概述
- 5.2 用户管理-列表展示
- 5.2.1 用户列表布局
- 5.2.2 用户状态列和操作列处理
- 5.2.3 表格数据填充
- 5.2.4 表格数据分页
- 5.2.5 搜索功能
- 5.3 用户管理-用户状态控制
- 5.4 用户管理-添加用户
- 5.4.1 添加用户表单弹窗布局
- 5.4.2 表单验证
- 5.4.3 表单提交
- 5.5 用户管理-编辑用户
- 5.5.1 根据 ID 查询用户信息
- 5.5.2 编辑提交表单
- 5.6 用户管理-删除用户
- 6 权限管理
- 6.1 权限管理业务分析
- 6.2 添加权限列表路由
- 6.3 添加面包屑导航
- 6.2 权限列表展示
- 6.3 角色列表展示
- 6.4 用户角色分配
- 6.4.1 展示角色对话框
- 6.4.2 完成角色分配功能
- 6.5 角色权限分配
- ~ 未完待续 ~
项目效果展示:
1. 项目概述
1.1 电商项目基本业务概述
一般情况下客户使用的业务服务包括:PC端,小程序,移动web,移动app。
管理员使用的业务服务:PC后台管理端;
PC后台管理端的功能
管理用户账号(登录,退出,用户管理,权限管理),商品管理(商品分类,分类参数,商品信息,订单),数据统计;
开发模式
本项目(电商后台管理系统)采用 前、后端分离 的开发模式。
前端项目是基于 Vue 的 SPA(单页应用程序)项目;
本项目技术选型
前端项目技术栈()
Vue
;Vue-Router
;Element-UI
;Axios
;Echarts
* **后端项目技术栈**(做一般了解)
* `Node.js`;
* `Express`;
* `Jwt`(模拟`session`);
* `Mysql`;
* `Sequelize`(操作数据库的框架)
1.2 电商后台管理系统的功能
电商后台管理系统用于管理用户账号、商品分类、商品信息、订单、数据统计等业务功能。
1.3 电商后台管理系统的开发模式(前、后端分离)
电商后台管理系统整体采用前后端分离的开发模式,其中前端项目是 基于 Vue 技术栈的 SPA 项目 。
什么是 前后端分离 的开发模式?
- 前端:主要 负责绘制页面,同时,基于 Ajax 技术,调用后端提供的 API 接口;
- 后端:主要负责 操作数据库,并且向前端 暴露 API 接口。
前、后端分离的开发模式,是目前 主流 的 开发模式 。
优点: 开发效率高、项目易于维护。
2. 项目初始化
2.1 前端项目初始化步骤
- 安装 Vue 脚手架(如在此之前本机已 全局 安装过,则可略过)
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70_pic_center 3]
安装 Vue CLI 交互式项目脚手架(一个基于 Vue.js 进行快速开发的完整系统)。
1)cmd 中执行以下命令安装 **Vue CLI** 包:
npm install -g @vue/cli
2)安装完成后,检查版本是否正确:
vue --version
> Vue CLI 的包名称由 vue-cli 改成了 @vue/cli。 如果你已经全局安装了旧版本的 vue-cli (1.x 或 2.x),你需要先通过 `npm uninstall vue-cli -g` 或 `yarn global remove vue-cli` 卸载它。
> Node 版本要求:
> Vue CLI 4.x 需要 Node.js v8.9 或更高版本 (推荐 v10 以上)。你可以使用 n,nvm 或 nvm-windows 在同一台电脑中管理多个 Node 版本。
更多内容请参见 Vue CLI 官方文档:[https://cli.vuejs.org/zh/guide/][https_cli.vuejs.org_zh_guide]
通过 Vue 脚手架创建项目(本项目开发采用 “ 可视化面板 ” 创建)
1)cmd 终端中输入 :
vue ui
运行完成后,自动打开如下页面:
2)点击 “+ 创建 ” 进入到目录选择面板,选择目录后。再点击 “+ 在此创建新目录 ”按钮。填写项目信息:
3)进入“ 预设”面板,并按如下图示勾选:
4)点击下一步,进入“ **功能** ”选择面板:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 3]
上图中,勾选了 “ 使用配置文件 ” 后,就会将不同的配置 单独 地存放为一个配置文件。点击 **下一步**,打开 配置 面板。
5)完成如下操作:![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 4]
6)单击 下一步。提示是否保存新预设(方便下次直接选择该配置 / 也可不保存)。
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 5]
点击 “**保存预设并创建项目**” 按钮后,系统自动创建项目如下:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 6]
配置 Vue 路由
在上面步骤中已自动配置。
配置 Element-UI 组件库:在插件中安装,搜索
vue-cli-plugin-element
1)打开“仪表盘”,单击左侧 “ 插件 ”按钮,再单击右上角的 “ + 添加插件 ”
2)搜索插件 `vue-cli-plugin-element` 并安装。
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 8]
3)跳转到 “ **配置插件** ”面板,操作如下:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 9]
配置 axios 库:在依赖中安装,搜索
axios
(运行依赖)1)点击 左侧边栏 “ 依赖 ” 按钮,再点击右上角 “ 安装依赖 ”
2)安装依赖
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 11]
- 初始化 git 远程仓库
- 将本地项目托管到 Github 或 码云中(方便团队成员协作开发)
码云相关操作
- 注册登录码云账号,注册地址:https://gitee.com/signup
安装 git
在Windows上使用 Git,可以从Git 官网直接下载安装程序进行安装。
测试:
git --version
(终端中打印出版本号 即为安装成功 )
- 点击网站右上角“登录”,登录码云,并进行账号设置
下一步:
下一步:
在本地创建公钥(终端中运行如下命令)
ssh-keygen -t rsa -C "xxx@xxx.com"
注 :上述命令(示例)中的 “ xxx@xxx.com” 字样,请务必替换为自己注册 gitee 时的真实邮箱后,再回车执行!
然后回车,接着连敲 3 次回车(中间不需任何操作)即可生成公钥。如图:
找到公钥地址
Your identification has been saved in /c/Users/My/.ssh/id_rsa;
Your public key has been saved in /c/Users/My/.ssh/ id_rsa.pub。当创建公钥完毕后,请注意打印出来的信息“
Your public key has been saved in
”/c/Users/My/.ssh/id_rsa.pub : c盘下面的Users下面的My下面的.ssh下面的 id_rsa.pub 就是创建好的 公钥 了。
用 记事本 或其它编辑器打开
id_rsa.pub
文件,复制文件中的所有代码:
再点击码云中的 SSH 公钥按钮,将复制好的的公钥粘贴到公钥文本框中。
点击”确定“按钮,再根据提示输入验证密码后,即完成ssh公钥添加:
测试公钥是否添加成功
在完成 gitee 设置中添加公钥后,再 cmd 终端中输入:
ssh -T git@gitee.com
过程中出现如下图所示的询问(是否继续连接?)时,输入 “yes” 回车继续,直到命令执行完成。
首次使用需要确认并添加主机到本机SSH可信列表。若返回 Hi XXX! You’ve successfully authenticated, but Gitee.com does not provide shell access. 内容,则证明添加成功。如下图所示:
再次运行命令ssh -T git@gitee.com
,同样也可看到如下所示信息:
将本地代码托管到码云中
点击码云右上角的
+
号 -> 新建仓库进行
git
配置注:执行如下命令前,必须确保本机已安装过 git,否则终端中会 报错 。
10 **项目首次提交**
1)**检查状态**:项目根目录下输入以下命令:
git status
运行结果显示项目中存在未跟踪的文件没有被提交 ,如下所示:![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 21]
此时需要做一下处理,即把所有的文件都添加到 暂存区。
2)**添加到 暂存区**:运行如下命令:
git add .
注意:命令中的 `add` 和后面的`.`(小圆点)中间有个空格,否则会报错。
3)**本地提交**:将暂存区中的文件提交至 本地仓库中 :
git commit -m "add files"
再次检查状态:
git status
运行结果如下:
![在这里插入图片描述][20201218222504593.png]
提示当前 “ 处于主分支,工作目录是干净的 ”,即没有要提交的文件。
但当前的这些操作只是在本地操作仓库,仓库还没上传到码云中,
4)将本地仓库与远程 `git`仓库 关联
找到并在终端中运行(你新建的码云仓库 vue\_shop 页面最底部提供的那两句)代码,如下所示:
git remote add origin https://gitee.com/XXXXX/vue_shop.git
git push -u origin master
**注:** XXXX 为你的码云 **帐户名称** ( 非邮箱名称)。如果运行 报错,请点击 这里 查看解决办法。
执行第二句命令时,会弹出如下安全验证,输入用户名和密码确认后,等待完成提交即可。
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 22]
说明:如果是第一次向码云中提交代码,会弹出码云的帐号和密码输入窗口(以后不会再出现)
**5)检查是否上传(远程仓库)成功**
在远程仓库中点击 “刷新”,即可看到提交信息
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 23]
类似这样,表示本地仓库已成功上传到了码云中。
--------------------
\*\*关于 报错\*\*,如果执行上述命令时终端中报错如:
fatal: not a git repository (or any of the parent directories): .git
【**解决办法**】
—— 请检查项目根目录下,是否存在一个隐藏的 `.git` 文件夹,如下所示:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 24]
如果不存在,则在此项目根目录下,打开终端运行如下命令(初始化本地仓库):
git init
已了解,点击 返回。
2.2 后台项目的环境安装配置
2.2.1 安装 MySQL 数据库
用到的素材,点击 网盘下载 提取码:8up0
① 安装素材中提供的 phpStudy ,傻瓜式安装。
② 将素材中的压缩包解压,记住解压路径。
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 25]
③ 运行phpStudy,单击“ MySQL管理器 ” 按钮,选择 MySQL导入导出 菜单项。
④ 按上图所示,找到对应路径下已解压得到的 db 文件夹中的 `mydb.sql` 数据库脚本文件,点击 “ 导入” 按钮,自动弹出黑色的命令行窗口,开始还原数据库(此时间稍长,请耐心等待~);
--------------------
***温馨提示!** 还原结束时,黑色的命令行窗口会自动关闭,此时可按如下所示查看生成的数据库。*
![在这里插入图片描述][20201219005059565.png]
如在数据库目录下能够看到如下图所示的路径、文件,表示 *数据库还原成功!*![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 26]
--------------------
**注:** 由于开发过程中不需要用到 Apache,可将其 “ 停止 ” 服务,如下图所示:![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 27]
--------------------
2.2.2 配置后台项目
在前面已经解压出来的 vue_api_server,就是后台 API 项目 。但需要先 安装依赖包 才能正常运行。
**A**. 安装 nodeJS 环境,配置后台项目
**B. 安装项目依赖包**
进入 vue\_api\_server 目录中,shift+右键 在弹出的菜单中选择 “在此处打开 Powershell 窗口 ” 打开终端,输入命令安装项目依赖包:
npm install
**C.启动项目**
继续在终端中输入如下命令,启动项目:
node .\app.js
**注意**:启动前,必须先将 phpStudy 的 MySQL 服务开启。
**D**. 使用 postman 测试 API 接口是否正常。
安装 postman 软件([点此下载][Link 2]),启动 PostMan 填写相关参数(首次使用该软件需进行简单注册),如下所示:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70_pic_center 7]
--------------------
注意:输入登录请求地址、用户字段名、密码字段名时,请务必与 API 文档保持一致;
--------------------
点击 “ **Send**” 后,服务端返回如下信息:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 28]
电商管理后台 API 接口文档 下载:[https://pan.baidu.com/s/1OGxh05B0BocQm9cP3BWO7w][https_pan.baidu.com_s_1OGxh05B0BocQm9cP3BWO7w] 提取码:
3. 登录 / 退出 功能
3.1 登录概述
登录业务流程
- 在登录页面输入用户名和密码
- 调用后台接口进行验证
- 通过验证之后,根据后台的响应状态跳转到项目主页
登录业务的相关技术点
http
是无状态的;- 通过
cookie
在客户端记录状态; - 通过
session
在服务器端记录状态; - 通过
token
方式维持状态(推荐垮域 时采用)
3.2 登录 - token 原理分析
3.3 实现登录功能
一、登录逻辑:
在登录页面输入账号和密码进行登录,将数据发送给服务器 ==> 服务器返回登录的结果,登录成功则返回数据中带有token ==> 客户端得到 token 并进行保存,后续的请求都需要将此 token 发送给服务器,服务器会验证 token 以保证用户身份。
二、登录状态保持:
1)如果服务器和客户端 同源 1,建议可以使用
cookie
或者session
来保持登录状态;2)如果客户端和服务器 跨域 2,建议使用
token
进行维持登录状态。举例如下:
http://www.123.com/index.html 调用 http://www.123.com/abc.do ( 非跨域 )
http://www.123.com/index.html 调用 http://www.456.com/abc.do ( 主域名不同:123/456,跨域 )
http://abc.123.com/index.html 调用 http://def.123.com/server.do ( 子域名不同:abc/def,跨域 )
http://www.123.com:8080/index.html 调用 http://www.123.com:8081/server.do( 端口不同:8080/8081,跨域 )
http://www.123.com/index.html 调用 https://www.123.com/server.do ( 协议不同:http/https,跨域 )
因合作方域名与我方域名不同,当从合作方加载页面调用我方接口时,会出现跨域的报错。
- 三、添加新分支 login,在 login分支 中开发当前项目 vue_shop:
1)项目根目录中打开 vue\_shop 终端(shift + 右键 通过 vs code打开),使用`git status`命令确定当前项目状态(是否干净)。
git status
运行结果如下所示:
![在这里插入图片描述][20201220213536234.png]
表明当前工作区是干净的,可以进行登录页面的绘制。
—— 此时需要创建一个新分支。
2)确定当前工作目录是干净的之后,创建一个新分支并切换到该分支进行开发,开发完毕之后将其合并到 master
git checkout -b login
> **注:** 在开发中,只要进行一个新功能开发的时候,尽量把它放到一个新分支上,当这个功能开发完毕后,再把它合并到主分支 master 上。
3)然后`git branch`命令查看新创建的分支,确定我们正在使用 login分支 进行开发。
![在这里插入图片描述][2020121810225882.jpg_pic]
**绿色** 表示当前所处的分支。
4)接着,执行`vue ui`命令打开 **ui** 界面,然后运行 serve,运行 app 查看当前项目效果。
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70_pic_center 9]
--------------------
**四、登录页面的布局**
点击 “ 启动App ” ,打开项目:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70_pic_center 10]
此时,我们可以看到现在它只是一个默认页面,需要把它重置为空白页面:
1)打开项目的 src 目录,点击查看`main.js`文件(这是整个项目的 **入口文件**):
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import './plugins/element.js'
Vue.config.productionTip = false
new Vue({
// 把 router 路由挂载到实例
router,
// 通过 render 函数,把 App 根组件渲染到页面上
render: h => h(App)
}).$mount('#app')
2)再打开 App.vue (根组件),将根组件的内容进行清理(template 中只留下根节点,script 中留下默认导出,去掉组件,style 中去掉所有样式),清理完成后如下所示:
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script> export default { name: 'app' } </script>
<style> </style>
3)再打开路由文件夹下的 index.js(有些版本的 Vue ui 所创建的工程项目,其 router 文件夹下的路由文件名为`router.js`),将`routes`数组中默认的路由规则全部清除,然后将`views`删除:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
]
})
**五、新建 Login.vue 组件**
1)将文件夹`components`中的`helloworld.vue`删除,并新建 Login.vue 单文件组件,添加 template、script、style 标签,style 标签中的 scoped 可以防止组件之间的样式冲突(没有`scoped`则样式是全局的)。
<template>
<div class="login_container">
</div>
</template>
<script>
export default {
}
</script>
<style lang="less" scoped>
.login_container {
background-color: #2b4b6b;
height: 100%;
}
</style>
`scoped`:是 vue 指令,用来控制组件生效的范围(表示只在当前组件内生效,只要是单文件组件,都应加上)
--------------------
**注**:当添加背景样式并保存代码修改后,浏览器会报错 “ 找不到 `less-loader`”,如下图所示:
![在这里插入图片描述][20201228222209783.png]
这是由于 Vue 的 cli 工具创建的项目默认并没有安装 less 相关的 loader,如果要使用 less 语法(如`<style lang="less" scoped>`),此时则需要配置 less加载器(开发依赖),安装 less (开发依赖)。。
因此,接下来打开可视化面板安装依赖 less-loader 和 less :
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 29]
如上图所示,搜索并安装好 `less-loader`以后,此时浏览器仍然报错,如下所示: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 30]
这时,回到安装依赖项界面,再次搜索 less 并安装好(因为 less-loader 依赖于 less)。
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 31]
--------------------
此时刷新网页并不能生效(仍然是报错状态),接下来,先关闭网页,**停止** server,再次点击 **运行**即可。
2)在路由文件 index.js 中导入`Login.vue` 组件并设置规则;
const router = new Router({
routes: [
// 当用户访问 /login 这个地址时,通过 component 属性指定要展示的组件 Loging
{ path: '/login', component: Login }
]
})
3)在 App.vue 中添加路由占位符:
<template>
<div id="app">
<!-- 路由点位符 -->
<router-view></router-view>
</div>
</template>
4)由于当前默认访问的是 `localhost:8080/#/` (即根路径),需要在其后手动添加 `/login`才能访问登录组件。
因此为了实现“只要用户访问了`/`(斜线)根路径,就自动重定向到 `/login` 地 址”,这里就必须添加一个重定向路由规则。即在路由文件 index.js 文件中添加一句 `{ path: '/', redirect: '/login' }`,如下所示:
const router = new Router({
routes: [
// 重定向路由
{ path: '/', redirect: '/login' },
// 当用户访问 /login 这个地址时,通过 component 属性指定要展示的组件 Loging
{ path: '/login', component: Login }
]
})
5)然后需要添加公共样式,在 assets 文件夹下面添加 css文件夹,创建`global.css`文件,添加全局样式。
/* 全局样式表 */
html,body,#app{
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
5)在入口文件 main.js 中导入 global.css,使得全局样式生效
import "./assets/css/global.css"
6)然后,将 Login.vue 中的根元素也设置为撑满全屏(`height:100%`)
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 32]
7)在 `Login.vue` 中绘制登录框
<div class="login_box"></div>
添加样式:
.login_box {
width: 450px;
height: 300px;
background-color: #fff;
border-radius: 3px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%); /* 位移,x轴 y轴*/
}
> transform 属性,定义 2D 或 3D 转换,进行 旋转、缩放、移动、倾斜。其中,取值为`translate(x,y)` 是 2D 转换( CSS3 `transform` 属性的更多详情点击 [这里][Link 3] )。
添加样式后效果如下:![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 33]
8)绘制顶部的默认头像盒子 `avatar_box`
<div class="login_box">
<div class="avatar_box">
<img src="../assets/logo.png" alt="">
</div>
</div>
写`avatar_box` 的 css 样式(嵌套到 `login_box`的样式内):
.avatar_box {
height: 130px;
width: 130px;
border: 1px solid #eee;
border-radius: 50%;
padding: 10px;
box-shadow: 0 0 10px #ddd; /* 添加盒子阴影 */
position: absolute;
left:50%;
transform: translate(-50%,-50%);
background-color: #fff; img {
width: 100%;
height: 100%;
border-radius:50%;
background-color: #eee;
}
}
> `box-shadow` 属性向盒子添加一个或多个阴影,该属性是由逗号分隔的阴影列表,每个阴影由 2-4 个长度值、可选的颜色值以及可选的 inset 关键词来规定。省略长度的值是 0( 更多详情点击 [这里][Link 4] )。
**效果如下:**
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 34]
9)**登录页面的布局**
通过 Element-UI 实现 页面布局:
* `el-form`
* `el-form-item`
* `el-input`
* `el-button`
* `字体图标`
打开官网 [element-cn.eleme.io/\#/zh-CN][element-cn.eleme.io_zh-CN] 找到组件:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 35]
--------------------
点击顶部导航的 “ 组件 ”,侧边栏中选择 Form 表单,再点击 “ 显示代码 ”:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 36]
--------------------
![在这里插入图片描述][20201230202054722.png]
复制一个 item 项的代码(如下所示),粘贴到 Login 组件中并将不需要用到属性绑定删除、添加结束标签后如下所示:
<el-form label-width="80px">
<el-form-item label="活动名称">
<el-input ></el-input>
</el-form-item>
</el-form>
保存后查看页面,发现控制台报错访问不到这几个元素:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 37]
这是因为 element-UI 是通过按需导入来使用的,必须先导入才能正常使用。
此时,打开 plugins 文件下的 element.js 文件,导入需要的组件(如果分几次导入可能会报错):
import { Button, Form, FormItem, Input } from 'element-ui'
把 Form 组件 input 输入框代码里不需要的文本 label="活动名称"去掉,再把占位的 `label-width="80px"`重置为 0,并复制 2 组 `el-form-item` 元素结构代码(增加一个密码输入框和一个按钮区) :
<!-- 登录表单区 -->
<el-form label-width="0">
<!-- 用户名 -->
<el-form-item>
<el-input ></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item>
<el-input ></el-input>
</el-form-item>
<!-- 按钮区 -->
<el-form-item>
</el-form-item>
</el-form>
保存后页面的效果如下: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 38]
在 Element [官网][Link 5] 找到 button 组件,复制 button 按钮的代码:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 39]
将复制的代码粘贴到按钮区,并给它添加一个`btns`的类名,以便设置样式,代码如下:
<!-- 登录表单区 -->
<el-form label-width="0">
<!-- 用户名 -->
<el-form-item>
<el-input ></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item>
<el-input ></el-input>
</el-form-item>
<!-- 按钮区 -->
<el-form-item class="btns">
<el-button type="primary">登录</el-button>
<el-button type="info">重置</el-button>
</el-form-item>
由于 `<el-button>` 按钮默认是靠左对齐,实际需要它 靠右对齐,在 `Login.vue` 的 `<style>` 标签内部写上css 样式:
.btns {
display: flex; /* 弹性布局 */
justify-content: flex-end; /* 横轴 项目位于容器的尾部*/
}
> `Flex` 弹性布局,可以简便、完整、响应式地实现各种页面布局:通过给父盒子添加`flex`属性,来控制子盒子的位置和排列方式,点击 >> [查看详情][Link 6]
> `justify-content` 用于设置或检索弹性盒子元素在主轴(横轴)方向上的对齐方式,访问 W3Cschool >>[查看详情][Link 7]
> 注: 当将父盒子设为 flex 布局后,子元素的 float、clear 、 vertical-align 属性将失效。
此时,页面效果如下: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 40]
接下来,将整个 Form 表单区域底部对齐,这需要给 `<el-form>`标签添加一个类名`login_form`,并添加样式:
.login_form {
position: absolute;
bottom: 0;
width:100%;
padding: 0 20px;
}
效果如图:![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 41]
`padding: 0 20px` 后,撑大了盒子,这是因为`form`表单的`boxsizing` 属性值默认为 `content-box`(即传统盒模型),需将其设置为 border-box(即css3盒模型) :
.login_form {
position: absolute;
bottom: 0;
width:100%;
padding: 0 20px;
box-sizing: border-box; /* C3盒模型 */
}
> box-sizing 属性定义了如何计算一个元素的 总宽度 和 总高度。只要在CSS中加上“box-sizing: border-box;”这句,那么就将一个普通的盒子变成CSS3盒模型,padding 和 border就不会再撑大盒子。详情点击 >> [这里][Link 6] 。
添加后,效果如下所示:![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 42]
接下来,绘制用户名和密码输入框前面的小图标。
在 [Element UI 官网][Link 5] 找到 input 组件 菜单项,再找到对应的样式:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 43]
--------------------
将复制的代码粘贴到项目文件 login.vue 中:
<!-- 用户名 -->
<el-form-item>
<el-input prefix-icon="el-icon-search"></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item>
<el-input prefix-icon="el-icon-search"></el-input>
</el-form-item>
生效后效果如下:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 44]
再从 [Element UI 官网][Link 5] 菜单中,点击侧边栏 icon 图标 菜单项,查找是否有对应的用户和密码图标:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 45]
--------------------
由于在这里我们没有找到所需图标,因此要用到第三方图标库。
这里我们用 [*阿里图标库*][Link 8] ,将所需字体图标选定后,下载到本地,并在该字体压缩文件解压后,将所得文件夹命名为 fonts ,放到 src \\ assets \\ 目录下。
接着,在入口文件 main.js 中,导入字体图标的 css 样式表:
// 导入字体图标
import './assets/fonts/iconfont.css'
在浏览器中打开 fonts 文件夹下的 demo\_index.html HTML文件,查看使用示例,并将图标分别放置到 `Loging.vue` 组件文件相应的用户名、密码`input`标签内,替换原来的放大镜图标:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 46]
![在这里插入图片描述][20201231201917960.png]
代码如下:
(注: `iconfont` 是基础类,不能缺少。`icon-xx`x 是图标名称)
<!-- 用户名 -->
<el-form-item>
<el-input prefix-icon="iconfont icon-user"></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item>
<el-input prefix-icon="iconfont icon-3702mima"></el-input>
</el-form-item>
替换原图标类名并保存,此时登录框 UI 效果如下:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 47]
10)**登录表单的数据绑定**(把用户名和密码对应的值自动绑定到数据源上):
打开 [Element UI][Link 5] 官网,找到 Form 表单的定义,在 典型表单 中展开代码结构,能看到第一行 `<el-form>` 上,有个属性绑定 `:model`就代表数据绑定:
--------------------
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 48]
--------------------
即,示例中表示的是 `<el-form>` 表单中填写的所有数据,都会自动同步到 `:model`指向的 "`form`"对象上,form 是个数据对象,其定义如下所示:
--------------------
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 49]
--------------------
**归纳:表单添加数据绑定的步骤如下:**
1)第一步:先给 `<el-form>` 添加 :model 属性进行数据绑定,指向一个数据对象;
2)第二步:为每个表单项里的文本输入框,通过`v-model`绑定到数据对象上对应的属性中。
***实现如下**:*
1)打开 Login.vue 文件,通过`:model` 绑定一个新命名的数据对象 `loginForm` ,代码如下:
<!-- 登录表单区 -->
<el-form :model="loginForm" label-width="0" class="login_form">
<!-- 省略不相关代码-->
</el-form>
2)接下来,在 script 标签所在的行为区域中,定义 loginForm 这个数据对象:
<script>
export default {
data () {
return {
// 登录表单的数据绑定对象
loginForm: {
username: 'admin',
password: '123456'
}
}
}
}
</script>
3)通过`v-model` 将 loginForm 里的对象 `username` 和 `password` ,分别双向绑定到用户名和密码输入框上,代码如下:
<!-- 登录表单区 -->
<el-form :model="loginForm" label-width="0" class="login_form">
<!-- 用户名 -->
<el-form-item>
<el-input v-model="loginForm.username" prefix-icon="iconfont icon-user"></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item>
<el-input v-model="loginForm.password" prefix-icon="iconfont icon-3702mima"></el-input>
</el-form-item>
<!-- 此处省略按钮区代码-->
<el-form>
此时,实现效果如下图:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 50]
再为密码输入框添加一个`type`属性,属性值设为 `password`,以使密码以 `*` 号显示,保证用户账户的安全性。
<el-input type="password" v-model="loginForm.password" prefix-icon="iconfont icon-3702mima"></el-input>
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 51]
11)**实现表单的数据验证**
**目标**:当填写完用户名和密码,只要鼠标一离开文本框,就会立即对填写的数据进行合法性的校验。要如何添加数据验证行为呢?
**方法**:打开 [Element UI ][Link 5] 官网, Form表单,找到该组件页面后面的表单验证:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 52]
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 53]
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 54]
**分析:** 如上图所示展开的结构代码,解读如下,
首先,要为`<el-form>`组件,进行属性绑定,绑定`rules` 属性,其属性值是一个表单验证规则对象;
其次,该验证规则对象要在`<script>` 标签行为区域中的 data 数据里进行定义,其中,每一个属性都是一个验证规则。
最后,为不同的表单 item 项,通过 `prop` 属性来指定不同的验证规则,来进行表单的验证。
**具体实现:**
1)打开登录组件 login.vue ,在 `<el-form>`元素上通过`:rules` 绑定 `loginFormRules`这个新的验证规则对象:
<!-- 登录表单区 -->
<el-form :model="loginForm" :rules="loginFormRules" label-width="0" class="login_form">
2)复制 `loginFormRules` 对象名称,到 login.vue文件的 script 行为区域中的 data 中进行定义:
// 表单的验证规则对象
loginFormRules: {
// 验证用户名是否合法
username: [],
// 验证密码是否合法
password: []
}
验证规则可直接到 [Element UI][Link 5] 官网中进行复制: ![在这里插入图片描述][20201231223740361.png]
--------------------
将复制过来的代码里的提示文字做适当修改,整理后代码如下:
// 表单的验证规则对象
loginFormRules: {
// 验证用户名是否合法
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 10, message: '用户名要求长度在 3 到 10 个字符', trigger: 'blur' }
],
// 验证密码是否合法
password: [
{ required: true, message: '请输入登录密码', trigger: 'blur' },
{ min: 6, max: 15, message: '密码要求长度在 6 到 15 个字符', trigger: 'blur' }
]
}
3)将定义的规则应用到表单中
分别在用户名和密码输入框的 `<el-form-item>`元素标签中,添加 prop 属性,并指向`username` 和 `password`:
<!-- 用户名 -->
<el-form-item prop="username">
<el-input v-model="loginForm.username" prefix-icon="iconfont icon-user"></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item prop="password">
<el-input v-model="loginForm.password" prefix-icon="iconfont icon-3702mima" type="password"></el-input>
</el-form-item>
此时完成效果如下图:![在这里插入图片描述][20201231224726729.gif_pic_center]
--------------------
**12)实现表单的重置功能**
**目标:** 当点击 “ 重置 ”按钮时,能够重置校验结果
如何实现 ? 查看 [Element UI][Link 5] 官方文档,在`Form`组件对应的页面中,底部提供了一个方法:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 55]
只要我们获取到了表单的实例对象,就可通过实例对象直接访问(调用) resetFields 函数。从而重置整个表单,将所有字段值重置为初始值并移除校验结果。
如何拿到表单的实例对象 ?
**分析:**
① 要拿到 `<el-form>`组件的实例对象,需要给它添加一个`ref` 引用,引用名称我们定义为 loginFormRef;
② 接下来,只要能够获取到 loginFormRef,就能拿到 `el-form` 表单的实例对象,即 loginFormRef 就是表单的实例对象,可以直接通过它来调用 resetFields 函数,以重置表单。
**实现过程**:
① 为组件添加 `ref` 引用:
<!-- 登录表单区 -->
<el-form ref="loginFormRef" :model="loginForm" :rules="loginFormRules" label-width="0" class="login_form">
② 通过`@click`为 **重置** 按钮绑定 **单击事件** resetLoginForm :
<!-- 按钮区 -->
<el-form-item class="btns">
<el-button type="primary">登录</el-button>
<el-button type="info" @click="resetLoginForm">重置</el-button>
</el-form-item>
③ 在 script 的`export default` 中定义\* resetLoginForm 方法,先打印出 `this` ,查看其具体指向。
methods: {
// 点击重置按钮,重置登录表单
resetLoginForm () {
console.log(this)
}
}
从打印结果可以看到,`this` 指向的是一个组件的实例对象 `VueComponent{...}`,展开该对象下,可见一个数据对象 `$refs: {loginFormRef: VueComponent}`,其中的一个属性 `loginFormRef` 就是前面为 el-form 定义的 ref 引用名称,如下图所示:![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 56]
由此可知,通过 `this.$refs` 可以直接获取到 resetLoginForm 这个引用对象,该引用对象就是 el-form 的实例。代码及效果如下:
methods: {
// 点击重置按钮,重置登录表单
resetLoginForm () {
// console.log(this)
this.$refs.loginFormRef.resetFields()
}
}
![在这里插入图片描述][20210101002126923.gif_pic_center]
这样,就实现了当点击 “ 重置 ”按钮时,重置校验结果的功能。而文本框好像没有被清空是什么原因呢 ?其实这是一种错觉。那是因为文本框是双向绑定到 `data` 中的数据中,而该数据是设置了默认值的,重置后,就又把默认值写进了已清空的文本框。
--------------------
13)**登录前的预校验**
当我们点击登录按钮时,不应该直接发起数据请求,而是在请求之前先对表单数据进行预验证,验证通过时,才允许发起网络请求。否则,应该直接提示用户表单数据 不合法 。
**实现思路:** 当点击登录时,通过调用表单的某个函数来验证。具体调用哪个函数呢 ? 同样是在 [Element UI][Link 5] 官网中,找到 组件 => Form表单 => Form Methods 节点的 `validate`函数: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 57]
Function(callback: Function(boolean, object))
该函数接收一个 `callback` 回调函数,回调函数的第 1 个行参是布尔值代表校验的结果。如果校验通过,则返回 `true`;反之,则返回`false`。
如何调用 `validate` 函数 ?
**调用方法:**
* ① 通过`@click` 给 登录 按钮绑定单击事件(事件处理函数命名为 `login`);
<el-button type="primary" @click="login">登录</el-button>
* ② 通过 `ref` 获取到表单的引用对象,再用该引用对象调用 `validate` 函数:
login () {
this.$refs.loginFormRef.validate(valid => {
console.log(valid) // 测试能否获取验证结果
})
接着,判断验证结果,决定是否发起登录请求。
login () {
this.$refs.loginFormRef.validate(valid => {
// console.log(valid)
if (!valid) return false
})
}
**注意!** 由于此时项目中还没有 **全局配置** axios 包,无法发起请求,因此,要先在入口文件 main.js 中对 axios 进行全局配置:
* ① 导入 axios 包:
import axios from 'axios'
* ② 把包挂载到 **Vue** 原型对象上:
Vue.prototype.$http = axios
这样,每个 **Vue** 的组件都可以通过 `this` 直接访问到 `$http`,从而发起 ajax 请求 。
* ③ 当挂载为原型的属性之后,回头再为 ajax 设置请求的根路径:
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
(*这个根路径在项目的 API 文档中能找到*)
此 3 个步骤的完整代码如下:
import axios from 'axios'
// 配置请求的根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
Vue.prototype.$http = axios
配置完 ajax ,现在回到 Login.vue 登录组件中,通过 `this` 就可以访问到原型上的 `$http` 成员,从而通过它发起 Ajax 请求 (请求地址 login,请求方式 post):
login () {
this.$refs.loginFormRef.validate(valid => {
// console.log(valid)
if (!valid) return false
const result = this.$http.post('login', this.loginForm) // eslint-disable-line no-unused-vars
console.log(result)
})
}
**注解:** 在 data 中,有个 `loginFrom`,也就是登录表单的数据绑定对象,由于用户在`el-form`表单中填写的数据都会自动同步到该对象。因此可以直接将这个 loginFrom 当做请求参数。
接下来,启动 MySQL 数据库服务~
现在,可以测试一下:在登录表单中随便填入用户名和密码,点击 登录 ,看看请求结果 `result` 输出了什么,如下图所示:
![在这里插入图片描述][20210102232233259.png]
可以看到在控制台输出的是 Promise 对象,我们知道,如果某个方法的返回值是 Promise,那么就可以用 `async`、`await` 来简化这次 Promise 操作,因此前面的代码可以写成:
login () {
this.$refs.loginFormRef.validate(async valid => {
// console.log(valid)
if (!valid) return false
const result = await this.$http.post('login', this.loginForm) // eslint-disable-line no-unused-vars
console.log(result)
})
}
**注:** 如果仅添加 await 会报错:‘await’ is only allowed within async functions。这是因为 `await` 只能用在被 `async` 修饰的方法中(这里就是把箭头函数 valid 修饰成 异步 的函数)。
修改完毕再次打印时,可发现 result 已不再是一个 Promise 对象,而是一个具体的响应对象,对象中包含了 6 个属性,都是 axios 帮我们封装好的,其中,data 才是服务器返回的真实数据(其它 5 个属性我们不需要),如下图所示: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 58]
此时,我们可以从 result 对象身上的 data 属性给解构赋值出来并重命名为 res ,这样就表示能访问到真实的数据了:
const { data: res} = await this.$http.post('login', this.loginForm) // eslint-disable-line no-unused-vars
console.log(res)
随意输入用户名、密码,点击 登录 按钮后,控制台打印如下:
![在这里插入图片描述][20210103000151725.png]
现在,我们来对登录状态进行判断:
login () {
this.$refs.loginFormRef.validate(async valid => {
// console.log(valid)
if (!valid) return false // 阻止登录请求
const { data: res } = await this.$http.post('login', this.loginForm) // eslint-disable-line no-unused-vars
// console.log(res)
if (res.meta.status !== 200) return alert('登录失败!')
alert('登录成功!')
})
}
页面测试如下:
![在这里插入图片描述][20210103001448735.gif_pic_center]
(注:演示中刷新网页时,由于加载了默认的正确用户名和密码,所以获取了登录成功的验证码)
接着,借助 [Element UI][] 的 Message 弹出层,给登录添加登录成功与否的消息提示,这样显得更加友好,如下所示:
--------------------
![在这里插入图片描述][2021010522165480.gif_pic_center]
--------------------
* **消息组件的使用**
* 1)在 `element.js`中导入 Message 组件:
import { Button, Form, FormItem, Input, Message } from 'element-ui'
// Vue.use(Message) // 错误的写法
**注意**:Message的配置和其它组件不一样,它需要进行全局挂载,即把 Message 挂载为 **Vue** 原型上的一个属性;
* 2)**配置 Message** :
// import { Button, Form, FormItem, Input, Message } from 'element-ui'
Vue.prototype.$Message = Message
**注:** 这里的 `$Message` 是我们的自定义属性名,可取任意合法名。“`=`” 后面的 Message 是组件名,必须按这个拼写来写(即,把弹框组件挂载到了原型对象上,这样的话,每一个组件都可以通过 this 来访问到 `$Message`进行弹窗提示! )。
* 3) **Message 的使用**
// if (res.meta.status !== 200) return alert('登录失败!')
// alert('登录成功!')
// 修改为
if (res.meta.status !== 200) return this.$message.error('登录失败!')
this.$message.success('登录成功!')
此时,效果如下图所示:![在这里插入图片描述][20210105225650760.gif_pic_center]
最终,**完整的 Login.vue 组件代码**,如下所示:
--------------------
<template>
<div class="login_container">
<!-- 登录盒子 -->
<div class="login_box">
<!-- 头像 -->
<div class="avatar_box">
<img src="../assets/logo.png" alt="">
</div>
<!-- 登录表单 -->
<el-form :model="loginForm" ref="LoginFormRef" :rules="loginFormRules" label-width="0px" class="login_form">
<!-- 用户名 -->
<el-form-item prop="username">
<el-input v-model="loginForm.username" prefix-icon="iconfont icon-user" ></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item prop="password">
<el-input type="password" v-model="loginForm.password" prefix-icon="iconfont icon-3702mima"></el-input>
</el-form-item>
<!-- 按钮 -->
<el-form-item class="btns">
<el-button type="primary" @click="login">登录</el-button>
<el-button type="info" @click="resetLoginForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script> export default { data() { return { //数据绑定 loginForm: { username: 'admin', password: '123456' }, //表单验证规则 loginFormRules: { username: [ { required: true, message: '请输入登录名', trigger: 'blur' }, { min: 3, max: 10, message: '登录名长度在 3 到 10 个字符', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 6, max: 15, message: '密码长度在 6 到 15 个字符', trigger: 'blur' } ] } } }, //添加行为, methods: { //添加表单重置方法 resetLoginForm() { // this=>当前组件对象,其中的属性$refs包含了设置的表单ref // console.log(this) this.$refs.LoginFormRef.resetFields() }, login() { //点击登录的时候先调用validate方法验证表单内容是否有误 this.$refs.LoginFormRef.validate(async valid => { console.log(this.loginFormRules) //如果valid参数为true则验证通过 if (!valid) { return } //发送请求进行登录 const { data: res } = await this.$http.post('login', this.loginForm) // console.log(res); if (res.meta.status !== 200) { //console.log("登录失败:"+res.meta.msg) return this.$message.error('登录失败:' + res.meta.msg) } this.$message.success('登录成功') console.log(res) //保存token window.sessionStorage.setItem('token', res.data.token) // 导航至/home this.$router.push('/home') }) } } } </script>
<style lang="less" scoped> .login_container { background-color: #2b5b6b; height: 100%; } .login_box { width: 450px; height: 300px; background: #fff; border-radius: 3px; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); .avatar_box { height: 130px; width: 130px; border: 1px solid #eee; border-radius: 50%; padding: 10px; box-shadow: 0 0 10px #ddd; position: absolute; left: 50%; transform: translate(-50%, -50%); background-color: #fff; img { width: 100%; height: 100%; border-radius: 50%; background-color: #eee; } } } .login_form { position: absolute; bottom: 0; width: 100%; padding: 0 20px; box-sizing: border-box; } .btns { display: flex; justify-content: flex-end; } </style>
--------------------
**其中,有用到以下内容,需要进行进一步处理:**
Ⅰ. **添加 element-ui 的表单组件**
在 plugins 文件夹中打开 element.js 文件,进行 elementui 的按需导入:
import Vue from 'vue'
import { Button } from 'element-ui'
import { Form, FormItem } from 'element-ui'
import { Input } from 'element-ui'
Vue.use(Button)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Input)
**Ⅱ. 添加第三方字体**
复制素材中的 fonts 文件夹到 assets 中,在入口文件 main.js 中导入:
import './assets/fonts/iconfont.css'
然后直接 `<el-input prefix-icon="iconfont icon-3702mima"></el-input>`
接着添加登录盒子
**Ⅲ. 添加表单验证的步骤**
1)给`<el-form>`添加属性`:rules="rules"`,rules 是一堆验证规则,定义在 script 中。
2)在 script 中添加 rules:export default\{ data()\{return\{…, rules: \{
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
region: [
{ required: true, message: '请选择活动区域', trigger: 'change' }
]
3)通过`<el-form-item>`的 prop 属性设置验证规则`<el-form-item label="活动名称" prop="name">`
4)导入axios 以发送 ajax 请求
打开main.js,导入 axios:
import axios from 'axios';
设置请求的根路径:
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/';
挂载axios:
Vue.prototype.$http = axios;
5)配置弹窗提示
在 plugins 文件夹中打开 element.js 文件,进行 elementui 的按需导入:
import { Message} from 'element-ui'
进行全局挂载:
Vue.prototype.$message = Message;
在 login.vue 组件中编写弹窗代码:
this.$message.error('登录失败')
六、登录成功之后的操作
实现思路:
1.将登录成功之后的 token ,保存到客户端的 sessionStorage 中;
原因如下:
- 项目中除了 登录 以外的其它 API 接口 ,必须在登录之后才能访问(访问接口时提供 token,表明已登录);
- token 只应在当前网站打开期间生效,所以将 token 保存在 sessionStorage 中。
- 之所以保存在 sessionStorage 而不是 localStorage 中,是因为
localStorage
是持久化的存贮机制,而sessionStorage
是会话期间的存贮机制。
* 2.通过 编程式导航 跳转到后台主页,路由地址是 /home (编程式导航:通过 `$router`对象,调用 `push` 方法来发生跳转)。
代码实现
A. 保持用户
token
信息要保存 token ,先要能访问到它:
console.log(res)
可以看到 res 上有个 data 属性,data 属性中包含 token字符串,可以调用sessionStorage.setItem
这个 API 接口,将这个 token 保存在 sessionStorage 中:window.sessionStorage.setItem('token', res.data.token)
括号中的参数是键值对的形式(键:
token
,值:res.data.token
)。
因此,当登录成功之后,我们将后台返回的 token 保存到 sessionStorage 中,操作完毕之后,需要跳转到`/home`目录:
this.$router.push('/home')
此时 login 方法的代码如下:
login () {
this.$refs.loginFormRef.validate(async valid => {
// console.log(valid)
if (!valid) return false
const { data: res } = await this.$http.post('login', this.loginForm) // eslint-disable-line no-unused-vars
// console.log(res)
if (res.meta.status !== 200) return this.$message.error('登录失败!')
this.$message.success('登录成功!')
// console.log(res)
window.sessionStorage.setItem('token', res.data.token)
this.$router.push('/home')
})
}
调试运行程序,当点击登录后,Application 里的 session Storage 中保存到了 `token` 值。同时,通过这次 编程式导航,发生了页面跳转。如下所示:
![在这里插入图片描述][2021010523562910.gif_pic_center]
但此时的 home 页面是空白的。
创建 home 页面并完善路由规则
- 1)创建 home 页面:在 components 目录中,新建一个 Home.vue 组件文件;
2)用 template 标签书写好基本结构代码:
<template>
<div>
Home 组件
</div>
</template>
<script> export default { } </script>
<style lang="less" scoped> </style>
- 1)创建 home 页面:在 components 目录中,新建一个 Home.vue 组件文件;
* 3)在路由文件夹下的`index.js`中导入组件并添加 路由规则:
import Home from '../components/Home.vue'
export default new VueRouter({
routes: [
{ path: '/', redirect: '/login' },
{ path: '/login', component: Login },
{ path: '/home', component: Home }
]
})
* **路由导航卫士控制页面访问权限**
当前“ `/home` ” 所对应的页面只有在登录的时候才允许被访问。
因此,如果用户未登录,直接通过 URL 访问特定(需要权限)的页面,则需要重新导航(*即强制跳转* )到登录页面。
那么,如何进行导航 ?
这就需要用到路由 **导航守卫**:为路由对象 router 调用一个 beforeEach 函数,这个 beforeEach 就叫做 **导航守卫** 。
router.beforeEach ((to, from, next) => { })
该函数接收一个 **回调函数**,包括 3 个行参,分别为`to`、`from`和`next`,其中:
* `to`:将要访问的路径;
* `from`:从哪个页面跳转过来;
* `next`:放行的一个函数,`next()`表示直接放行;`next('/login')`表示强制跳转。
* **路由导航卫士的用法**
判断 `to` 对应的地址是否为`/login`,
* 如果是,则表示用户要访问登录页,而登录页是不需要权限的,这时就直接调用 `next` 函数(即 `return next()`)放行,允许访问登录页;
* 如果不是,则要判断 sessionStorage 中是否存在 token。
先把 token 取出来,然后判断是否有 token ,如果没有 token,证明用户没有登录,此时需要强制跳转到登录页(`/login`),让用户登录后再进行访问。如果有 token 则直接放行(`next ()`)
* **代码实现**(导航跳转):
打开路由所对应的文件(有些版本的 Vue ui 生成的路由文件名是 router.js),我这里是 `index.js`,我们在前面已完成的代码:
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../components/Login.vue'
import Home from '../components/Home.vue'
Vue.use(VueRouter)
export default new VueRouter({
routes: [
{ path: '/', redirect: '/login' },
{ path: '/login', component: Login },
{ path: '/home', component: Home }
]
})
这里需要对代码进行改造,当前`export default new VueRouter({})` 表示是直接 new 了一个 VueRouter 对象并默认导出。
此时需要将 `export default` 和 `VueRouter`拆分开,先拿到 VueRouter 对象,给它挂载一个 **导航守卫**,然后再用 export default 暴露出去。修改如下:
const router = new VueRouter({
routes: [
{ path: '/', redirect: '/login' },
{ path: '/login', component: Login },
{ path: '/home', component: Home }
]
})
// 暴露路由对象之前,要挂载路由守卫
router.beforeEach((to, from, next) => {
if (to.push === '/login') return next()
// 获取 token
const tokenStr = window.sessionStorage.getItem('token')
if (!tokenStr) return next('/login')
next()
})
export default router
3.4 实现退出功能
1、实现原理:
基于 token 的方式实现 退出 比较简单,只需要 销毁本地的 token 即可。这样,后续的请求就不会携带 token ,必须重新登录生成一个新的 token 之后才可以访问页面。
2、功能实现
在 Home 组件中添加一个退出功能按钮,给 退出按钮 添加 点击事件,添加事件处理代码如下:
export default {
methods:{
logout(){
// 清空token
window.sessionStorage.clear();
// 跳转到登录页
this.$router.push('/login');
}
}
}
处理 ESLint 警告
- 补充
* **A、处理 ESLint 警告**
打开脚手架面板,查看 **警告** 信息
默认情况下,ESLint 和 VS code 格式化工具有 冲突,需要添加配置文件解决冲突。
* 1)在项目根目录新建 `.prettierrc.json` 文件(注意prettierrc前面有小点):
{
"semi":false, // 结尾处的分号(;)
"singleQuote":true // 单引号
}
* 2)打开`.eslintrc.js`文件,禁用对 space-before-function-paren 的检查:
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'space-before-function-paren' : 0
},
3)**\*其它方法**(注:此方法来源于网络,未亲测!仅记录于此供参考)
如果在使用 vue-cli 创建项目时已经选择了 `babel`、`eslint`,那么只需要安装缺少的包:
npm i prettier prettier-eslint --save-dev
这样也能得到正确的格式,其原理是先把代码用 prettier 格式化,然后再用 ESLint fix。
* **B、合并按需导入的 element-ui**
import Vue from 'vue'
import { Button, Form, FormItem, Input, Message } from 'element-ui'
Vue.use(Button)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Input)
// 进行全局挂载:
Vue.prototype.$message = Message
* **C.将代码提交到码云**
① 新建一个项目终端,输入命令查看修改过的或新增的文件内容:
git status
② 将所有文件添加到暂存区:
git add.
此时,所有文件变成绿色,表示已经都添加到了暂存 区
③ 将所有代码提交到本地仓库:
git commit -m "添加登录功能以及/home的基本结构"
④ 查看所处分支(所有代码都被提交到了 login 分支):
git branch
⑤ 将 login 分支代码合并到 master 主分支:
a、先切换到 master:
git checkout master
b、再 master 分支进行代码合并:
git merge login
⑥ 将本地最新的 master 推送到远端的码云:
git push
打开码云中的仓库如下图(表示已把本地的master 分支推送到了云端仓库中进行了保存)。![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 60]
⑦ 推送本地的子分支到码云
如下图所示,当前云端中只有一个 master 分支,并没有 login 子分支:![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 61]
因此还需要将 login 分支推送到云端:
a. 先切换到子分支:
git checkout 分支名
b. 然后推送到码云:
此时如果直接用 git push 推送是不会成功的,因为云端并没有记录 login 子分支。
由于是首次把 login 分支推送到云端分支,此时,需要在 `push` 加上一个参数 `-u`,完整命令如下:
git push -u origin 远端分支名
刷新后,即可看到仓库中多了一个 login 子分支:
![在这里插入图片描述][20210111224818835.png]
【**关于推送**】
我们写的源代码,经过测试之后没问题,一定要先合并到主分支,然后再将主分支推送到云端仓库中,同时,也不要忘了再把新建的子分支推送到云端仓库中。
4. 主页布局
- 实现后台首页的基本布局
- 实现左侧菜单栏
- 实现用户列表展示
- 实现添加用户
4.1 后台首页基本布局
整体布局: 先上下划分,再左右划分。
借助 Element UI 中的布局容器进行布局:
进入官网 ,找到 Container 布局容器组件:
- 找到符合主页设计图的样式:
- 找到该布局结构的代码,展开后复制粘贴到项目中
**基本布局**:打开 Home.vue 组件
* 原来 Home.vue 文件中的结构代码:
<template>
<div>
<el-button type="info" @click="logout">退出</el-button>
</div>
</template>
修改为官方提供的布局容器的结构:
<template>
<el-container>
<!-- 头部区域 -->
<el-header>Header<el-button type="info" @click="logout">退出</el-button></el-header>
<!-- 页面主体区域 -->
<el-container>
<!-- 侧边栏 -->
<el-aside width="200px">Aside</el-aside>
<!-- 右侧内容主体 -->
<el-main>Main</el-main>
</el-container>
</el-container>
</template>
保存后,打开页面并没有看到想要的效果,并且终端里有报错,如下图所示:![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 65]
这是因为我们还没有注册 el-container 这些组件。
在plugins 目录下的 `element.js` 中导入组件:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 66]
保存后,页面效果如下图:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 67]
--------------------
此时,虽然很丑,但是已初具雏形,并且也解决了报错问题。
接下来,在 Home.vue 文件中的 `<style>`标签内部添加css样式。
默认情况下,类似 element-ui 组件的名称就是 类名,利用这个类名可直接给对应的组件添加样式 。
.home-container {
height: 100%;
}
.el-header{
background-color:#373D41;
}
.el-aside{
background-color:#333744;
}
.el-main{
background-color:#eaedf1;
}
保存并刷新页面后如下图:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 68]
接下来要解决一个问题,就是让整个页面主体区域撑满屏幕。需要先检查元素看是什么原因导致的
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 69]
然后你可以看到,section 元素其实就是布局容器组件结构代码中最外层的 `<el-container>`,只要让它全屏,就能实现页面的布局效果。
给 Home.vue 里的`<el-container>` 添加一个类名`home-container`并设置高为100%:
<el-container class="home-container">
.home-container {
height:100%
}
这样就实现了充满整个屏幕的效果,如下图所示:![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 70]
4.2 顶部布局,侧边栏布局
4.2.1. 顶部布局
1)HTML:顶部原来的结构:
<!-- Home.vue 头部区域 -->
<el-header>Header<el-button type="info" @click="logout">退出</el-button></el-header>
HTML:修改后的结构为:
<!-- Home.vue 头部区域 -->
<el-header>
<div>
<img src="../assets/heima.png" alt="">
<span>电商后台管理系统</span>
</div>
<el-button type="info" @click="logout">退出</el-button>
</el-header>
2). 添加CSS样式
CSS:顶部原来的样式:
.el-header{
background-color:#373D41;
}
CSS:顶部修改后的样式:
.el-header{
background-color:#373D41;
display:flex;
justify-content: space-between;
padding-left: 10px;
align-items: center;
font-size: 20px;
color:#f1d277; div {
display: flex;
align-items: center;
}
span {
margin-left: 15px;
user-select:none;
}
}
【注】
align-items: center
:纵向上居中对齐;justify-content: space-between
:横向上两端对齐;user-select:none
:文本禁止被选中。效果如下图:
4.2.2 侧边栏菜单布局
菜单分为二级,并且可以折叠。
主要结构如下:
<el-menu>
<el-submenu>
<!-- 这个 template 是一级菜单的内容模板 -->
<i class="el-icon-menu"></i>
<span>一级菜单</span>
<!-- 在一级菜单中,可以嵌套二级菜单 -->
<el-menu-item>
<i class="el-icon-menu"></i>
<span slot="title">二级菜单</span>
</el-menu-item>
</el-submenu>
</el-menu>
最外层的<el-menu>
是一个包裹性质的容器,整个菜单项最外层必须用<el-menu>
进行包裹。一级菜单项中,用 <i>
指定图标项,<span>
指定一级菜单文本。二级菜单为 <el-menu-item>
,也有图标和文本。
在 element UI 官网找到导航菜单组件:
点击 显示代码 ,找到 “ 自定义颜色 ” 菜单对应的代码:
选中所有的UI结构后,粘贴到Home.vue
中的“侧边栏区域”。
侧边栏原代码:
<!-- 侧边栏 -->
<el-aside width="200px">Aside</el-aside>
删除 标签中的 “ Aside
” 文本,并复制 官网中的 UI 结构代码粘贴进来,同时,在将不需要的属性删掉后,侧边栏代码如下:
<!-- Home.vue 文件 -->
<!-- 侧边栏 -->
<el-aside width="200px">
<!-- 侧边栏菜单区域 -->
<el-menu background-color="#545c64" text-color="#fff" active-text-color="#ffd04b">
<el-submenu index="1">
<template slot="title">
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="1-1">选项1</el-menu-item>
<el-menu-item index="1-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="1-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="1-4">
<template slot="title">选项4</template>
<el-menu-item index="1-4-1">选项1</el-menu-item>
</el-submenu>
</el-submenu>
<el-menu-item index="2">
<i class="el-icon-menu"></i>
<span slot="title">导航二</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<i class="el-icon-document"></i>
<span slot="title">导航三</span>
</el-menu-item>
<el-menu-item index="4">
<i class="el-icon-setting"></i>
<span slot="title">导航四</span>
</el-menu-item>
</el-menu>
</el-aside>
删掉的 <el-menu>
中不需要的属性,包括:default-active="2"
、class="el-menu-vertical-demo"
、@open="handleOpen"
和 @close="handleClose"
。
此时,由于侧边栏中用到的组件还没有 “ 按需 ” 导入,接下来打开plugins路径下的 element.js
文件:
导入并注册以下组件:
Menu
;Submenu
;MenuItemGroup
(菜单分组项);MenuItem
代码如下:
// element.js 文件
import { Menu, Submenu, MenuItemGroup, MenuItem } from 'element-ui'
Vue.use(Menu)
Vue.use(Submenu)
Vue.use(MenuItemGroup)
Vue.use(MenuItem)
此时,实现的效果如下:
但是,当前侧边栏的颜色和整个侧边栏的颜色不一致,<el-menu>
标签的background-color
属性的值需要修改#333744
:
<el-menu background-color="#333744" text-color="#fff" active-text-color="#ffd04b">
修改后的效果:
这个官方提供的侧边栏,菜单中默认有禁用、三级菜单等,而本项目只需要一级、二级菜单,多余的不需要。因此,需再次梳理,将不需要的去除。
将导航二、三、四的代码全部删除后,结构如下:
<!-- 侧边栏 -->
<el-aside width="200px">
<el-menu background-color="#333744" text-color="#fff" active-text-color="#ffd04b">
<!-- 一级菜单 -->
<el-submenu index="1">
<!-- 一级菜单的模板区 -->
<template slot="title">
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<!-- 二级菜单 -->
<el-menu-item index="1-4-1">选项1</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
如下图:
但是,此时二级菜单没有图标,需要将一级菜单中的<i>
标签和<span>
标签复制后,粘贴到二级菜单<el-menu-item>
标签内:修改前:
<!-- 二级菜单 -->
<el-menu-item index="1-4-1">选项1</el-menu-item>
修改后:
<!-- 二级菜单 -->
<el-menu-item index="1-4-1">
<i class="el-icon-location"></i>
<span>导航一</span>
</el-menu-item>
基本结构修改完毕后,主页界面如下图所示:
此时的 Home.vue 组件布局全部代码如下:
<template>
<el-container class="home-container">
<!-- 头部区域 -->
<el-header>
<div>
<img src="../assets/heima.png" alt="">
<span>电商后台管理系统</span>
</div>
<el-button type="info" @click="logout">退出</el-button>
</el-header>
<!-- 页面主体区域 -->
<el-container>
<!-- 侧边栏 -->
<el-aside width="200px">
<el-menu background-color="#333744" text-color="#fff" active-text-color="#ffd04b">
<!-- 一级菜单 -->
<el-submenu index="1">
<!-- 一级菜单的模板区 -->
<template slot="title">
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<!-- 二级菜单 -->
<el-menu-item index="1-4-1">
<i class="el-icon-location"></i>
<span>导航一</span>
</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>
<!-- 右侧内容主体 -->
<el-main>Main</el-main>
</el-container>
</el-container>
</template>
4.3 通过接口获取菜单数据
axios请求拦截器
后台除了登录接口之外,都需要 token 权限验证,通过添加 axios请求拦截器来添加 token,以保证拥有获取数据的权限。
在 main.js 中添加代码,在将 axios 挂载到 vue 原型 之前添加相应代码:
打开 入口文件 main.js ,找
axios
配置节点:// 配置请求的根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
Vue.prototype.$http = axios
在挂载到原型对象之前,先为它设置拦截器:
// 配置请求的根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
axios.interceptors.request.use(config => {
console.log(config)
return config
})
Vue.prototype.$http = axios
config
即是请求对象,里面包含了很多的属性。通过打印,可以看到它包含了请求头headers
:接口说明:
根据 API 接口说明,需要为请求对象挂载一个
Authorization
字段,但是目前并没有headers
字段,需要手动为其添加,其值为已经保存在SessionStorage
里的token
字符串。//请求在到达服务器之前,先会调用use中的这个回调函数来添加请求头信息
// 配置请求的根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
axios.interceptors.request.use(config => {
// console.log(config)
config.headers.Author = window.sessionStorage.getItem('token')
// 最后必须 return config
return config
})
Vue.prototype.$http = axios
Authorization
字段添加成功,只是它的值是null
。原因是我们发起的是 “登录” 请求,登录期间服务器还没有颁发令牌。如果是登录后,调用其它接口时,再次监听这次请求,就会发现Authorization
的值是一个真正的token
令牌。这样,就为每一次的 API 请求,挂载了
Authorization
请求头,对于有权限要求的 API ,就能成功调用了。注释:
request
就是请求拦截器,为请求拦截器挂载一个回调函数,只要用户通过axios向服务器端发送了数据请求,就必然会在请求期间,优先调用 use 回调函数,在请求到达服务器之前,对请求数据做预处理。return config
—— 最后必须写上这句,代表已经把请求头做了一次预处理(固定写法)。请求拦截器相当于一个预处理的过程。
此时,`main.js`文件完整代码如下:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import './plugins/element.js'
// 导入全局样式表
import './assets/css/global.css'
// 导入字体图标
import './assets/fonts/iconfont.css'
import axios from 'axios'
// 配置请求的根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
axios.interceptors.request.use(config => {
// console.log(config)
config.headers.Authorization = window.sessionStorage.getItem('token')
// 最后必须 return config,固定写法
return config
})
Vue.prototype.$http = axios
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount('#app')
发起请求,获取左侧导航菜单
API 接口:
- 请求路径:menus
- 请求方法:get
- 响应数据:
. **1)打开`Home.vue`文件,添加相关代码**
**分析:** 网页刚一加载时,就应立即获取左侧菜单,因此需在行为区域定义一个生命周期函数:
添加前:
<script>
export default {
methods: {
logout () {
window.sessionStorage.clear()
this.$router.push('/login')
}
}
}
</script>
添加后:
<script>
export default {
created() {
this.getMenuList()
},
methods: {
logout () {
window.sessionStorage.clear()
this.$router.push('/login')
},
// 获取所有菜单
async getMenuList () {
const { data: res } = await this.$http.get('menus')
console.log(res)
}
}
}
</script>
`res`控制台打印结果如下:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 82]
如上图,得到的是一个对象,显示 “获取菜单列表成功”。共5个一级菜单,而且在一级菜单中,通过`children`属性嵌套了自己的二级菜单。
为了下一步在页面中渲染出来,应把获取到的数据立即挂载到自己的 data 中,需定义一个组件的私有数据 `data`:
data () {
return {
// 左侧菜单数据
menulist: []
}
}
判断:
// 获取所有菜单
async getMenuList () {
const { data: res } = await this.$http.get('menus')
if (res.meta.status !== 200) return this.$message.error(res.meta.msg)
this.menulist = res.data
console.log(res)
}
4.4 动态渲染菜单数据并进行路由控制
A、请求侧边栏数据
<script>
export default {
data() {
return {
// 左侧菜单数据
menuList: null
}
},
created() {
// 在created阶段请求左侧菜单数据
this.getMenuList()
},
methods: {
logout() {
window.sessionStorage.clear()
this.$router.push('/login')
},
async getMenuList() {
// 发送请求获取左侧菜单数据
const { data: res } = await this.$http.get('menus')
if (res.meta.status !== 200) return this.$message.error(res.meta.msg)
this.menuList = res.data
console.log(res)
}
}
}
</script
B、通过v-for双重循环渲染左侧菜单
如上图所示,由于左侧菜单数据的menuList组数中,存在一、二级菜单项,因此需要双层循环来渲染菜单,外层获取一级菜单,内层获取二级菜单:1)一级菜单
添加循环渲染之前:
<!-- Home.vue 结构区域:一级菜单 -->
<el-submenu index="1">
<!-- 一级菜单的模板区 -->
<template slot="title">
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
添加循环渲染之后:
<!-- 一级菜单 -->
<el-submenu index="1" v-for="item in menulist" :key="item.id">
<!-- 一级菜单的模板区 -->
<template slot="title">
<i class="el-icon-location"></i>
<span>{
{item.authName}}</span>
</template>
效果如下:
但是可以发现,现在是点开一级菜单中的任何一个,所有的菜单项都会全部同时展开,这不符合要求(只能展开自己的菜单项,不能影响其它的)。此时需要为
<el-submenu>
指定一个唯一的index
(目前所有的都为index="1"
,相同了!),我们知道menulist
获取的数据中,item.id
是唯一的,所以可以给 index 绑定一个动态的值,即,将index="1"
,修改为:index="item.id"
,如下:<!-- <el-submenu index="1" v-for = "item in menulist" :key = "item.id"> -->
<el-submenu :index="item.id" v-for="item in menulist" :key="item.id">
此时,当点击当前菜单项时,其它的菜单项就不会再同步展开了,如下图所示:
but,我们也可以看到,控制台报错了,报错的原因是 index 只接收 “ 字符串 ” ,但是item.id
是一个数值,最简单的方法,是给它拼接一个空字符串,item.id + ''
,如下所示:<el-submenu :index="item.id + ''" v-for="item in menulist" :key="item.id">
至此,一级菜单渲染完成。
2)二级菜单
二级菜单就是循环所有一级菜单的 children 属性
添加循环前:
<!-- 二级菜单 -->
<el-menu-item index="1-4-1">
<i class="el-icon-location"></i>
<span>导航一</span>
</el-menu-item>
添加循环渲染后:
<!-- 二级菜单 -->
<el-menu-item index="subItem.id + ''" v-for="subItem in item.children" :key="subItem.id">
<i class="el-icon-location"></i>
<span>{
{subItem.authName}}</span>
</el-menu-item>
到此为止,左侧菜单的 结构 渲染完毕。
本部分的**结构**代码如下:
<el-menu background-color="#333744" text-color="#fff" active-text-color="#ffd04b">
<!-- 一级菜单 -->
<el-submenu :index="item.id+''" v-for="item in menuList" :key="item.id">
<!-- 一级菜单模板 -->
<template slot="title">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>{
{item.authName}}</span>
</template>
<!-- 二级子菜单 -->
<el-menu-item :index="subItem.id+''" v-for="subItem in item.children" :key="subItem.id">
<!-- 二级菜单模板 -->
<template slot="title">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>{
{subItem.authName}}</span>
</template>
</el-menu-item>
</el-submenu>
</el-menu>
4.5 设置激活子菜单样式
通过更改 el-menu 的 active-text-color 属性可以设置侧边栏菜单中点击的激活项的文字颜色;
通过更改菜单项模板(template)中的i标签的类名,可以将左侧菜单栏的图标进行设置,我们需要在项目中使用第三方字体图标;
在数据中添加一个 iconsObj:
1 )左侧菜单美化
- ① 修改左侧菜单激活菜单项文本的颜色:
将最外层<el-menu>
中的active-text-color
属性的值修改为目标颜色#409eff
即可 ② 修改二级菜单项图标
在 Element UI 官网中找到 icon 图标,将原来默认的图标类名修改即可:<!-- 二级菜单 -->
<el-menu-item :index="subItem.id + ''" v-for="subItem in item.children" :key="subItem.id">
<!-- <i class="el-icon-location"></i> -->
<i class="el-icon-menu"></i>
- ① 修改左侧菜单激活菜单项文本的颜色:
* **③ 修改一级菜单图标**
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 85]
由于每个一级菜单项的图标都不一样,不能像二级菜单那样统一添加一种图标。所以需要通过自定义图标的形式来解决。
这里仍然要用到前面使用过的第三方的字体图标库,之前已下载好放在 `\assets\fonts` 中:
![在这里插入图片描述][20210123120515293.gif_pic_center]
【**问题** 】:
前面我们的一级菜单的每一项,都是通过 for循环 自动生成的。那么,怎样让它在生成一级菜单期间,自动生成图标 ?
【**解决方案**】:
在 data 中,先定义一个**字体图标对象** `iconsObj`,将每个菜单项的 id 做为 Key, id 对应的图标做为值 。
找到一级菜单各项的 id 值,将其放在 iconsObj 中分别指向字体图标的类名
![在这里插入图片描述][20210123132525750.png]
定义字体图标对象的代码如下:
data () {
return {
// 左侧菜单数据
menulist: [],
iconsObj: {
'125': 'iconfont icon-user',
'103': 'iconfont icon-showpassword',
'101': 'iconfont icon-shangpin',
'102': 'iconfont icon-danju',
'145': 'iconfont icon-baobiao'
}
}
},
**注:** 如果语法报错,可把包裹 Key 的引号去掉,如:`125: 'iconfont icon-user'`。
然后将图标类名进行数据绑定,绑定 iconsObj 中的数据:
接下来,修改原来的字体图标部分的代码,进行动态绑定,即每循环一次,根据 iconsObj 对象,把它 id 所对应的类取出来,放在图标标签`<i>`中:
<!-- 一级菜单 -->
<el-submenu :index="item.id + ''" v-for="item in menulist" :key="item.id">
<!-- 一级菜单的模板区 -->
<template slot="title">
<i :class="iconsObj[item.id]"></i>
<span>{
{item.authName}}</span>
</template>
保存后,图标就有了如下效果:
![在这里插入图片描述][2021012313473973.png]
需要给图标和菜单项文本增加间距。因为这些字体图标有一个共同的类 iconfont,因此只需在`<style></style>` 样式区域标签内,写上:
.iconfont {
margin-right: 10px;
}
2)每次只能展开一个菜单项并解决边框问题
① 解决多个菜单项能同时展开的问题
【问题】:当前所有的菜单都能被同时展开,而实际需求只允许每次展开一个,其余的折叠。【解决】
这个在 Element UI 官方的 NavMenu导航菜单 组件中,为
<el-menu>
提供了unique-opened
属性,其值类型为布尔值,默认值为 false,即,可同时展开若干菜单项。因此,为了保持左侧菜单每次只能打开一个,显示其中的子菜单,可在 el-menu 中添加一个属性 unique-opened;
把unique-opened
属性值重置为 true 即可实现(每次只展开一个菜单项)。添加属性并重置为 true 如下:
注: 或者也可以数据绑定进行设置(此时true
认为是一个bool
值,而不是字符串):unique-opened="true"
这样就实现了每次只展开唯一一个菜单项的效果,如下图所示:
* **② 解决边框线未对齐的问题**
![在这里插入图片描述][20210123142407949.png]
通过检查元素,发现`el-menu`类的样式中,存在一个`border-right`,其值为 1px 的边框,如下图所示:
![在这里插入图片描述][20210123143205970.gif_pic_center]
通过类名选择器,将其重置为 none :
.el-aside {
background-color:#333744; .el-menu {
border-right: none;
}
}
4.6 制作侧边菜单栏的伸缩(展开/折叠)功能
打开
Home.vue
文件,在侧边栏内部、菜单之前的上方添加一个 div 盒子:<!-- 侧边栏 -->
<el-aside width="200px">
<div class="toggle-button">|||</div>
<el-menu background-color="#333744" text-color="#fff" active-text-color="#409eff" unique-opened>
在
Home.vue
中的<style>
样式区域中,给展开折叠按钮盒子设置样式:.toggle-button {
background-color: #4a5064;
font-size: 10px;
line-height: 34px;
color: #fff;
text-align: center;
letter-spacing: 0.4em;
cursor: pointer;
}
注: letter-spacing 属性增加或减少字符间的空白(字符间距),详见 W3school 关于 letter-spacing 的说明文档 。
效果如下:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 87]
要实现折叠与展开功能,在按钮盒子上,定义一个事件名称 toggleCollapse,给按钮盒子绑定**单击事件**:
<div class="toggle-button" @click="toggleCollapse">|||</div>
接下来,需要在方法 methods 中 **定义事件函数**。
【**分析**】:
由于 **Element UI** 官网中,[NavMenu导航菜单][NavMenu] Menu 属性上提供了一个 collapse 参数,值类型为 **布尔值**:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 88]
因此,我们只需要在点击按钮时,切换 `<el-menu>` 标签的 collapse 属性值为 true 或 false 即可实现展开与折叠,回到代码中:
1)在 data 中,定义一个布尔值 isCollapse :
data () {
return {
// 左侧菜单数据
menulist: [],
iconsObj: { ...},
// 左侧菜单栏是否水平折叠:默认不折叠
isCollapse: false
}
},
再将这个布尔值`isCollapse`绑定到 `<el-Menu>` :
<!-- 侧边栏 -->
<el-aside width="200px">
<div class="toggle-button" @click="toggleCollapse">|||</div>
<el-menu background-color="#333744" text-color="#fff" active-text-color="#409eff" unique-opened :collapse="isCollapse">
3)事件处理函数:
// 点击按钮,切换菜单的折叠与展开
toggleCollapse() {
this.isCollapse = !this.isCollapse
}
运行效果如下:
![在这里插入图片描述][20210123160915123.gif_pic_center]
可以发现,默认的展开和折叠的动画效果很丑,为了让它流畅一些,需要把默认的动画效果关闭。
在 [Element UI NavMenu 导航菜单][NavMenu] 的 Menu 属性中,有个 collapse-transition 参数用于控制动画显示与否,默认参数值为 true ,因此,如将其值设为 false 即可关闭动画。
继续在`<el-menu>`中,绑定collapse-transition 属性,以关闭动画:
<el-menu background-color="#333744" text-color="#fff" active-text-color="#409eff" unique-opened :collapse="isCollapse" :collapse-transition="false">
但是,此时通过运行发现,菜单栏的背景并没有因为菜单的折叠而变小,如下所示:
![在这里插入图片描述][20210123162257819.gif_pic_center]
这是因为 模板结构中,侧边栏的宽度是写死的:
<!-- 侧边栏 -->
<el-aside width="200px">
正常的是,当折叠后,即 isCollapse 值为 true 时,侧边栏宽度变小,其值为 false 时重置为`200px`即可。
![在这里插入图片描述][20210123163157305.png]
由元素检查可知最小宽度为 64px ,所以,要绑定的宽度大小,用三元表达式判断的代码如下:
<!-- 侧边栏,宽度根据是否折叠进行设置 -->
<el-aside :width="isCollapse ? '64px':'200px'">
<!-- 伸缩侧边栏按钮 -->
<div class="toggle-button" @click="toggleCollapse">|||</div>
<!-- 侧边栏菜单,:collapse="isCollapse"(设置折叠菜单为绑定的 isCollapse 值),:collapse-transition="false"(关闭默认的折叠动画) -->
<el-menu
:collapse="isCollapse"
:collapse-transition="false"
......
效果如下图:
![在这里插入图片描述][20210123164003325.gif_pic_center]
4.7 在后台首页添加子级路由
4.7.1 主页 - 实现首页路由的重定向
【需求】: 当我们登录成功后,需重定向到一个 欢迎页,并在 Main 页面主体区域展示 Welcome 组件。
【实现过程】:
新增子级路由组件 Welcome.vue。
在 components 路径下新建 Welcome.vue 组件文件,如图:
同时在 Welcome.vue 中定义一个基本的 template 结构:<template>
<div>
<h3>Welcome</h3>
</div>
</template>
注: 如果组件中暂时没有需要的样式和行为可不写,单写一个 template 也不会报错。
在 router.js 中导入子级路由组件,并设置路由规则以及子级路由的默认重定向;
import Welcome from '../components/Welcome.vue'
...
{
path: '/home',
component: Home,
redirect: '/welcome',
children: [{ path: '/welcome', component: Welcome }]
}
**注意**,Welcome 是以子路由的形式存在于 Home 规则中(在 Home 页面中嵌套显示 欢迎页面)的,因此要把 Welcome 做为 Home 的子路由规则。所以这里是新增一个 children 属性(即子路由,是个数组,数组里面再放子路由的规则对象)。
* path:路由规则地址;
* component:用于指定要显示的组件。
* redirect:重定向。
如图所示:
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 90]
主体结构中添加一个路由占位符
打开 Home.vue,在 main 的主体结构中添加一个路由占位符
<router-view>
;<!-- 右侧内容主体 -->
<el-main>Main</el-main>
添加路由点位符后,如下所示:
<!-- 右侧内容主体 -->
<el-main>
<router-view></router-view>
</el-main>
至此,实现了首页路由的重定向。
制作好了 Welcome 子级路由之后,我们需要将所有的侧边栏二级菜单都改造成子级路由链接;
将 Welcome 路由设置为 Home 路由的子路由规则
【目 的】:点击左侧菜单栏中的二级菜单,可以在右侧主体区域切换显示不同的组件页面。
【实现原理】:将每一个二级菜单改为路由链接实现跳转 —— ps:当然不是单独为其设置 router-link ,而是有更简单的方式 。
【解决方法】:
在 Element UI 的 Menu 属性中,有个 router 参数,用于控制 “ 是否使用 vue-router 的模式 ” 其参数值的类型为布尔。默认值为 false,即未开启。如下图所示:
因此,只需要将 el-menu 的 router 属性设置为true
即可,此时当我们点击二级菜单的时候,就会根据菜单的index
属性进行路由跳转,如:/110
,<el-menu background-color="#333744" text-color="#fff" active-text-color="#409eff" unique-opened :collapse="isCollapse" :collapse-transition="false" router>
如图:
但是,使用index id
来作为跳转的路径不合适,我们可以重新绑定index的值为:index="'/'+subItem.path"
,设置时,需要在 path 地址名称前手动补上斜线(“/
”)。修改前:
<!-- 二级菜单 -->
<el-menu-item :index="subItem.id + ''" v-for="subItem in item.
将 index 绑定值修改为 path 做为跳转地址:
<!-- 二级菜单 -->
<el-menu-item :index= "'/'+ subItem.path + ''" v-for="subItem in item.
至此,实现了侧边栏路由改造,由于尚未对这些路由链接进行监听,因此打开的页面都是空白的。
效果如下图:
5. 用户管理
5.1 用户管理概述
通过后台管理用户的账号信息,具体包括用户信息的 **展示**、**添加**、**修改**、**删除**、**角色分配**、**账号启用** / **注销** 等功能。
- 用户信息列表展示
- 添加用户
- 修改用户
- 删除用户
- 启用或禁用用户
- 用户角色分配
5.2 用户管理-列表展示
5.2.1 用户列表布局
用户列表主体区域主要分两部分,具体布局划分如下:
页面布局实现:
创建 “ 用户列表 ” 链接对应的组件页面
- 在组件目录 conponets 中,新建 user 目录,再在此路径下新建 Users.vue 组件,如图:
在新组件中创建基本的页面结构:
<template>
<div>
<h3>用户列表组件</h3>
</div>
</template>
<script> export default { } </script>
<style lang="less" scoped> </style>
通过路由的形式,在右侧主体区展示用户列表
在 router 目录里的 index.js 路由文件中,导入用户列表组件、定义路由规则:
... // 导入的其它组件
import Users from '../components/user/Users.vue'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
... // 其它路由规则
{
path: '/home',
component: Home,
redirect: '/welcome',
children: [
{ path: '/welcome', component: Welcome },
{ path: '/users', component: Users }
]
}
]
})
但是,此时发现激活的菜单文本并没有高亮显示:
在 Menu 属性中有个 default-active 参数,表示当前激活菜单的 index 可以赋值给 default-active。即,如果想让菜单中的某一项被激活时高亮,就把该项对应的 index 赋值给整个 Menu 菜单的属性 default-active 。
打开 Home.vue ,在<el-menu>
中添加 default-active 属性,为便于测试,直接将其值写死为/users
,即default-active="/users"
,效果如下:
如果要把激活项的 index 换成动态的,要如何实现 ???
- 在组件目录 conponets 中,新建 user 目录,再在此路径下新建 Users.vue 组件,如图:
**实现思路:**
当我们点击链接时,把对应的地址先保存到 Session Storage 中。当刷新页面(即Home组件刚被创建)时,再把这个值取出来,动态的赋值给 el-menu 中的 default-active 属性 。
**第一步**:给所有的二级菜单绑定一个 **单击事件**,命名为 `saveNavState`,在单击事件中把 path 值存贮起来:
<!-- 二级菜单 -->
<el-menu-item :index="'/'+subItem.path + ''" v-for="subItem in item.children" :key="subItem.id" @click="saveNavState('/'+subItem.path)">
**第二步:** 定义 saveNavState 函数
// 保存链接的激活状态
saveNavState(activePath) {
window.sessionStorage.setItem('activePath', activePath)
}
运行程序后,即可保存path 值:![在这里插入图片描述][20210123214011217.gif_pic_center]
接下来,需要把保存的值取出来
**第三步:** 在 data 中定义 activePath 来保存数据,默认为空:`activePath: ''`。再将 activePath 绑定到 `<el-menu>` 的 default-active 属性上。替换原来写死的值`/users`。
**第四步:** 动态赋值
当整个 Home 组件一被创建的时候,就立即从 Session Storage 中把值取出来赋给 activePath ;
由于组件被创建时,第一时间执行的是 `Created` 生命周期函数,所以就直接在 `Created` 里进行赋值:
created() {
this.getMenuList()
this.activePath = window.sessionStorage.getItem('ctivePath')
}
![在这里插入图片描述][20210123221020952.gif_pic_center]
我们发现,当点击别的链接之后,再次点击 **用户列表** 时,对应的链接文本并没有高亮,原因是缺少了一步。应该是点击不同链接时,也要为当前的 activePath 重新赋值( saveNavState 点击事件里):
// 保存链接的激活状态
saveNavState(activePath) {
window.sessionStorage.setItem('activePath', activePath)
// 点击链接时,重新赋值
this.activePath = activePath
}
* **绘制用户列表基本 UI 结构**
* 面包屑导航:`el-breadcrumb`
* Element-UI 栅格系统基本使用:`el-row`
* 表格布局:`el-table`、`el-pagination`
在 [Element UI ][Element UI 2] 官方的 面包屑 导航组件中,找到对应的代码并复制。![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 97]
打开 Users.vue 文件,将复制好的代码粘贴到该组件文件 `template` 模板区中:
<template>
<div>
<!-- 面包屑导航区域 -->
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
<el-breadcrumb-item>用户列表</el-breadcrumb-item>
</el-breadcrumb>
</div>
</template>
接下来,在`element.js` 中按需导入 Breadcrumb 和 BreadcrumbItem 组件并注册(否则不生效):
import Vue from 'vue'
... // 其它组件
import { Breadcrumb, breadcrumbItem } from 'element-ui'
// 注册全局组件
... // 注册的其它组件
Vue.use(Breadcrumb)
Vue.use(breadcrumbItem)
Vue.prototype.$message = Message
此时,面包屑导航功能完成如下:
![在这里插入图片描述][20210123224940876.gif_pic_center]
**绘制卡片视图区域**:
在 [Element UI 官网][Element UI 3] 中找到 card卡片 组件,
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 98]
将复制的代码粘贴到 users.vue 文件中:
<!-- 卡片视图区 -->
<el-card class="box-card">
<div v-for="o in 4" :key="o" class="text item">
{
{'列表内容 ' + o }}
</div>
</el-card>
整理代码,将不需要的 for 循环和 box-card 类删掉后,结构代码如下:
<!-- 卡片视图区 -->
<el-card>
123
</el-card>
再在 element.js 中按需导入 card 卡片后,效果如下:
![在这里插入图片描述][20210125234449120.png]
目视可见卡片离面包屑导航太近。在 assets 中的 css 目录下,找到 `global.css`,为卡片视图区设置上部的间距,这里通过面包屑的类名选择器 el-breadcrumb ,给面包屑增加一个 `margin -bottom` 值,把卡片盒子挤下来一点:
.el-breadcrumb {
margin-bottom: 15px;
font-size: 12px;
}
保存后,基本的效果如下图: ![在这里插入图片描述][20210125235500797.png]
为卡片视图重置阴影样式。
.el-car {
box-shadow: 0 1px 1px rgba(0, 0 ,0.15) !important;
}
![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pnbGliaw_size_16_color_FFFFFF_t_70 99]
5.2.2 用户状态列和操作列处理
- 作用域插槽
接口调用
5.2.3 表格数据填充
- 调用后台接口;
表格数据初填充
const { data: res } = await this.$http.get(‘users’, { params: this.queryInfo })
if (res.meta.status !== 200) {return this.$message.error('查询用户列表失败!')
}
this.total = res.data.total
this.userlist = res.data.users
5.2.4 表格数据分页
分页组件用法:
- 当前页码:
pagenum
- 每页条数:
pagesize
- 记录总数:
total
- 页码变化事件
- 每页条数变化事件
分页条菜单控制
<el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryInfo.pagenum" :page-sizes="[2, 3, 5, 10]" :page-size="queryInfo.pagesize" layout="total, sizes, prev, pager, next, jumper" :total="total">
</el-pagination>
5.2.5 搜索功能
将搜索 关键字,作为 参数 添加到列表查询的参数中。
<el-input placeholder="请输入搜索的内容" v-model="queryInfo.query" clearable @clear="getUserList">
<el-button slot="append" icon="el-icon-search" @click="getUserList"> </el-button>
</el-input>
5.3 用户管理-用户状态控制
- 开关组件的用法
接口调用更改用户的状态
<el-switch v-model="scope.row.mg_state" @change="stateChanged(scope.row.id, scope.row.mg_state)">
</el-switch>
async stateChanged(id, newState) {
const { data: res } = await this.$http.put(`users/${ id}/state/${ newState}`)
if (res.meta.status !== 200) {
return this.$message.error('修改状态失败!')
}
}
5.4 用户管理-添加用户
5.4.1 添加用户表单弹窗布局
- 弹窗组件用法
控制弹窗显示和隐藏
<el-dialog title="添加用户" :visible.sync="addDialogVisible" width="50%">
<el-form :model="addForm" label-width="70px">
<el-form-item label="用户名" prop="username">
<el-input v-model="addForm.username"></el-input>
</el-form-item>
<!-- 更多表单项 -->
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="resetAddForm">取 消</el-button>
<el-button type="primary" @click="addUser">确 定</el-button>
</span>
</el-dialog>
5.4.2 表单验证
内置 表单验证规则:
<el-form :model="addForm" :rules="addFormRules" ref="addFormRef" >
<!-- 表单 -->
</el-form>
addFormRules: {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
}
this.$refs.addFormRef.validate(async valid => {
if (!valid) return
})
自定义 表单验证规则
手机号验证规则
const checkMobile = (rule, value, cb) => {
let reg = /^(0|86|17951)?(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$/
if (reg.test(value)) {
cb()
} else {
cb(new Error('手机号码格式不正确'))
}
}
mobile: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ validator: checkMobile, trigger: 'blur' }
]
5.4.3 表单提交
将用户信息作为参数,调用后台接口添加用户 。
this.$refs.addFormRef.validate(async valid => {
if (!valid) return
const { data: res } = await this.$http.post('users', this.addForm)
if (res.meta.status !== 201) {
return this.$message.error('添加用户失败!')
}
this.$message.success('添加用户成功!')
this.addDialogVisible = false
this.getUserList()
})
5.5 用户管理-编辑用户
5.5.1 根据 ID 查询用户信息
<el-button type="primary" size="mini" icon="el-icon-edit" @click="showEditDialog(scope.row.id)">
</el-button>
async showEditDialog(id) {
const { data: res } = await this.$http.get('users/' + id)
if (res.meta.status !== 200) {
return this.$message.error('查询用户信息失败!')
}
// 把获取到的用户信息对象,保存到 编辑表单数据对象中
this.editForm = res.data
this.editDialogVisible = true
}
5.5.2 编辑提交表单
this.$refs.editFormRef.validate(async valid => {
if (!valid) return
// 发起修改的请求
const { data: res } = await this.$http.put('users/' + this.editForm.id, {
email: this.editForm.email,
mobile: this.editForm.mobile
})
if (res.meta.status !== 200) {
return this.$message.error('编辑用户信息失败!')
}
this.$message.success('编辑用户信息成功!')
this.getUserList()
this.editDialogVisible = false
})
5.6 用户管理-删除用户
在点击删除按钮的时候,应跳出提示信息框,让用户确认要进行删除操作。
如果想要使用确认取消提示框,需要先将提示信息框挂载到vue中。
A. 导入MessageBox组件,并将MessageBox组件挂载到实例。
Vue.prototype.$confirm = MessageBox.confirm
B. 给用户列表中的删除按钮添加事件,并在事件处理函数中弹出确定取消窗,最后再根据id发送删除用户的请求
<el-button type="danger" size="mini" icon="el-icon-delete" @click="remove(scope.row.id)"></el-button>
async remove(id) {
// 询问是否要删除
const confirmResult = await this.$confirm('此操作将永久删除该用户, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).catch(err => err)
//如果用户点击确认,则confirmResult 为'confirm'
//如果用户点击取消, 则confirmResult获取的就是catch的错误消息'cancel'
if(confirmResult != "confirm"){
return this.$message.info("已经取消删除")
}
//发送请求,根据id完成删除操作
const { data: res } = await this.$http.delete('users/' + id)
if (res.meta.status !== 200) return this.$message.error('删除用户失败!')
this.$message.success('删除用户成功!')
this.getUserList()
},
6 权限管理
6.1 权限管理业务分析
通过权限管理模块控制不同的用户可以进行哪些操作,具体可以通过角色的方式进行控制,即每个用户分配一个特定的角色,角色包括不同的功能权限。
6.2 添加权限列表路由
创建权限管理组件**(Rights.vue),并在router.js添加对应的路由规则
import Rights from './components/power/Rights.vue'
......
path: '/home', component: Home, redirect: '/welcome', children: [
{ path: "/welcome", component: Welcome },
{ path: "/users", component: Users },
{ path: "/rights", component: Rights }
]
......
6.3 添加面包屑导航
在 Rights.vue 中添加面包屑组件展示导航路径。
6.2 权限列表展示
在data
中添加一个rightsList
数据,在methods
中提供一个getRightsList
方法发送请求获取权限列表数据,在created
中调用这个方法获取数据。
<el-table :data="rightsList" stripe>
<el-table-column type="index"></el-table-column>
<el-table-column label="权限名称" prop="authName"></el-table-column>
<el-table-column label="路径" prop="path"></el-table-column>
<el-table-column label="权限等级" prop="level">
<template slot-scope="scope">
<el-tag v-if="scope.row.level === 0">一级权限</el-tag>
<el-tag v-if="scope.row.level === 1" type="success">二级权限</el-tag>
<el-tag v-if="scope.row.level === 2" type="warning">三级权限</el-tag>
</template>
</el-table-column>
</el-table>
<script>
export default {
data(){
return {
//列表形式的权限
rightsList:[]
}
},
created(){
this.getRightsList();
},
methods:{
async getRightsList(){
const { data:res} = await this.$http.get('rights/list')
//如果返回状态为异常状态则报错并返回
if (res.meta.status !== 200)
return this.$message.error('获取权限列表失败')
//如果返回状态正常,将请求的数据保存在data中
this.rightsList = res.data
}
}
}
</script>
完整的添加权限,删除权限,编辑权限功能,这里不再列出,请参照前面编写过的角色管理的代码还有接口文档完成。
获取权限列表数据
// 获取权限列表数据
async getRightsList() {
const { data: res } = await this.$http.get('rights/list')
if (res.meta.status !== 200) {
return this.$message.error('获取权限列表失败!')
}
this.rightsList = res.data
}
6.3 角色列表展示
- 调用后台接口获取角色列表数据
角色列表展示
// 获取所有角色列表
async getRolesList() {const { data: res } = await this.$http.get('roles')
if (res.meta.status !== 200) {
return this.$message.error('获取角色列表失败!')
}
this.rolesList = res.data
},
6.4 用户角色分配
6.4.1 展示角色对话框
① 实现用户角色对话框布局
② 控制角色对话框显示和隐藏
③ 角色对话框显示时,加载角色列表数据
async showSetRoleDialog(userInfo) {
this.userInfo = userInfo
// 发起请求,获取所有角色的列表
const { data: res } = await this.$http.get('roles')
if (res.meta.status !== 200) {
return this.$message.error('获取角色列表失败!')
}
this.rolesList = res.data
this.setRoleDialogVidible = true
}
6.4.2 完成角色分配功能
async saveNewRole() {
if (this.selectedRoleId === '') {
return this.$message.error('请选择新角色后再保存!')
}
const { data: res } = await this.$http.put(`users/${ this.userInfo.id}/role`, {
rid: this.selectedRoleId
})
if (res.meta.status !== 200) {
return this.$message.error('分配角色失败!')
}
this.$message.success('分配角色成功!')
this.getUserList()
this.setRoleDialogVidible = false
}
6.5 角色权限分配
~ 未完待续 ~
注 解:
同源:即协议相同、域名相同、端口相同
同源策略的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。 ↩︎
跨域:是指浏览器不能执行其它网站的脚本。它是由浏览器的同源策略造成的,是浏览器对
javascript
实施的 安全限制。简单来讲,就是从地址A加载的页面,不能访问地址B的服务(如上图)。此时地址A与地址B不同源。 ↩︎
还没有评论,来说两句吧...