一般情况下客户使用的业务服务包括:PC端,小程序,移动web,移动app。
管理员使用的业务服务:PC后台管理端;
管理用户账号(登录,退出,用户管理,权限管理),商品管理(商品分类,分类参数,商品信息,订单),数据统计;
本项目(电商后台管理系统)采用 前、后端分离 的开发模式。
前端项目是基于 Vue 的 SPA(单页应用程序)项目;
前端项目技术栈()
Vue
;Vue-Router
;Element-UI
;Axios
;Echarts
后端项目技术栈
Node.js
;Express
;Jwt
(模拟session
);Mysql
;Sequelize
(操作数据库的框架)电商后台管理系统用于管理用户账号、商品分类、商品信息、订单、数据统计等业务功能。
电商后台管理系统整体采用前后端分离的开发模式,其中前端项目是 基于 Vue 技术栈的 SPA 项目 。
什么是 前后端分离 的开发模式?
前、后端分离的开发模式,是目前 主流 的 开发模式 。
优点: 开发效率高、项目易于维护。
安装 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 版本。
2. 通过 Vue 脚手架创建项目(本项目开发采用 “ 可视化面板 ” 创建)
1)cmd 终端中输入 :
vue ui
运行完成后,自动打开如下页面:
2)点击 “ + 创建 ” 进入到目录选择面板,选择目录后。再点击 “ + 在此创建新目录 ”按钮。
填写项目信息:
3)进入“ 预设 ”面板,并按如下图示勾选:
4)点击下一步,进入“ 功能 ”选择面板:
上图中,勾选了 “ 使用配置文件 ” 后,就会将不同的配置 单独 地存放为一个配置文件。点击 下一步,打开 配置 面板。
6)单击 下一步。提示是否保存新预设(方便下次直接选择该配置 / 也可不保存)。
点击 “保存预设并创建项目” 按钮后,系统自动创建项目如下:
3. 配置 Vue 路由
在上面步骤中已自动配置。
4. 配置 Element-UI 组件库:在插件中安装,搜索vue-cli-plugin-element
1)打开“仪表盘”,单击左侧 “ 插件 ”按钮,再单击右上角的 “ + 添加插件 ”
2)搜索插件 vue-cli-plugin-element
并安装。
5. 配置 axios 库:在依赖中安装,搜索axios
(运行依赖)
1)点击 左侧边栏 “ 依赖 ” 按钮,再点击右上角 “ 安装依赖 ”
2)安装依赖
6. 初始化 git 远程仓库
7. 将本地项目托管到 Github 或 码云中(方便团队成员协作开发)
1. 注册登录码云账号,注册地址:https://gitee.com/signup
2. 安装 git
在Windows上使用 Git,可以从Git 官网直接下载安装程序进行安装。
测试:git --version
(终端中打印出版本号 即为安装成功 )
下一步:
4. 在本地创建公钥(终端中运行如下命令)
ssh-keygen -t rsa -C "[email protected]"
注 :上述命令(示例)中的 “ [email protected]” 字样,请务必替换为自己注册 gitee 时的真实邮箱后,再回车执行!
然后回车,接着连敲 3 次回车(中间不需任何操作)即可生成公钥。如图:
5. 找到公钥地址
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 就是创建好的 公钥 了。
6. 用 记事本 或其它编辑器打开id_rsa.pub
文件,复制文件中的所有代码:
再点击码云中的 SSH 公钥按钮,将复制好的的公钥粘贴到公钥文本框中。
点击”确定“按钮,再根据提示输入验证密码后,即完成ssh公钥添加:
7. 测试公钥是否添加成功
在完成 gitee 设置中添加公钥后,再 cmd 终端中输入:
ssh -T [email protected]
过程中出现如下图所示的询问(是否继续连接?)时,输入 “yes” 回车继续,直到命令执行完成。
首次使用需要确认并添加主机到本机SSH可信列表。若返回 Hi XXX! You’ve successfully authenticated, but Gitee.com does not provide shell access. 内容,则证明添加成功。如下图所示:
再次运行命令ssh -T [email protected]
,同样也可看到如下所示信息:
8. 将本地代码托管到码云中
点击码云右上角的+
号 -> 新建仓库
9. 进行git
配置
注:执行如下命令前,必须确保本机已安装过 git,否则终端中会 报错 。
10 项目首次提交
1)检查状态:项目根目录下输入以下命令
git status
此时需要做一下处理,即把所有的文件都添加到 暂存区。
2)添加到 暂存区:运行如下命令:
git add .
注意:命令中的 add
和后面的.
(小圆点)中间有个空格,否则会报错。
3)本地提交:将暂存区中的文件提交至 本地仓库中 :
git commit -m "add files"
再次检查状态:
git status
提示当前 “ 处于主分支,工作目录是干净的 ”,即没有要提交的文件。
但当前的这些操作只是在本地操作仓库,仓库还没上传到码云中,
4)将本地仓库与远程 git
仓库 关联
找到并在终端中运行(你新建的码云仓库 vue_shop 页面最底部提供的那两句)代码,如下所示:
git remote add origin https://gitee.com/XXXXX/vue_shop.git
git push -u origin master
注: XXXX 为你的码云 帐户名称 ( 非邮箱名称)。如果运行 报错,请点击 这里 查看解决办法。
执行第二句命令时,会弹出如下安全验证,输入用户名和密码确认后,等待完成提交即可。
说明:如果是第一次向码云中提交代码,会弹出码云的帐号和密码输入窗口(以后不会再出现)
5)检查是否上传(远程仓库)成功
类似这样,表示本地仓库已成功上传到了码云中。
2.2.1 安装 MySQL 数据库
① 安装素材中提供的 phpStudy ,傻瓜式安装。
② 将素材中的压缩包解压,记住解压路径。
③ 运行phpStudy,单击“ MySQL管理器 ” 按钮,选择 MySQL导入导出 菜单项。
④ 按上图所示,找到对应路径下已解压得到的 db 文件夹中的 mydb.sql 数据库脚本文件,点击 “ 导入” 按钮,自动弹出黑色的命令行窗口,开始还原数据库(此时间稍长,请耐心等待~);
温馨提示! 还原结束时,黑色的命令行窗口会自动关闭,此时可按如下所示查看生成的数据库。
如在数据库目录下能够看到如下图所示的路径、文件,表示 数据库还原成功!
注: 由于开发过程中不需要用到 Apache,可将其 “ 停止 ” 服务,如下图所示:
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 软件(点此下载),启动 PostMan 填写相关参数(首次使用该软件需进行简单注册),如下所示:
注意:输入登录请求地址、用户字段名、密码字段名时,请务必与 API 文档保持一致;
点击 “ Send ” 后,服务端返回如下信息:
电商管理后台 API 接口文档 下载:https://pan.baidu.com/s/1OGxh05B0BocQm9cP3BWO7w 提取码:
1. 登录业务流程
在登录页面输入用户名和密码
调用后台接口进行验证
通过验证之后,根据后台的响应状态跳转到项目主页
2. 登录业务的相关技术点
http
是无状态的;
通过 cookie
在客户端记录状态;
通过 session
在服务器端记录状态;
通过 token
方式维持状态(推荐垮域 时采用)
一、登录逻辑:
在登录页面输入账号和密码进行登录,将数据发送给服务器 ==> 服务器返回登录的结果,登录成功则返回数据中带有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
表明当前工作区是干净的,可以进行登录页面的绘制。
—— 此时需要创建一个新分支。
2)确定当前工作目录是干净的之后,创建一个新分支并切换到该分支进行开发,开发完毕之后将其合并到 master
git checkout -b login
注: 在开发中,只要进行一个新功能开发的时候,尽量把它放到一个新分支上,当这个功能开发完毕后,再把它合并到主分支 master 上。
3)然后git branch
命令查看新创建的分支,确定我们正在使用 login分支 进行开发。
绿色 表示当前所处的分支。
4)接着,执行vue ui
命令打开 ui 界面,然后运行 serve,运行 app 查看当前项目效果。
四、登录页面的布局
此时,我们可以看到现在它只是一个默认页面,需要把它重置为空白页面:
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 中去掉所有样式),清理完成后如下所示:
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
则样式是全局的)。
scoped
:是 vue 指令,用来控制组件生效的范围(表示只在当前组件内生效,只要是单文件组件,都应加上)
注:当添加背景样式并保存代码修改后,浏览器会报错 “ 找不到 less-loader
”,如下图所示:
这是由于 Vue 的 cli 工具创建的项目默认并没有安装 less 相关的 loader,如果要使用 less 语法(如),此时则需要配置 less加载器(开发依赖),安装 less (开发依赖)。
因此,接下来打开可视化面板安装依赖 less-loader 和 less :
如上图所示,搜索并安装好 less-loader
以后,此时浏览器仍然报错,如下所示:
这时,回到安装依赖项界面,再次搜索 less 并安装好(因为 less-loader 依赖于 less)。
此时刷新网页并不能生效(仍然是报错状态),接下来,先关闭网页,停止 server,再次点击 运行 即可。
2)在路由文件 index.js 中导入Login.vue
组件并设置规则;
const router = new Router({
routes: [
// 当用户访问 /login 这个地址时,通过 component 属性指定要展示的组件 Loging
{ path: '/login', component: Login }
]
})
3)在 App.vue 中添加路由占位符:
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;
}
6)在入口文件 main.js 中导入 global.css,使得全局样式生效
import "./assets/css/global.css"
7)然后,将 Login.vue 中的根元素也设置为撑满全屏(height:100%
)
8)在 Login.vue
中绘制登录框
添加样式:
.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
属性的更多详情点击 这里 )。
添加样式后效果如下:
9)绘制顶部的默认头像盒子 avatar_box
写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( 更多详情点击 这里 )。
10)登录页面的布局
通过 Element-UI 实现 页面布局:
el-form
el-form-item
el-input
el-button
字体图标
打开官网 element-cn.eleme.io/#/zh-CN 找到组件:
点击顶部导航的 “ 组件 ”,侧边栏中选择 Form 表单,再点击 “ 显示代码 ”:
复制一个 item 项的代码(如下所示),粘贴到 Login 组件中并将不需要用到属性绑定删除、添加结束标签后如下所示:
保存后查看页面,发现控制台报错访问不到这几个元素:
这是因为 element-UI 是通过按需导入来使用的,必须先导入才能正常使用。
此时,打开 plugins 文件下的 element.js 文件,导入需要的组件(如果分几次导入可能会报错):
import { Button, Form, FormItem, Input } from 'element-ui'
把 Form 组件 input 输入框代码里不需要的文本 label="活动名称"去掉,再把占位的 label-width="80px"
重置为 0,并复制 2 组 el-form-item
元素结构代码(增加一个密码输入框和一个按钮区) :
在 Element 官网 找到 button 组件,复制 button 按钮的代码:
将复制的代码粘贴到按钮区,并给它添加一个btns
的类名,以便设置样式,代码如下:
登录
重置
由于
按钮默认是靠左对齐,实际需要它 靠右对齐,在 Login.vue
的 标签内部写上css 样式:
.btns {
display: flex; /* 弹性布局 */
justify-content: flex-end; /* 横轴 项目位于容器的尾部*/
}
Flex
弹性布局,可以简便、完整、响应式地实现各种页面布局:通过给父盒子添加flex
属性,来控制子盒子的位置和排列方式,点击 >> 查看详情
justify-content
用于设置或检索弹性盒子元素在主轴(横轴)方向上的对齐方式,访问 W3Cschool >>查看详情
注: 当将父盒子设为 flex 布局后,子元素的 float、clear 、 vertical-align 属性将失效。
此时,页面效果如下:
接下来,将整个 Form 表单区域底部对齐,这需要给
标签添加一个类名login_form
,并添加样式:
.login_form {
position: absolute;
bottom: 0;
width:100%;
padding: 0 20px;
}
效果如图: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就不会再撑大盒子。详情点击 >> 这里 。
接下来,绘制用户名和密码输入框前面的小图标
在 Element UI 官网 找到 input 组件 菜单项,再找到对应的样式:
将复制的代码粘贴到项目文件 login.vue 中:
再从 Element UI 官网 菜单中,点击侧边栏 icon 图标 菜单项,查找是否有对应的用户和密码图标:
再从 Element UI 官网 菜单中,点击侧边栏 icon 图标 菜单项,查找是否有对应的用户和密码图标:
由于在这里我们没有找到所需图标,因此要用到第三方图标库。
这里我们用 阿里图标库 ,将所需字体图标选定后,下载到本地,并在该字体压缩文件解压后,将所得文件夹命名为 fonts ,放到 src \ assets \ 目录下。
接着,在入口文件 main.js 中,导入字体图标的 css 样式表:
// 导入字体图标
import './assets/fonts/iconfont.css'
在浏览器中打开 fonts 文件夹下的 demo_index.html HTML文件,查看使用示例,并将图标分别放置到 Loging.vue
组件文件相应的用户名、密码input
标签内,替换原来的放大镜图标:
在浏览器中打开 fonts 文件夹下的 demo_index.html HTML文件,查看使用示例,并将图标分别放置到 Loging.vue 组件文件相应的用户名、密码input标签内,替换原来的放大镜图标:
代码如下:
(注: iconfont
是基础类,不能缺少。icon-xx
x 是图标名称)
11)登录表单的数据绑定(把用户名和密码对应的值自动绑定到数据源上):
打开 Element UI 官网,找到 Form 表单的定义,在 典型表单 中展开代码结构,能看到第一行
上,有个属性绑定 :model
就代表数据绑定:
即,示例中表示的是
表单中填写的所有数据,都会自动同步到 :model
指向的 "form
"对象上,form 是个数据对象,其定义如下所示:
归纳:表单添加数据绑定的步骤如下:
1)第一步:先给
添加 :model 属性进行数据绑定,指向一个数据对象;
2)第二步:为每个表单项里的文本输入框,通过v-model
绑定到数据对象上对应的属性中。
实现如下:
1)打开 Login.vue 文件,通过:model
绑定一个新命名的数据对象 loginForm
,代码如下:
2)接下来,在 script 标签所在的行为区域中,定义 loginForm 这个数据对象:
3)通过v-model
将 loginForm 里的对象 username
和 password
,分别双向绑定到用户名和密码输入框上,代码如下:
再为密码输入框添加一个type
属性,属性值设为 password
,以使密码以 *
号显示,保证用户账户的安全性。
12)实现表单的数据验证
目标:当填写完用户名和密码,只要鼠标一离开文本框,就会立即对填写的数据进行合法性的校验。要如何添加数据验证行为呢?
方法:打开 Element UI 官网, Form表单,找到该组件页面后面的表单验证:
分析: 如上图所示展开的结构代码,解读如下,
首先,要为
组件,进行属性绑定,绑定rules
属性,其属性值是一个表单验证规则对象;
其次,该验证规则对象要在 标签行为区域中的 data 数据里进行定义,其中,每一个属性都是一个验证规则。
最后,为不同的表单 item 项,通过 prop
属性来指定不同的验证规则,来进行表单的验证。
具体实现:
1)打开登录组件 login.vue ,在
元素上通过:rules
绑定 loginFormRules
这个新的验证规则对象:
2)复制 loginFormRules
对象名称,到 login.vue文件的 script 行为区域中的 data 中进行定义:
// 表单的验证规则对象
loginFormRules: {
// 验证用户名是否合法
username: [],
// 验证密码是否合法
password: []
}
验证规则可直接到 Element UI 官网中进行复制:
将复制过来的代码里的提示文字做适当修改,整理后代码如下:
// 表单的验证规则对象
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)将定义的规则应用到表单中
分别在用户名和密码输入框的
元素标签中,添加 prop 属性,并指向username
和 password
:
13)实现表单的重置功能
目标: 当点击 “ 重置 ”按钮时,能够重置校验结果
如何实现 ? 查看 Element UI 官方文档,在Form
组件对应的页面中,底部提供了一个方法:
只要我们获取到了表单的实例对象,就可通过实例对象直接访问(调用) resetFields 函数。从而重置整个表单,将所有字段值重置为初始值并移除校验结果。
如何拿到表单的实例对象 ?
分析:
① 要拿到
组件的实例对象,需要给它添加一个ref
引用,引用名称我们定义为 loginFormRef;
② 接下来,只要能够获取到 loginFormRef,就能拿到 el-form
表单的实例对象,即 loginFormRef 就是表单的实例对象,可以直接通过它来调用 resetFields 函数,以重置表单。
实现过程:
① 为组件添加 ref
引用:
② 通过@click
为 重置 按钮绑定 单击事件 resetLoginForm :
登录
重置
③ 在 script 的export default
中定义* resetLoginForm 方法,先打印出 this
,查看其具体指向。
methods: {
// 点击重置按钮,重置登录表单
resetLoginForm () {
console.log(this)
}
}
从打印结果可以看到,this 指向的是一个组件的实例对象 VueComponent{...},展开该对象下,可见一个数据对象 $refs: {loginFormRef: VueComponent},其中的一个属性 loginFormRef 就是前面为 el-form 定义的 ref 引用名称,如下图所示:
由此可知,通过 this.$refs
可以直接获取到 resetLoginForm 这个引用对象,该引用对象就是 el-form 的实例。代码及效果如下:
methods: {
// 点击重置按钮,重置登录表单
resetLoginForm () {
// console.log(this)
this.$refs.loginFormRef.resetFields()
}
}
这样,就实现了当点击 “ 重置 ”按钮时,重置校验结果的功能。而文本框好像没有被清空是什么原因呢 ?其实这是一种错觉。那是因为文本框是双向绑定到 data
中的数据中,而该数据是设置了默认值的,重置后,就又把默认值写进了已清空的文本框。
13)登录前的预校验
当我们点击登录按钮时,不应该直接发起数据请求,而是在请求之前先对表单数据进行预验证,验证通过时,才允许发起网络请求。否则,应该直接提示用户表单数据 不合法 。
实现思路: 当点击登录时,通过调用表单的某个函数来验证。具体调用哪个函数呢 ? 同样是在 Element UI 官网中,找到 组件 => Form表单 => Form Methods 节点的 validate
函数:
Function(callback: Function(boolean, object))
该函数接收一个 callback
回调函数,回调函数的第 1 个行参是布尔值代表校验的结果。如果校验通过,则返回 true
;反之,则返回false
。
如何调用 validate
函数 ?
调用方法:
① 通过@click
给 登录 按钮绑定单击事件(事件处理函数命名为 login
);
登录
② 通过 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
输出了什么,如下图所示:
这样,就实现了当点击 “ 重置 ”按钮时,重置校验结果的功能。而文本框好像没有被清空是什么原因呢 ?其实这是一种错觉。那是因为文本框是双向绑定到 data 中的数据中,而该数据是设置了默认值的,重置后,就又把默认值写进了已清空的文本框。
可以看到在控制台输出的是 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 个属性我们不需要),如下图所示:
此时,我们可以从 result 对象身上的 data 属性给解构赋值出来并重命名为 res ,这样就表示能访问到真实的数据了:
const {data: res} = await this.$http.post('login', this.loginForm) // eslint-disable-line no-unused-vars
console.log(res)
随意输入用户名、密码,点击 登录 按钮后,控制台打印如下:
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('登录成功!')
})
}
接着,借助 Element UI 的 Message 弹出层,给登录添加登录成功与否的消息提示,这样显得更加友好,如下所示:
消息组件的使用
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('登录成功!')
最终,完整的 Login.vue 组件代码,如下所示:
登录
重置
其中,有用到以下内容,需要进行进一步处理:
Ⅰ. 添加 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'
然后直接
接着添加登录盒子
Ⅲ. 添加表单验证的步骤
1)给
添加属性: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)通过
的 prop 属性设置验证规则
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 中;
原因如下:
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
值。同时,通过这次 编程式导航,发生了页面跳转。如下所示:
创建 home 页面并完善路由规则
1)创建 home 页面:在 components 目录中,新建一个 Home.vue 组件文件;
修改完毕再次打印时,可发现 result 已不再是一个 Promise 对象,而是一个具体的响应对象,对象中包含了 6 个属性,都是 axios 帮我们封装好的,其中,data 才是服务器返回的真实数据(其它 5 个属性我们不需要),如下图所示:
2)用 template 标签书写好基本结构代码:
Home 组件
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
,
先把 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
1、实现原理:
基于 token 的方式实现 退出 比较简单,只需要 销毁本地的 token 即可。这样,后续的请求就不会携带 token ,必须重新登录生成一个新的 token 之后才可以访问页面。
2、功能实现
在 Home 组件中添加一个退出功能按钮,给 退出按钮 添加 点击事件,添加事件处理代码如下:
export default {
methods:{
logout(){
// 清空token
window.sessionStorage.clear();
// 跳转到登录页
this.$router.push('/login');
}
}
}
补充
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 分支推送到了云端仓库中进行了保存)。
⑦ 推送本地的子分支到码云
如下图所示,当前云端中只有一个 master 分支,并没有 login 子分支:
因此还需要将 login 分支推送到云端:
a. 先切换到子分支:
git checkout 分支名
b. 然后推送到码云:
此时如果直接用 git push 推送是不会成功的,因为云端并没有记录 login 子分支。
由于是首次把 login 分支推送到云端分支,此时,需要在 push
加上一个参数 -u
,完整命令如下:
git push -u origin 远端分支名
【关于推送】
我们写的源代码,经过测试之后没问题,一定要先合并到主分支,然后再将主分支推送到云端仓库中,同时,也不要忘了再把新建的子分支推送到云端仓库中。
整体布局: 先上下划分,再左右划分。
借助 Element UI 中的布局容器进行布局:
基本布局:打开 Home.vue 组件
原来 Home.vue 文件中的结构代码:
退出
修改为官方提供的布局容器的结构:
Header退出
Aside
Main
保存后,打开页面并没有看到想要的效果,并且终端里有报错,如下图所示:
这是因为我们还没有注册 el-container 这些组件。
在plugins 目录下的 element.js
中导入组件:
此时,虽然很丑,但是已初具雏形,并且也解决了报错问题。
接下来,在 Home.vue 文件中的 标签内部添加css样式。
默认情况下,类似 element-ui 组件的名称就是 类名,利用这个类名可直接给对应的组件添加样式 。
.home-container {
height: 100%;
}
.el-header{
background-color:#373D41;
}
.el-aside{
background-color:#333744;
}
.el-main{
background-color:#eaedf1;
}
接下来要解决一个问题,就是让整个页面主体区域撑满屏幕。需要先检查元素看是什么原因导致的
然后你可以看到,section 元素其实就是布局容器组件结构代码中最外层的
,只要让它全屏,就能实现页面的布局效果。
给 Home.vue 里的
添加一个类名home-container
并设置高为100%:
.home-container {
height:100%
}
1)HTML:顶部原来的结构:
Header退出
HTML:修改后的结构为:
电商后台管理系统
退出
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
:文本禁止被选中。
菜单分为二级,并且可以折叠。
主要结构如下:
一级菜单
二级菜单
最外层的
是一个包裹性质的容器,整个菜单项最外层必须用
进行包裹。一级菜单项中,用 指定图标项,
指定一级菜单文本。二级菜单为
,也有图标和文本。
在 element UI 官网找到导航菜单组件:
点击 显示代码 ,找到 “ 自定义颜色 ” 菜单对应的代码:
选中所有的UI结构后,粘贴到Home.vue
中的“侧边栏区域”。
侧边栏原代码:
Aside
删除 标签中的 “ Aside
” 文本,并复制 官网中的 UI 结构代码粘贴进来,同时,在将不需要的属性删掉后,侧边栏代码如下:
导航一
分组一
选项1
选项2
选项3
选项4
选项1
导航二
导航三
导航四
删掉的
中不需要的属性,包括: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)
此时,实现的效果如下:
但是,当前侧边栏的颜色和整个侧边栏的颜色不一致,
标签的background-color
属性的值需要修改#333744
:
这个官方提供的侧边栏,菜单中默认有禁用、三级菜单等,而本项目只需要一级、二级菜单,多余的不需要。因此,需再次梳理,将不需要的去除。
后台除了登录接口之外,都需要 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 接口:
. 1)打开Home.vue
文件,添加相关代码
分析: 网页刚一加载时,就应立即获取左侧菜单,因此需在行为区域定义一个生命周期函数:
添加前:
添加后:
res
控制台打印结果如下:
如上图,得到的是一个对象,显示 “获取菜单列表成功”。共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)
}
A、请求侧边栏数据
B、通过v-for双重循环渲染左侧菜单
如上图所示,由于左侧菜单数据的menuList组数中,存在一、二级菜单项,因此需要双层循环来渲染菜单,外层获取一级菜单,内层获取二级菜单:
1)一级菜单
添加循环渲染之前:
导航一
添加循环渲染之后:
{{item.authName}}
效果如下:
但是可以发现,现在是点开一级菜单中的任何一个,所有的菜单项都会全部同时展开,这不符合要求(只能展开自己的菜单项,不能影响其它的)。
此时需要为
此时,当点击当前菜单项时,其它的菜单项就不会再同步展开了,如下图所示:
but,我们也可以看到,控制台报错了,报错的原因是 index 只接收 “ 字符串 ” ,但是item.id
是一个数值,最简单的方法,是给它拼接一个空字符串,item.id + ''
,如下所示:
至此,一级菜单渲染完成。
2)二级菜单
二级菜单就是循环所有一级菜单的 children 属性
添加循环前:
导航一
添加循环渲染后:
{{subItem.authName}}
到此为止,左侧菜单的 结构 渲染完毕。
本部分的结构代码如下:
{{item.authName}}
{{subItem.authName}}
通过更改 el-menu 的 active-text-color 属性可以设置侧边栏菜单中点击的激活项的文字颜色;
通过更改菜单项模板(template)中的i标签的类名,可以将左侧菜单栏的图标进行设置,我们需要在项目中使用第三方字体图标;
在数据中添加一个 iconsObj:
1 )左侧菜单美化
将最外层
中的 active-text-color
属性的值修改为目标颜色#409eff
即可
② 修改二级菜单项图标
在 Element UI 官网中找到 icon 图标,将原来默认的图标类名修改即可:
③ 修改一级菜单图标
由于每个一级菜单项的图标都不一样,不能像二级菜单那样统一添加一种图标。所以需要通过自定义图标的形式来解决。
这里仍然要用到前面使用过的第三方的字体图标库,之前已下载好放在 \assets\fonts
中:
【问题 】:
前面我们的一级菜单的每一项,都是通过 for循环 自动生成的。那么,怎样让它在生成一级菜单期间,自动生成图标 ?
【解决方案】:
在 data 中,先定义一个字体图标对象 iconsObj
,将每个菜单项的 id 做为 Key, id 对应的图标做为值 。
找到一级菜单各项的 id 值,将其放在 iconsObj 中分别指向字体图标的类名
定义字体图标对象的代码如下:
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 所对应的类取出来,放在图标标签中:
{{item.authName}}
保存后,图标就有了如下效果:
需要给图标和菜单项文本增加间距。因为这些字体图标有一个共同的类 iconfont,因此只需在 样式区域标签内,写上:
.iconfont {
margin-right: 10px;
}
2)每次只能展开一个菜单项并解决边框问题
【问题】:当前所有的菜单都能被同时展开,而实际需求只允许每次展开一个,其余的折叠。
【解决】
这个在 Element UI 官方的 NavMenu导航菜单 组件中,为
因此,为了保持左侧菜单每次只能打开一个,显示其中的子菜单,可在 el-menu 中添加一个属性 unique-opened;
把 unique-opened
属性值重置为 true 即可实现(每次只展开一个菜单项)。添加属性并重置为 true 如下:
注: 或者也可以数据绑定进行设置(此时true
认为是一个bool
值,而不是字符串) :unique-opened="true"
② 解决边框线未对齐的问题
通过检查元素,发现el-menu
类的样式中,存在一个border-right
,其值为 1px 的边框,如下图所示:
通过类名选择器,将其重置为 none :
.el-aside {
background-color:#333744;
.el-menu {
border-right: none;
}
}
Home.vue
文件,在侧边栏内部、菜单之前的上方添加一个 div 盒子:
Home.vue
中的
样式区域中,给展开折叠按钮盒子设置样式: .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 的说明文档 。
效果如下:
要实现折叠与展开功能,在按钮盒子上,定义一个事件名称 toggleCollapse,给按钮盒子绑定 单击事件:
接下来,需要在方法 methods 中 定义事件函数。
【分析】:
由于 Element UI 官网中,NavMenu导航菜单 Menu 属性上提供了一个 collapse 参数,值类型为 布尔值:
因此,我们只需要在点击按钮时,切换
标签的 collapse 属性值为 true 或 false 即可实现展开与折叠,回到代码中:
1)在 data 中,定义一个布尔值 isCollapse :
data () {
return {
// 左侧菜单数据
menulist: [],
iconsObj: {...},
// 左侧菜单栏是否水平折叠:默认不折叠
isCollapse: false
}
},
再将这个布尔值isCollapse
绑定到
:
3)事件处理函数:
// 点击按钮,切换菜单的折叠与展开
toggleCollapse() {
this.isCollapse = !this.isCollapse
}
可以发现,默认的展开和折叠的动画效果很丑,为了让它流畅一些,需要把默认的动画效果关闭。
在 Element UI NavMenu 导航菜单 的 Menu 属性中,有个 collapse-transition 参数用于控制动画显示与否,默认参数值为 true ,因此,如将其值设为 false 即可关闭动画。
继续在
但是,此时通过运行发现,菜单栏的背景并没有因为菜单的折叠而变小,如下所示:
这是因为 模板结构中,侧边栏的宽度是写死的:
正常的是,当折叠后,即 isCollapse 值为 true 时,侧边栏宽度变小,其值为 false 时重置为200px
即可。
由元素检查可知最小宽度为 64px ,所以,要绑定的宽度大小,用三元表达式判断的代码如下:
【需求】: 当我们登录成功后,需重定向到一个 欢迎页,并在 Main 页面主体区域展示 Welcome 组件。
【实现过程】:
在 components 路径下新建 Welcome.vue 组件文件,如图:
同时在 Welcome.vue 中定义一个基本的 template 结构:
Welcome
注: 如果组件中暂时没有需要的样式和行为可不写,单写一个 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 属性(即子路由,是个数组,数组里面再放子路由的规则对象)。
如图所示:
主体结构中添加一个路由占位符
打开 Home.vue,在 main 的主体结构中添加一个路由占位符
;
Main
添加路由点位符后,如下所示:
至此,实现了首页路由的重定向。
制作好了 Welcome 子级路由之后,我们需要将所有的侧边栏二级菜单都改造成子级路由链接;
将 Welcome 路由设置为 Home 路由的子路由规则
【目 的】:点击左侧菜单栏中的二级菜单,可以在右侧主体区域切换显示不同的组件页面。
【实现原理】:将每一个二级菜单改为路由链接实现跳转 —— ps:当然不是单独为其设置 router-link ,而是有更简单的方式 。
【解决方法】:
在 Element UI 的 Menu 属性中,有个 router 参数,用于控制 “ 是否使用 vue-router 的模式 ” 其参数值的类型为布尔。默认值为 false,即未开启。如下图所示:
因此,只需要将 el-menu 的 router 属性设置为true
即可,此时当我们点击二级菜单的时候,就会根据菜单的index
属性进行路由跳转,如:/110
,
但是,使用index id
来作为跳转的路径不合适,我们可以重新绑定index的值为:index="'/'+subItem.path"
,设置时,需要在 path 地址名称前手动补上斜线(“/
”)。
修改前:
将 index 绑定值修改为 path 做为跳转地址:
5. 用户管理
5.1 用户管理概述
通过后台管理用户的账号信息,具体包括用户信息的 展示、添加、修改、删除、角色分配、账号启用 / 注销 等功能。
-
用户信息列表展示
-
添加用户
-
修改用户
-
删除用户
-
启用或禁用用户
-
用户角色分配
5.2 用户管理-列表展示
5.2.1 用户列表布局
页面布局实现:
-
创建 “ 用户列表 ” 链接对应的组件页面
-
在组件目录 conponets 中,新建 user 目录,再在此路径下新建 Users.vue 组件,如图:
-
-
在新组件中创建基本的页面结构:
用户列表组件
3. 通过路由的形式,在右侧主体区展示用户列表
-
在 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 ,在
中添加 default-active 属性,为便于测试,直接将其值写死为 /users
,即default-active="/users"
,效果如下:
如果要把激活项的 index 换成动态的,要如何实现 ???
实现思路:
当我们点击链接时,把对应的地址先保存到 Session Storage 中。当刷新页面(即Home组件刚被创建)时,再把这个值取出来,动态的赋值给 el-menu 中的 default-active 属性 。
第一步:给所有的二级菜单绑定一个 单击事件,命名为 saveNavState
,在单击事件中把 path 值存贮起来:
第二步: 定义 saveNavState 函数
// 保存链接的激活状态
saveNavState(activePath) {
window.sessionStorage.setItem('activePath', activePath)
}
运行程序后,即可保存path 值:
接下来,需要把保存的值取出来
第三步: 在 data 中定义 activePath 来保存数据,默认为空:activePath: ''。再将 activePath 绑定到 的 default-active 属性上。替换原来写死的值/users。
第四步: 动态赋值
当整个 Home 组件一被创建的时候,就立即从 Session Storage 中把值取出来赋给 activePath ;
由于组件被创建时,第一时间执行的是 Created 生命周期函数,所以就直接在 Created 里进行赋值:
created() {
this.getMenuList()
this.activePath = window.sessionStorage.getItem('ctivePath')
}
我们发现,当点击别的链接之后,再次点击 用户列表 时,对应的链接文本并没有高亮,原因是缺少了一步。应该是点击不同链接时,也要为当前的 activePath 重新赋值( saveNavState 点击事件里):
// 保存链接的激活状态
saveNavState(activePath) {
window.sessionStorage.setItem('activePath', activePath)
// 点击链接时,重新赋值
this.activePath = activePath
}
2. 绘制用户列表基本 UI 结构
- 面包屑导航:
el-breadcrumb
- Element-UI 栅格系统基本使用:
el-row
- 表格布局:
el-table
、el-pagination
在 Element UI 官方的 面包屑 导航组件中,找到对应的代码并复制。
打开 Users.vue 文件,将复制好的代码粘贴到该组件文件 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
-
此时,面包屑导航功能完成如下:
绘制卡片视图区域:
在 Element UI 官网 中找到 card卡片 组件,
将复制的代码粘贴到 users.vue 文件中:
{{'列表内容 ' + o }}
整理代码,将不需要的 for 循环和 box-card 类删掉后,结构代码如下:
123
再在 element.js 中按需导入 card 卡片后,效果如下:
目视可见卡片离面包屑导航太近。在 assets 中的 css 目录下,找到 global.css
,为卡片视图区设置上部的间距,这里通过面包屑的类名选择器 el-breadcrumb ,给面包屑增加一个 margin -bottom
值,把卡片盒子挤下来一点:
.el-breadcrumb {
margin-bottom: 15px;
font-size: 12px;
}
保存后,基本的效果如下图:
为卡片视图重置阴影样式。
.el-car {
box-shadow: 0 1px 1px rgba(0, 0 ,0.15) !important;
}
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
-
页码变化事件
-
每页条数变化事件
-
分页条菜单控制
5.2.5 搜索功能
将搜索 关键字,作为 参数 添加到列表查询的参数中。
5.3 用户管理-用户状态控制
-
开关组件的用法
-
接口调用更改用户的状态
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 添加用户表单弹窗布局
-
弹窗组件用法
-
控制弹窗显示和隐藏
5.4.2 表单验证
- 内置 表单验证规则:
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 查询用户信息
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发送删除用户的请求
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.4 权限列表展示
在data
中添加一个rightsList
数据,在methods
中提供一个getRightsList
方法发送请求获取权限列表数据,在created
中调用这个方法获取数据。
一级权限
二级权限
三级权限
完整的添加权限,删除权限,编辑权限功能,这里不再列出,请参照前面编写过的角色管理的代码还有接口文档完成。
获取权限列表数据
// 获取权限列表数据
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.5 角色列表展示
- 调用后台接口获取角色列表数据
- 角色列表展示
// 获取所有角色列表
async getRolesList() {
const { data: res } = await this.$http.get('roles')
if (res.meta.status !== 200) {
return this.$message.error('获取角色列表失败!')
}
this.rolesList = res.data
},
6.6 用户角色分配
6.6.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.6.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.7 角色权限分配
注 解: