前情提要
由于项目要求,主要是项目页面数量过多和流程有些复杂并且不熟悉,需要重构,但是也不能一下子就迭代使用 vue 来开发,最初的版本是使用 jquery + Mustache + jsp 的 随后端一起部署,改成了 jquery + Mustache ,利用 nginx 进行了前端部署。但是这次想要直接 加一个 tab 切换,可以缓存 页面,不要每次点击 导航条都打开新的页面,选择了 vue-admin-template 后台前端解决方案 来解决,在调研过程中 网上看到一种解决方案就是 vue-element-admin 拓展组件: Iframes,下面介绍从头开始利用该框架 融入好原来的系统的过程。当然,由于之前一直没有接触过已经成型的模板框架,所以百度到很多网上这种成形的框架模板,解决的问题和样式各有侧重,选择该种是由于项目选择如此。
框架认识
介绍 vue-element-admin
选择了基础模板中带有权限控制功能的: vue-admin-template 的 permission-control 分支
需求调研
- 登录拦截 : 根据本地登录和远程登录状态,控制是否显示
home
页面 - 权限控制 : 根据后端获取的不同用户的权限,来设置不一样的导航栏
tab
实现 : 点击导航栏二三级目录时候,可以在tab
选项上面记录下来,并且内容切换到当前页面,如果切换到上一个tab
页面时,不需要重新加载全部的页面(包含侧边栏,头部等)。并且以前的项目中集成其他服务器部署的页面也是用了iframe
,所以这边显示内容使用iframe
。iframe
切换保留 : 在每次新打开一个选项卡的时候,需要新建 iframe, 设置 iframe 的显隐,否则,无法保留上次打开选项卡时交互的信息。
选择讨论
- 当前框架比较主流,使用者比较多
- 样式 elementui 可以符合当前迁移之后的样式需求
- 能够找到一些需求的解决方案, 包括 tab 实现、权限控制、iframe 切换保留。
实践过程
原项目逻辑说明
- 技术栈: jqury + Mustache + Bootstrap + Layui
- 登录拦截: 后端利用了 session 来确定用户是否登录,设置了用户最长不操作时长 =》 后端提供了一个接口来 判断用户是否登录状态
- 用户权限获取与加载显示:
- 用户登录
login
之后,可以通过返回值获得用户角色等信息,也可以通过登录之后利用GetUser
接口获取用户的相关信息; - 通过
GetPlatFormName
接口获取平台名称;第三步,获取 平台的样式风格信息…… - 通过 用户的 角色 来获取 第一层级的 导航栏目录,即区分用户的权限, 根据 url 后面的 index 来选中第一层级导航,根据第一层级的 导航 id 来 确定显示哪些下层导航,
- 通过角色来获取第二、三级的导航信息,即区分用户的权限,根据 url 后面的 index 来选中二、三级导航;有一些需要特殊处理,比如第三方链接,需要将 index 加到 a 标签的 href 上面。
- 用户登录
vue-admin-template 逻辑说明
网上资料丰富,先看了看网上资料,基本已经确定怎样修改,其中要注意地方是
这两个参数的作用和这两个判断的作用,其中 token 是 登录之后设置在 cookie 当中的,role 如果没有需要获取,更新路由信息,如果有,就直接根据输入导航走。
参考链接:
- vue-admin-template登录执行逻辑笔记
- vue-admin-template登录功能梳理
- B站 学习视频
前期环境搭建
*由于使用的是 iframe 嵌入原页面,但是原页面会检查 session, session 是在 cookie 里面存放了一个 值 (类似 JSON_SESSION), 在需要验证登录的接口时,需要检验请求的Cookie 是否含有相同的 用户标识,所以本地选择了 nginx 来部署环境,tomcat 本地部署后端环境 *
修改配置环境
vue.config.js
配置如下:根据具体情况配置1
2
3
4
5
6
7
8
9
10proxy: {
[process.env.VUE_APP_BASE_API]: {
target: 'http://localhost:8008/server', // 这个链接是要代理到的 api 地址
changeOrigin: true,
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: ''
}
}
},
// before: require('./mock/mock-server.js')target
.env.development
开发环境一些配置:使用如下:1
2
3
4base api
VUE_APP_BASE_API = '/server'
给 iframe 设置的前缀, 自己添加的, 用户可以根据使用自行添加
VUE_APP_BASE_URL = 'http://localhost:8008'1
return process.env.VUE_APP_BASE_URL + ...
.env.production
生产环境配置:配置以能够运行好为条件。1
2
3
4base api
VUE_APP_BASE_API = '/server'
网上/本地部署的地址,当然 该项目打包之后与 之前前端部署在同一服务器
VUE_APP_BASE_URL = 'http://xxx.xxx.xxx.xxx:xxxx'
vue-admin-template-permission-control 目录介绍
认识vue-admin-template的目录
在这就不详细说明了,自己看链接。
权限控制实现
修改登录接口等
src/api/user.js
: 修改login
接口等接口1
2
3
4
5
6
7export function login(params) {
return request({
url: '/user/login',
method: 'get',
params
})
}src/store/modules/user.js
: 根据需求进行修改state 存储的数据
,actions 中的login\getInfo\logout\resetToken
src/views/login/index.vue
: 其中handleLogin
调用 store 的 login 接口通过接口获取权限,设置相应路由配置
- 需要配置一些 platformname 等信息放置在
src/store/modules/app.js
中,配置相关的state\actions\mutations
- 获取路由信息/权限数据 在
src/store/modules/permission.js
中generateRoutes
中,本次配置大体如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185const actions = {
generateRoutes({ commit, state }, data) {
return new Promise((resolve, reject) => {
const loadMenuData = []
const { funcs, roles } = data // 其中 funcs 就是获取的后端控制的侧边栏数据
// 先查询后台并返回左侧菜单数据并把数据添加到路由
// 映射 组件
if (!funcs || funcs.length <= 0) {
Message.error('菜单数据加载异常')
reject()
}
Object.assign(loadMenuData, funcs) // 深度拷贝
let a = []
generaMenu(a, loadMenuData) // 组装路由 和 componet
let accessedRoutes
accessedRoutes = a || []
commit('SET_ROUTES', accessedRoutes) // 设置路由
resolve(accessedRoutes)
})
}
}
/**
* 后台查询的菜单数据拼装成路由格式的数据,
* 总共两级
* 函数较为复杂,主要是由于原先 的 配置怎么能够统一进行 转为 路由配置 的处理
* @param routes
*/
export function generaMenu(routes, data) {
var set = {}
var len = routes.length
data.forEach((item, index) => {
var { root, child } = item
var menu = null
/**
* a. 第三方直接跳转 http://www.baidu.com
* - 一级
* - 二级
* b. iframe
* tips(必须知晓):
* 动态路由创建由 url 来决定,其中需要进行分割
* 如果在同一级出现重复者会自动加上 1, 2
* 例如: /a /a1 /a2
* /a/b /a/b1
*/
// 一级
if (isExternal(root.url)) {
menu = {
path: '/third', // + root.name,
component: Layout,
children: [
{
path: root.url,
meta: { title: root.name, rootFunction: root.rootFunction, icon: root.icon }
}
],
meta: {
third: true
}
}
} else {
var isFather = false
if (root.url === '#') { // 特殊字符
isFather = true
if (child.length > 0) {
if (isExternal(child[0].url)) { // 自带在 utils 工具里面
root.url = '/a/a/third/third'
} else {
root.url = child[0].url
}
} else {
// 只有一个一级菜单,没有二级, 自动分配一个路由
root.url = '/a/a/a/a'
}
}
// 一级
var paths = getPath(root.url)
menu = {
path: '/' + paths[3],
component: Layout,
redirect: '/' + paths[3] + '/' + paths[4],
children: [
{
path: paths[4],
component: () => import('@/views/iframes/index'),
meta: {
title: root.name,
url: root.url + '?index=' + root.indexMy,
iframe: true,
indexmy: root.indexMy
}
}
],
meta: {
title: root.name,
icon: root.icon,
rootFunction: root.rootFunction,
url: root.url + '?index=' + root.indexMy,
iframe: true,
indexmy: root.indexMy
}
}
if (isFather) {
menu.alwaysShow = true
if (menu.path === '/a' && child.length <= 0) {
menu.children = [] // 只有一个一级菜单,没有二级
}
}
if (isIn(set, menu.path, index)) {
menu.path += index
}
}
// 二级
if (child.length > 0) {
menu.children = []
child.forEach((r, i) => {
var childmenu = {}
var cpaths = getPath(r.url)
if (isExternal(r.url)) {
// 默认路由,如果重复,重新赋值
cpaths = ['', '', '', '', 'third']
childmenu = {
path: cpaths[4],
component: Layout,
children: [
{
path: r.url,
meta: {
title: r.name, rootFunction: r.rootFunction,
// icon: r.icon
}
}
],
meta: {
third: true
}
}
} else {
if (cpaths[3] === 'ccc') { // 特殊控制
r.url = 'http://xxx.xxx.xxx.xxx:xxxx/ccc#/' + cpaths[4]
}
childmenu = {
path: cpaths[4],
component: () => import(`@/views/iframes/index`),
meta: {
title: r.name,
url: r.url + '?index=' + r.indexMy,
iframe: true,
indexmy: r.indexMy
}
}
}
if (isIn(set, menu.path + '/' + childmenu.path, i)) {
childmenu.path += i
}
const t = childmenu
menu.children.push(t)
})
// 一级的 redirect 也需要修改, 需要避开 children 中有 第三方跳转的子项
menu.redirect = menu.path + '/' + menu.children[0].path
}
const tmp = menu
routes.push(tmp)
})
const menu = {
path: '/',
component: Layout,
redirect: routes[len].path
}
routes.unshift(menu)
function isIn(set, path, index) {
if (set[path]) {
set[path + index] = 1
return true
}
set[path] = 1
return false
}
function getPath(url) { // 处理某些有问题的短 path , 需要有 四个/, 以便统一处理
var curl = url.split('.html')[0];
if ('/xxx/xxx' === curl) {
curl = '/cc/ccc/cc/ccc'
}
return curl.split('/')
}
} src/permission.js
中调用相关的actions
如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54...
// determine whether the user has obtained his permission roles through getInfo
const hasRoles = store.getters.roles && store.getters.roles.length > 0 && store.getters.roles[0].length > 0
if (hasRoles) {
next()
} else {
try {
const user = await store.dispatch('user/getInfo')
// 用 session 来判断
const session = await store.dispatch('user/session')
if (session === 'fail' || session.user === null) {
next({ path: '/login' })
NProgress.done()
} else {
// 获取平台一些参数
await store.dispatch('app/getPlatFormName')
await store.dispatch('app/getFuncs', user.roleId) // 获取 菜单权限
await store.dispatch('app/getFuncActive', store.getters.active) // 默认情况下的菜单选中
var accessRoutes = await store.dispatch('permission/generateRoutes', { funcs: store.state.app.funcs, roles: [user.roleName] })
// 找到 to 的 顶部 选中
const ps = to.path.split('/')
if (ps[1].length > 0) { // to is not "/"
// 检查 to 是否合法, 如果不在,to 改成 默认显示的第一页
let isIn = false
for (let i = 0; i < accessRoutes.length; i++) {
if (accessRoutes[i].path.split('/')[1] === ps[1]) {
if (ps.length === 2) {
isIn = true
} else if (accessRoutes[i].children.length > 0 && ps.length > 2 && ps[2].length > 0) {
for (let j = 0; j < accessRoutes[i].children.length; j++) {
if (ps[2] === accessRoutes[i].children[j].path) {
// 合法
isIn = true
break
}
}
}
if (isIn) {
ps[0] = accessRoutes[i].meta.rootFunction
break
}
}
}
if (ps[0] !== '' && isIn) {
await store.dispatch('app/setNavTopActive', ps[0])
}
if (!isIn) {
to = { path: '/' }
}
}
// dynamically add accessible routes
resetRouter()
router.addRoutes(accessRoutes)
...
tab 实现
修改网上的已有组件 TagsView
- 先将网上的 down 下来,调出正常效果:添加 tagsview 动态 tabs 页
- 修改样式,删除一些右击效果,不需要,剩余添加、删除单个、滚动功能。
控制 iframe 的删除
中 ```delView``` 删除 tab 操作时,通知 iframe 组件,删除 iframe, 以便 iframe 下次重新创建: 1
2
3
4
5
6
7
8
9
10
11
12```javascript
delView({ dispatch, state }, view) {
return new Promise(resolve => {
dispatch('delVisitedView', view)
dispatch('delCachedView', view)
dispatch('iframes/delIframe', view.title, { root: true }) // view.title 是特殊标识, 调用其他组件需要加 {root: true}
resolve({
visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews]
})
})
},
iframe 切换保留实现
修改网上的已有组件 iframes 组件:
- 点击链接 , 但是当前仓库拥有者已经不设置可开放了,所以需要自己看看
=》 csdn
先保持实现效果。 - 由于使用了动态路由,所以不可能每一个页面都手动创建一个 vue 文件来存放,这里使用了 一个 iframe 修改如下:
src/layout/components/AppMain.vue
代码如下:1
2</transition>
<iframes v-if="iframesEnable" />src/layout/components/iframes/index.vue
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<template>
<div
v-show="iframeBoxState"
class="iframe-box">
<iframe-container
v-for="( value, index ) in iframeArr"
v-show="value.show"
:key="index"
:item="value"
:index="index"
frameborder="0"
width="100%"
height="100%"
/>
</div>
</template>
<!-- 需要引入 iframeContainer -->src/layout/components/iframeContainer/index.vue
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79<template>
<iframe class="iframe" :ref="'iframe' + index"
:src="src"
:name="item.name"
v-loading.fullscreen.lock="fullscreenLoading" @load="loaded"></iframe>
</template>
<script>
import { isExternal } from '@/utils/validate'
export default {
name: 'IframeContainer',
props: {
index: {
type: [Number, String],
required: true
},
item: {
type: Object,
default: {},
required: true
}
},
data() {
return {
fullscreenLoading: false,
iframeWin: {}
}
},
created() {
this.fullscreenLoading = true
},
mounted() {
this.iframeInit()
window.onresize = () => {
this.iframeInit()
}
this.iframeWin = this.$refs['iframe' + this.index].contentWindow
},
components: {},
computed: {
src () {
if (isExternal(this.item.src)) {
return this.item.src + this.item.params
}
return process.env.VUE_APP_BASE_URL + this.item.src + this.item.params
},
methods: {
iframeInit() {
const iframe = this.$refs['iframe' + this.index]
const clientHeight = document.documentElement.clientHeight - 45 - 40
iframe.style.height = `${clientHeight}px`
if (iframe.attachEvent) {
iframe.attachEvent('onload', () => {
this.fullscreenLoading = false
})
} else {
iframe.onload = () => {
this.fullscreenLoading = false
}
}
},
loaded () {
const cookie = document.cookie
this.iframeWin.postMessage(cookie, this.src)
}
}
}
</script>
<style>
.iframe {
width: 100%;
height: 100%;
border: 0;
overflow: hidden;
box-sizing: border-box;
}
</style>src/store/modules/iframes.js
代码如下:1
2
3
4
5
6
7
8
9
10
11// mutations
DEL_IFRAME(state, id) {
Vue.delete(state, id)
},
// actions
delIframe({ commit }, id) {
// console.log('----- delIframe')
commit('HIDE_IFRAME_BOX')
commit('DEL_IFRAME', id)
commit('SHOW_IFRAME_BOX')
},src/utils/iframes/iframes-mixin.js
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// import { PAGE_LIST } from './iframes-config'
// methods
created() {
// console.log('----- created')
if (this.PAGE_ID == null || this.PAGE_ID.length <= 0) {
this.MISSING_ID = true
console.error('缺少 PAGE_ID 参数')
return
}
const obj = { id: this.PAGE_ID, src: this.PAGE_SRC }
if (this.PARAMS) obj.params = this.PARAMS
this.createdPage(obj)
const params = this.iframes[this.PAGE_ID].params
if (params != null && this.PARAMS != null && params !== this.PARAMS) {
this.changeParams({ id: this.PAGE_ID, params: this.PARAMS })
}
this.activatedPage(this.PAGE_ID) // 由于 没有使用 keep-alive ,所以 activated 和 deactivated 事件不会触发,放到 created 和 beforeDestroy 里面
},
activated() {
// console.log('----- activated')
if (this.MISSING_ID) return
const params = this.iframes[this.PAGE_ID].params
if (params != null && this.PARAMS != null && params !== this.PARAMS) {
this.changeParams({ id: this.PAGE_ID, params: this.PARAMS })
}
this.activatedPage(this.PAGE_ID)
},
deactivated() {
// console.log('----- deactivated')
if (this.MISSING_ID) return
this.deactivatedPage(this.PAGE_ID)
},
beforeDestroy() {
// console.log('----- beforeDestroy')
if (this.MISSING_ID) return
this.deactivatedPage(this.PAGE_ID)
// this.beforeDestroyPage(this.PAGE_ID)
},src/views/iframes/index.vue
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<template>
<div class="app-container bg-white"> </div>
</template>
<script>
import iframeMixin from '@/utils/iframes/iframes-mixin'
// 百度
export default {
// name: this.$route.meta.title,
mixins: [iframeMixin],
data() {
return {
PAGE_ID: this.$route.meta.title,
PAGE_SRC: this.$route.meta.url
}
}
}
</script>理解好
IFRAME_BOX_STATE
和 某页的enable: false, show: false
的作用
ps: 原项目页面统一配置了 iframe 根据 origin 来配置 是否显示 头部和侧边栏,以防止页面重复,类似如下:
1 | // 添加 iframe 监听事件 |
实践难点(注意要点)
相信正常能够写 vue 项目的前端选手肯定可以上来就理清 vue-admin-template 的文件结构,以及一些配置,主要不知道的是,其中登录和权限验证这里的逻辑,所以可能会导致多花很多时间在这里,当然其实有条理的慢慢来还是能够很快知道其中的过程的,然后融合原项目的登录逻辑即可完成任务。
其中,iframe 的解决方法也是颇费了一番功夫,其中 利用 vuex 来存储 iframe 的切换信息,在理解他人写的代码的时候需要仔细。
总结
所以针对这种问题,对前端人员还是很有考验性的,需要确定好需求或者是实现效果,然后确定前端的解决办法,哪些才是真正可行并且效果较好的解决办法,有些需要一个个自己手动尝试才能知道,所以总体流程不能乱,也不要急,最终完美解决。
发布时间: 2022-05-09
最后更新: 2023-05-09
本文标题: 利用 Vue-admin-template 框架 - 实践
本文链接: https://zx1001011.github.io/2022/05/09/vue-element-template-use1/
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可。转载请注明出处!