xs-common-plugins 1.1.7 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/package.json +1 -1
- package/src/common/common.js +37 -10
- package/src/components/Search/index.vue +5 -1
- package/src/components/Search/product_option/index.scss +1 -1
- package/src/components/Search/product_option/index.vue +1 -8
- package/src/components/Search/product_option/methods.js +16 -4
- package/src/router/index.js +12 -1
- package/src/store/index.js +2 -0
- package/src/store/modules/tagsView.js +160 -0
- package/src/utils/filterRules.js +10 -5
- package/src/views/layout/components/AppMain.vue +2 -2
- package/src/views/layout/components/Navbar.vue +2 -2
- package/src/views/layout/components/TagsView/ScrollPane.vue +94 -0
- package/src/views/layout/components/TagsView/index.vue +291 -0
- package/src/views/layout/components/index.js +1 -0
- package/src/views/layout/index.vue +4 -2
- package/src/views/redirect/index.vue +12 -0
package/README.md
CHANGED
|
@@ -278,4 +278,22 @@
|
|
|
278
278
|
```
|
|
279
279
|
1.1.7
|
|
280
280
|
1. 多余显示移除
|
|
281
|
+
```
|
|
282
|
+
```
|
|
283
|
+
1.1.8
|
|
284
|
+
1. common.js 增加 方法
|
|
285
|
+
2. common.js commom.format() 拓展类型枚举
|
|
286
|
+
3. 表单校验增加 min, max 参数
|
|
287
|
+
```
|
|
288
|
+
```
|
|
289
|
+
1.2.0
|
|
290
|
+
1. 项目增加多页签功能, 方便切换
|
|
291
|
+
```
|
|
292
|
+
```
|
|
293
|
+
1.2.1
|
|
294
|
+
1. 权益项目 渠道组件 商品组件修改
|
|
295
|
+
```
|
|
296
|
+
```
|
|
297
|
+
1.2.2
|
|
298
|
+
1. 组件样式
|
|
281
299
|
```
|
package/package.json
CHANGED
package/src/common/common.js
CHANGED
|
@@ -5,7 +5,7 @@ import store from '@/store/index'
|
|
|
5
5
|
import {OrgEnum} from '@/utils/enum'
|
|
6
6
|
import filterRules from '@/utils/filterRules'
|
|
7
7
|
import request from "xs-request";
|
|
8
|
-
|
|
8
|
+
import moduleCfg from '@/modules/module.config.js'
|
|
9
9
|
const common = {}
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -400,28 +400,45 @@ common.requiredList = (list) => {
|
|
|
400
400
|
/**
|
|
401
401
|
* 格式化数据样式
|
|
402
402
|
* @param {*} value 值
|
|
403
|
-
* @param {*} type 要格式化的类型 [date, price]
|
|
403
|
+
* @param {*} type 要格式化的类型 [date, price, price, money, enum, list(前端自定义枚举)]
|
|
404
404
|
* @returns
|
|
405
405
|
*/
|
|
406
406
|
common.format = (value, type="date") => {
|
|
407
|
-
|
|
407
|
+
let newType = ['money', 'date', 'mdate'].includes(type) ? type : type.split('.').length > 1 ? 'enum' : 'list'
|
|
408
|
+
switch (newType) {
|
|
409
|
+
case 'money':
|
|
410
|
+
return Number(value).toFixed(4);
|
|
408
411
|
case 'date':
|
|
409
|
-
if(value) return
|
|
410
|
-
|
|
411
|
-
|
|
412
|
+
if(value === '1970-01-01T00:00:00') return '/'
|
|
413
|
+
return value ? value.replace('T', ' ').substr(0, 19) : ''
|
|
414
|
+
case 'mdate':
|
|
415
|
+
if(value === '1970-01-01T00:00:00') return '/'
|
|
416
|
+
return value ? value.replace('T', ' ').substr(5, 19) : ''
|
|
412
417
|
case 'price':
|
|
413
418
|
if(typeof(value) === 'number') {
|
|
414
419
|
return value.toFixed(2).toString().replace(/,/g,'').replace(/\d+/, function (n) { // 先提取整数部分
|
|
415
420
|
return n.replace(/(\d)(?=(\d{3})+$)/g, function ($1) { // 对整数部分添加分隔符
|
|
416
|
-
|
|
421
|
+
return $1 + ",";
|
|
417
422
|
});
|
|
418
423
|
});
|
|
419
424
|
}
|
|
420
425
|
else return ''
|
|
421
|
-
|
|
426
|
+
case 'enum':
|
|
427
|
+
return getValueByType('enum', type, value)
|
|
428
|
+
case 'list':
|
|
429
|
+
return getValueByType('list', type, value)
|
|
422
430
|
default:
|
|
423
431
|
break;
|
|
424
432
|
}
|
|
433
|
+
function getValueByType (type, name, value) {
|
|
434
|
+
if(type === 'enum') {
|
|
435
|
+
let row = OrgEnum[name].find(item => item.Id == value)
|
|
436
|
+
return row ? row.Text : ''
|
|
437
|
+
} else if(type === 'list') {
|
|
438
|
+
let row = moduleCfg.format ? moduleCfg.format[name].find(item => item.id == value) : null
|
|
439
|
+
return row ? row.name: ''
|
|
440
|
+
}
|
|
441
|
+
}
|
|
425
442
|
}
|
|
426
443
|
|
|
427
444
|
/**
|
|
@@ -476,7 +493,7 @@ common.getReportList = (that, url, query, callBack) => {
|
|
|
476
493
|
|
|
477
494
|
|
|
478
495
|
/**
|
|
479
|
-
* 字典表 里通过Id 查 name, 或者查询多条数据
|
|
496
|
+
* 字典表 里通过Id 查 name, 或者查询多条数据 (单条返回 name, 多条返回数组)
|
|
480
497
|
* @param {Array} props ['服务名', '表名', '字段名']
|
|
481
498
|
* @param {String, Number, Array} val 值, 传入id, 或者 [id1, id2, ...]
|
|
482
499
|
*
|
|
@@ -518,5 +535,15 @@ common.dic = async (props, val) => {
|
|
|
518
535
|
}
|
|
519
536
|
}
|
|
520
537
|
}
|
|
521
|
-
|
|
538
|
+
/**
|
|
539
|
+
* 快速给页面变量赋值
|
|
540
|
+
* @param {*} url 请求接口的地址
|
|
541
|
+
* @param {*} query 接口参数
|
|
542
|
+
* @param {*} prop 要为哪个字段名赋值
|
|
543
|
+
*/
|
|
544
|
+
common.getLabelList = (that, prop, url, query) => {
|
|
545
|
+
common.getObject(url)(query).then(res => {
|
|
546
|
+
that[prop] = res.data
|
|
547
|
+
})
|
|
548
|
+
}
|
|
522
549
|
export default common
|
|
@@ -150,6 +150,10 @@
|
|
|
150
150
|
<HKCascader v-model="inputValue[optionItem.fieldName]" :placeholder="optionItem.defaultValue"
|
|
151
151
|
:style="{ width: optionItem.itemWidth }" :optionItem="optionItem"></HKCascader>
|
|
152
152
|
</el-form-item>
|
|
153
|
+
<el-form-item :label="optionItem.displayName" v-if="optionItem.component "
|
|
154
|
+
:style="{ width: optionItem.itemWidth }" class="dateRange">
|
|
155
|
+
<component :is="optionItem.component" v-model="inputValue[optionItem.fieldName]" :placeholder="optionItem.defaultValue" v-bind="optionItem" />
|
|
156
|
+
</el-form-item>
|
|
153
157
|
</div>
|
|
154
158
|
|
|
155
159
|
<el-form-item v-if="!!getBtnContentIsOut().length">
|
|
@@ -173,7 +177,7 @@
|
|
|
173
177
|
</div>
|
|
174
178
|
<slot name="search_custemBtn" />
|
|
175
179
|
<!-- <div class="buttonBox">
|
|
176
|
-
|
|
180
|
+
|
|
177
181
|
<el-form-item v-if="!!getBtnContentIsOut().length">
|
|
178
182
|
<el-dropdown
|
|
179
183
|
trigger="click"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<span class="title">商品品类</span>
|
|
9
9
|
</div>
|
|
10
10
|
<div class="body">
|
|
11
|
-
<el-radio-group class="product-type" v-model="selectedType" size="mini">
|
|
11
|
+
<el-radio-group class="product-type" v-model="selectedType" size="mini" @change="changeProductType">
|
|
12
12
|
<el-radio
|
|
13
13
|
v-for="typeItem in $enumList.EnumProductType"
|
|
14
14
|
:key="typeItem.Id"
|
|
@@ -391,13 +391,6 @@ export default {
|
|
|
391
391
|
mixins: [MIX_VMODEL],
|
|
392
392
|
components: {},
|
|
393
393
|
watch: {
|
|
394
|
-
selectedType: {
|
|
395
|
-
handler(nVal) {
|
|
396
|
-
this.loadSource();
|
|
397
|
-
},
|
|
398
|
-
immediate: true
|
|
399
|
-
},
|
|
400
|
-
|
|
401
394
|
selectedAreaCode: {
|
|
402
395
|
handler(nVal) {
|
|
403
396
|
console.log(nVal, "=====>");
|
|
@@ -7,7 +7,10 @@ export default {
|
|
|
7
7
|
},
|
|
8
8
|
mountedInit() {
|
|
9
9
|
},
|
|
10
|
-
|
|
10
|
+
// 切换商品品类
|
|
11
|
+
changeProductType (value) {
|
|
12
|
+
this.loadSource();
|
|
13
|
+
},
|
|
11
14
|
checkboxHandle(event, fieldName) {
|
|
12
15
|
this.keys = []
|
|
13
16
|
this.checkboxForm = {
|
|
@@ -36,12 +39,21 @@ export default {
|
|
|
36
39
|
typeId: parseInt(this.selectedType)
|
|
37
40
|
}).then(res => {
|
|
38
41
|
if (res.data) {
|
|
39
|
-
this.areaCodeDicList = res.data.areaCodeDic
|
|
40
|
-
this.arsIdDicList = res.data.arsIdDic
|
|
41
|
-
this.denomIncList = res.data.denomInc
|
|
42
|
+
this.areaCodeDicList = res.data.areaCodeDic // 地区/种类列表
|
|
43
|
+
this.arsIdDicList = res.data.arsIdDic // 运营商类目列表
|
|
44
|
+
this.denomIncList = res.data.denomInc // 面值列表
|
|
42
45
|
} else {
|
|
43
46
|
this.areaCodeDicList = this.arsIdDicList = this.denomIncList = []
|
|
44
47
|
}
|
|
48
|
+
let list = []
|
|
49
|
+
this.areaCodeDicList.forEach(item => { list.push([item.value]) });
|
|
50
|
+
this.selectedAreaCode = list // selectedAreaCode 地区/种类
|
|
51
|
+
this.selectedArsId = this.formatData(this.arsIdDicList) // selectedArsId 运营商类目
|
|
52
|
+
this.selectedDenomInc = Object.values(this.denomIncList) // selectedDenomInc 面值
|
|
53
|
+
this.checkboxHandle()
|
|
45
54
|
})
|
|
55
|
+
},
|
|
56
|
+
formatData (list) {
|
|
57
|
+
return list.map(item => item.value)
|
|
46
58
|
}
|
|
47
59
|
}
|
package/src/router/index.js
CHANGED
|
@@ -8,6 +8,17 @@ VueRouter.prototype.push = function push(location) {
|
|
|
8
8
|
return originalPush.call(this, location).catch(err => err)
|
|
9
9
|
}
|
|
10
10
|
const routes = [
|
|
11
|
+
{
|
|
12
|
+
path: '/redirect',
|
|
13
|
+
component: () => import('../views/layout/index'),
|
|
14
|
+
hidden: true,
|
|
15
|
+
children: [
|
|
16
|
+
{
|
|
17
|
+
path: '/redirect/:path(.*)',
|
|
18
|
+
component: () => import('@/views/redirect/index')
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
11
22
|
{
|
|
12
23
|
path: '/',
|
|
13
24
|
name: 'layout',
|
|
@@ -20,7 +31,7 @@ const routes = [
|
|
|
20
31
|
path: '/home',
|
|
21
32
|
name: 'home',
|
|
22
33
|
component: () => import('../views/layout/index'),
|
|
23
|
-
meta: { title: '首页', icon: 'example' },
|
|
34
|
+
meta: { title: '首页', icon: 'example', affix: true,},
|
|
24
35
|
children: [{
|
|
25
36
|
path:'/',
|
|
26
37
|
component: () => import('../views/home/index'),
|
package/src/store/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import app from './modules/app'
|
|
|
5
5
|
import user from './modules/user'
|
|
6
6
|
import widgetdata from './modules/widgetdata'
|
|
7
7
|
import dic from './modules/dic'
|
|
8
|
+
import tagsView from './modules/tagsView'
|
|
8
9
|
import oss from './modules/oss'
|
|
9
10
|
|
|
10
11
|
const globalCfg = require("../modules/module.config");
|
|
@@ -44,6 +45,7 @@ let storeCfg = {
|
|
|
44
45
|
|
|
45
46
|
modules: {
|
|
46
47
|
app,
|
|
48
|
+
tagsView,
|
|
47
49
|
user,
|
|
48
50
|
widgetdata,
|
|
49
51
|
dic,
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const state = {
|
|
2
|
+
visitedViews: [],
|
|
3
|
+
cachedViews: []
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const mutations = {
|
|
7
|
+
ADD_VISITED_VIEW: (state, view) => {
|
|
8
|
+
if (state.visitedViews.some(v => v.path === view.path)) return
|
|
9
|
+
state.visitedViews.push(
|
|
10
|
+
Object.assign({}, view, {
|
|
11
|
+
title: view.meta.title || 'no-name'
|
|
12
|
+
})
|
|
13
|
+
)
|
|
14
|
+
},
|
|
15
|
+
ADD_CACHED_VIEW: (state, view) => {
|
|
16
|
+
if (state.cachedViews.includes(view.name)) return
|
|
17
|
+
if (!view.meta.noCache) {
|
|
18
|
+
state.cachedViews.push(view.name)
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
DEL_VISITED_VIEW: (state, view) => {
|
|
23
|
+
for (const [i, v] of state.visitedViews.entries()) {
|
|
24
|
+
if (v.path === view.path) {
|
|
25
|
+
state.visitedViews.splice(i, 1)
|
|
26
|
+
break
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
DEL_CACHED_VIEW: (state, view) => {
|
|
31
|
+
const index = state.cachedViews.indexOf(view.name)
|
|
32
|
+
index > -1 && state.cachedViews.splice(index, 1)
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
DEL_OTHERS_VISITED_VIEWS: (state, view) => {
|
|
36
|
+
state.visitedViews = state.visitedViews.filter(v => {
|
|
37
|
+
return v.meta.affix || v.path === view.path
|
|
38
|
+
})
|
|
39
|
+
},
|
|
40
|
+
DEL_OTHERS_CACHED_VIEWS: (state, view) => {
|
|
41
|
+
const index = state.cachedViews.indexOf(view.name)
|
|
42
|
+
if (index > -1) {
|
|
43
|
+
state.cachedViews = state.cachedViews.slice(index, index + 1)
|
|
44
|
+
} else {
|
|
45
|
+
// if index = -1, there is no cached tags
|
|
46
|
+
state.cachedViews = []
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
DEL_ALL_VISITED_VIEWS: state => {
|
|
51
|
+
// keep affix tags
|
|
52
|
+
const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
|
|
53
|
+
state.visitedViews = affixTags
|
|
54
|
+
},
|
|
55
|
+
DEL_ALL_CACHED_VIEWS: state => {
|
|
56
|
+
state.cachedViews = []
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
UPDATE_VISITED_VIEW: (state, view) => {
|
|
60
|
+
for (let v of state.visitedViews) {
|
|
61
|
+
if (v.path === view.path) {
|
|
62
|
+
v = Object.assign(v, view)
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const actions = {
|
|
70
|
+
addView({ dispatch }, view) {
|
|
71
|
+
dispatch('addVisitedView', view)
|
|
72
|
+
dispatch('addCachedView', view)
|
|
73
|
+
},
|
|
74
|
+
addVisitedView({ commit }, view) {
|
|
75
|
+
commit('ADD_VISITED_VIEW', view)
|
|
76
|
+
},
|
|
77
|
+
addCachedView({ commit }, view) {
|
|
78
|
+
commit('ADD_CACHED_VIEW', view)
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
delView({ dispatch, state }, view) {
|
|
82
|
+
return new Promise(resolve => {
|
|
83
|
+
dispatch('delVisitedView', view)
|
|
84
|
+
dispatch('delCachedView', view)
|
|
85
|
+
resolve({
|
|
86
|
+
visitedViews: [...state.visitedViews],
|
|
87
|
+
cachedViews: [...state.cachedViews]
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
delVisitedView({ commit, state }, view) {
|
|
92
|
+
return new Promise(resolve => {
|
|
93
|
+
commit('DEL_VISITED_VIEW', view)
|
|
94
|
+
resolve([...state.visitedViews])
|
|
95
|
+
})
|
|
96
|
+
},
|
|
97
|
+
delCachedView({ commit, state }, view) {
|
|
98
|
+
return new Promise(resolve => {
|
|
99
|
+
commit('DEL_CACHED_VIEW', view)
|
|
100
|
+
resolve([...state.cachedViews])
|
|
101
|
+
})
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
delOthersViews({ dispatch, state }, view) {
|
|
105
|
+
return new Promise(resolve => {
|
|
106
|
+
dispatch('delOthersVisitedViews', view)
|
|
107
|
+
dispatch('delOthersCachedViews', view)
|
|
108
|
+
resolve({
|
|
109
|
+
visitedViews: [...state.visitedViews],
|
|
110
|
+
cachedViews: [...state.cachedViews]
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
},
|
|
114
|
+
delOthersVisitedViews({ commit, state }, view) {
|
|
115
|
+
return new Promise(resolve => {
|
|
116
|
+
commit('DEL_OTHERS_VISITED_VIEWS', view)
|
|
117
|
+
resolve([...state.visitedViews])
|
|
118
|
+
})
|
|
119
|
+
},
|
|
120
|
+
delOthersCachedViews({ commit, state }, view) {
|
|
121
|
+
return new Promise(resolve => {
|
|
122
|
+
commit('DEL_OTHERS_CACHED_VIEWS', view)
|
|
123
|
+
resolve([...state.cachedViews])
|
|
124
|
+
})
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
delAllViews({ dispatch, state }, view) {
|
|
128
|
+
return new Promise(resolve => {
|
|
129
|
+
dispatch('delAllVisitedViews', view)
|
|
130
|
+
dispatch('delAllCachedViews', view)
|
|
131
|
+
resolve({
|
|
132
|
+
visitedViews: [...state.visitedViews],
|
|
133
|
+
cachedViews: [...state.cachedViews]
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
},
|
|
137
|
+
delAllVisitedViews({ commit, state }) {
|
|
138
|
+
return new Promise(resolve => {
|
|
139
|
+
commit('DEL_ALL_VISITED_VIEWS')
|
|
140
|
+
resolve([...state.visitedViews])
|
|
141
|
+
})
|
|
142
|
+
},
|
|
143
|
+
delAllCachedViews({ commit, state }) {
|
|
144
|
+
return new Promise(resolve => {
|
|
145
|
+
commit('DEL_ALL_CACHED_VIEWS')
|
|
146
|
+
resolve([...state.cachedViews])
|
|
147
|
+
})
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
updateVisitedView({ commit }, view) {
|
|
151
|
+
commit('UPDATE_VISITED_VIEW', view)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default {
|
|
156
|
+
namespaced: true,
|
|
157
|
+
state,
|
|
158
|
+
mutations,
|
|
159
|
+
actions
|
|
160
|
+
}
|
package/src/utils/filterRules.js
CHANGED
|
@@ -2,11 +2,16 @@ const filterRules = ({ required, type, min, max }) => {
|
|
|
2
2
|
let validate = [];
|
|
3
3
|
if (required) {
|
|
4
4
|
// 必填项
|
|
5
|
-
validate.push({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
validate.push({ required: true, message: "该输入项为必填项", trigger: "change",});
|
|
6
|
+
}
|
|
7
|
+
if(min&&max){
|
|
8
|
+
validate.push({ min:min,max:max, message: '字符长度在'+min+'至'+max+'之间!', trigger: 'change' })
|
|
9
|
+
}
|
|
10
|
+
if(min){
|
|
11
|
+
validate.push({ min:min, message: '最少输入'+min+'个字符!', trigger: 'change' })
|
|
12
|
+
}
|
|
13
|
+
if(max){
|
|
14
|
+
validate.push({ min:1,max:max, message: '最多输入'+max+'个字符!', trigger: 'change' })
|
|
10
15
|
}
|
|
11
16
|
if (type) {
|
|
12
17
|
let message = "";
|
|
@@ -25,10 +25,10 @@ export default {
|
|
|
25
25
|
.app-main {
|
|
26
26
|
display: flex;
|
|
27
27
|
/*50 = navbar */
|
|
28
|
-
height: calc(100vh -
|
|
28
|
+
height: calc(100vh - 85px);
|
|
29
29
|
width: 100%;
|
|
30
30
|
position: relative;
|
|
31
|
-
overflow:
|
|
31
|
+
overflow: auto;
|
|
32
32
|
}
|
|
33
33
|
.footer {
|
|
34
34
|
position: absolute;
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
</el-tag>
|
|
65
65
|
</div>
|
|
66
66
|
<!-- 租户 -->
|
|
67
|
-
<div class="tenant" v-if="userProfile.TId">
|
|
67
|
+
<div class="tenant" v-if="userProfile && userProfile.TId">
|
|
68
68
|
<el-dropdown @command="changeTenant">
|
|
69
69
|
<span class="el-dropdown-link">
|
|
70
70
|
{{tenantList && tenantList[userProfile.TId]}}
|
|
@@ -161,7 +161,7 @@ export default {
|
|
|
161
161
|
if (userName === "admin") {
|
|
162
162
|
this.LoginName = true;
|
|
163
163
|
}
|
|
164
|
-
if(this.userProfile.TId) this.getTenantList()
|
|
164
|
+
if(this.userProfile &&this.userProfile.TId) this.getTenantList()
|
|
165
165
|
},
|
|
166
166
|
methods: {
|
|
167
167
|
changeTenant (id) {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
|
|
3
|
+
<slot />
|
|
4
|
+
</el-scrollbar>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script>
|
|
8
|
+
const tagAndTagSpacing = 4 // tagAndTagSpacing
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
name: 'ScrollPane',
|
|
12
|
+
data() {
|
|
13
|
+
return {
|
|
14
|
+
left: 0
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
computed: {
|
|
18
|
+
scrollWrapper() {
|
|
19
|
+
return this.$refs.scrollContainer.$refs.wrap
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
mounted() {
|
|
23
|
+
this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
|
|
24
|
+
},
|
|
25
|
+
beforeDestroy() {
|
|
26
|
+
this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
|
|
27
|
+
},
|
|
28
|
+
methods: {
|
|
29
|
+
handleScroll(e) {
|
|
30
|
+
const eventDelta = e.wheelDelta || -e.deltaY * 40
|
|
31
|
+
const $scrollWrapper = this.scrollWrapper
|
|
32
|
+
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
|
|
33
|
+
},
|
|
34
|
+
emitScroll() {
|
|
35
|
+
this.$emit('scroll')
|
|
36
|
+
},
|
|
37
|
+
moveToTarget(currentTag) {
|
|
38
|
+
const $container = this.$refs.scrollContainer.$el
|
|
39
|
+
const $containerWidth = $container.offsetWidth
|
|
40
|
+
const $scrollWrapper = this.scrollWrapper
|
|
41
|
+
const tagList = this.$parent.$refs.tag
|
|
42
|
+
|
|
43
|
+
let firstTag = null
|
|
44
|
+
let lastTag = null
|
|
45
|
+
|
|
46
|
+
// find first tag and last tag
|
|
47
|
+
if (tagList.length > 0) {
|
|
48
|
+
firstTag = tagList[0]
|
|
49
|
+
lastTag = tagList[tagList.length - 1]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (firstTag === currentTag) {
|
|
53
|
+
$scrollWrapper.scrollLeft = 0
|
|
54
|
+
} else if (lastTag === currentTag) {
|
|
55
|
+
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
|
|
56
|
+
} else {
|
|
57
|
+
// find preTag and nextTag
|
|
58
|
+
const currentIndex = tagList.findIndex(item => item === currentTag)
|
|
59
|
+
const prevTag = tagList[currentIndex - 1]
|
|
60
|
+
const nextTag = tagList[currentIndex + 1]
|
|
61
|
+
|
|
62
|
+
// the tag's offsetLeft after of nextTag
|
|
63
|
+
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
|
|
64
|
+
|
|
65
|
+
// the tag's offsetLeft before of prevTag
|
|
66
|
+
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
|
|
67
|
+
|
|
68
|
+
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
|
69
|
+
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
|
|
70
|
+
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
|
71
|
+
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<style lang="scss" scoped>
|
|
80
|
+
.scroll-container {
|
|
81
|
+
white-space: nowrap;
|
|
82
|
+
position: relative;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
width: 100%;
|
|
85
|
+
::v-deep {
|
|
86
|
+
.el-scrollbar__bar {
|
|
87
|
+
bottom: 0px;
|
|
88
|
+
}
|
|
89
|
+
.el-scrollbar__wrap {
|
|
90
|
+
height: 49px;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div id="tags-view-container" class="tags-view-container">
|
|
3
|
+
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
|
|
4
|
+
<router-link
|
|
5
|
+
v-for="tag in visitedViews"
|
|
6
|
+
ref="tag"
|
|
7
|
+
:key="tag.path"
|
|
8
|
+
:class="isActive(tag)?'active':''"
|
|
9
|
+
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
|
10
|
+
tag="span"
|
|
11
|
+
class="tags-view-item"
|
|
12
|
+
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
|
|
13
|
+
@contextmenu.prevent.native="openMenu(tag,$event)"
|
|
14
|
+
>
|
|
15
|
+
{{ tag.title }}
|
|
16
|
+
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
|
|
17
|
+
</router-link>
|
|
18
|
+
</scroll-pane>
|
|
19
|
+
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
|
|
20
|
+
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
|
|
21
|
+
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
|
|
22
|
+
<li @click="closeOthersTags">关闭其他</li>
|
|
23
|
+
<li @click="closeAllTags(selectedTag)">关闭全部</li>
|
|
24
|
+
</ul>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script>
|
|
29
|
+
import ScrollPane from './ScrollPane'
|
|
30
|
+
import path from 'path'
|
|
31
|
+
import router from '../../../../router/index'
|
|
32
|
+
export default {
|
|
33
|
+
components: { ScrollPane },
|
|
34
|
+
data() {
|
|
35
|
+
return {
|
|
36
|
+
visible: false,
|
|
37
|
+
top: 0,
|
|
38
|
+
left: 0,
|
|
39
|
+
selectedTag: {},
|
|
40
|
+
affixTags: []
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
computed: {
|
|
44
|
+
visitedViews() {
|
|
45
|
+
return this.$store.state.tagsView.visitedViews
|
|
46
|
+
},
|
|
47
|
+
routes() {
|
|
48
|
+
return router.options.routes
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
watch: {
|
|
52
|
+
$route() {
|
|
53
|
+
this.addTags()
|
|
54
|
+
this.moveToCurrentTag()
|
|
55
|
+
},
|
|
56
|
+
visible(value) {
|
|
57
|
+
if (value) {
|
|
58
|
+
document.body.addEventListener('click', this.closeMenu)
|
|
59
|
+
} else {
|
|
60
|
+
document.body.removeEventListener('click', this.closeMenu)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
mounted() {
|
|
65
|
+
this.initTags()
|
|
66
|
+
this.addTags()
|
|
67
|
+
},
|
|
68
|
+
methods: {
|
|
69
|
+
isActive(route) {
|
|
70
|
+
return route.path === this.$route.path
|
|
71
|
+
},
|
|
72
|
+
isAffix(tag) {
|
|
73
|
+
return tag.meta && tag.meta.affix
|
|
74
|
+
},
|
|
75
|
+
filterAffixTags(routes, basePath = '/') {
|
|
76
|
+
let tags = []
|
|
77
|
+
routes.forEach(route => {
|
|
78
|
+
if (route.meta && route.meta.affix) {
|
|
79
|
+
const tagPath = path.resolve(basePath, route.path)
|
|
80
|
+
tags.push({
|
|
81
|
+
fullPath: tagPath,
|
|
82
|
+
path: tagPath,
|
|
83
|
+
name: route.name,
|
|
84
|
+
meta: { ...route.meta }
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
if (route.children) {
|
|
88
|
+
const tempTags = this.filterAffixTags(route.children, route.path)
|
|
89
|
+
if (tempTags.length >= 1) {
|
|
90
|
+
tags = [...tags, ...tempTags]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
return tags
|
|
95
|
+
},
|
|
96
|
+
initTags() {
|
|
97
|
+
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
|
|
98
|
+
for (const tag of affixTags) {
|
|
99
|
+
if (tag.name) {
|
|
100
|
+
this.$store.dispatch('tagsView/addVisitedView', tag)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
addTags() {
|
|
105
|
+
const { meta } = this.$route
|
|
106
|
+
if (meta && meta.title) {
|
|
107
|
+
this.$store.dispatch('tagsView/addView', this.$route)
|
|
108
|
+
}
|
|
109
|
+
return false
|
|
110
|
+
},
|
|
111
|
+
moveToCurrentTag() {
|
|
112
|
+
const tags = this.$refs.tag
|
|
113
|
+
this.$nextTick(() => {
|
|
114
|
+
for (const tag of tags) {
|
|
115
|
+
if (tag.to.path === this.$route.path) {
|
|
116
|
+
this.$refs.scrollPane.moveToTarget(tag)
|
|
117
|
+
// when query is different then update
|
|
118
|
+
if (tag.to.fullPath !== this.$route.fullPath) {
|
|
119
|
+
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
|
|
120
|
+
}
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
},
|
|
126
|
+
refreshSelectedTag(view) {
|
|
127
|
+
this.$store.dispatch('tagsView/delCachedView', view).then(() => {
|
|
128
|
+
const { fullPath } = view
|
|
129
|
+
this.$nextTick(() => {
|
|
130
|
+
this.$router.replace({
|
|
131
|
+
path: '/redirect' + fullPath
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
},
|
|
136
|
+
closeSelectedTag(view) {
|
|
137
|
+
this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
|
|
138
|
+
if (this.isActive(view)) {
|
|
139
|
+
this.toLastView(visitedViews, view)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
},
|
|
143
|
+
closeOthersTags() {
|
|
144
|
+
this.$router.push(this.selectedTag)
|
|
145
|
+
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
|
|
146
|
+
this.moveToCurrentTag()
|
|
147
|
+
})
|
|
148
|
+
},
|
|
149
|
+
closeAllTags(view) {
|
|
150
|
+
this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
|
|
151
|
+
if (this.affixTags.some(tag => tag.path === view.path)) {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
this.toLastView(visitedViews, view)
|
|
155
|
+
})
|
|
156
|
+
},
|
|
157
|
+
toLastView(visitedViews, view) {
|
|
158
|
+
const latestView = visitedViews.slice(-1)[0]
|
|
159
|
+
if (latestView) {
|
|
160
|
+
this.$router.push(latestView.fullPath)
|
|
161
|
+
} else {
|
|
162
|
+
// now the default is to redirect to the home page if there is no tags-view,
|
|
163
|
+
// you can adjust it according to your needs.
|
|
164
|
+
if (view.name === 'Dashboard') {
|
|
165
|
+
// to reload home page
|
|
166
|
+
this.$router.replace({ path: '/redirect' + view.fullPath })
|
|
167
|
+
} else {
|
|
168
|
+
this.$router.push('/')
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
openMenu(tag, e) {
|
|
173
|
+
const menuMinWidth = 105
|
|
174
|
+
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
|
|
175
|
+
const offsetWidth = this.$el.offsetWidth // container width
|
|
176
|
+
const maxLeft = offsetWidth - menuMinWidth // left boundary
|
|
177
|
+
const left = e.clientX - offsetLeft + 15 // 15: margin right
|
|
178
|
+
|
|
179
|
+
if (left > maxLeft) {
|
|
180
|
+
this.left = maxLeft
|
|
181
|
+
} else {
|
|
182
|
+
this.left = left
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.top = e.clientY
|
|
186
|
+
this.visible = true
|
|
187
|
+
this.selectedTag = tag
|
|
188
|
+
},
|
|
189
|
+
closeMenu() {
|
|
190
|
+
this.visible = false
|
|
191
|
+
},
|
|
192
|
+
handleScroll() {
|
|
193
|
+
this.closeMenu()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
</script>
|
|
198
|
+
|
|
199
|
+
<style lang="scss" scoped>
|
|
200
|
+
.tags-view-container {
|
|
201
|
+
height: 34px;
|
|
202
|
+
width: 100%;
|
|
203
|
+
background: #fff;
|
|
204
|
+
border-bottom: 1px solid #d8dce5;
|
|
205
|
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
|
206
|
+
.tags-view-wrapper {
|
|
207
|
+
.tags-view-item {
|
|
208
|
+
display: inline-block;
|
|
209
|
+
position: relative;
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
height: 26px;
|
|
212
|
+
line-height: 26px;
|
|
213
|
+
border: 1px solid #d8dce5;
|
|
214
|
+
color: #495060;
|
|
215
|
+
background: #fff;
|
|
216
|
+
padding: 0 8px;
|
|
217
|
+
font-size: 12px;
|
|
218
|
+
margin-left: 5px;
|
|
219
|
+
margin-top: 4px;
|
|
220
|
+
&:first-of-type {
|
|
221
|
+
margin-left: 15px;
|
|
222
|
+
}
|
|
223
|
+
&:last-of-type {
|
|
224
|
+
margin-right: 15px;
|
|
225
|
+
}
|
|
226
|
+
&.active {
|
|
227
|
+
background-color: #409EFF;
|
|
228
|
+
color: #fff;
|
|
229
|
+
border-color: #409EFF;
|
|
230
|
+
&::before {
|
|
231
|
+
content: '';
|
|
232
|
+
background: #fff;
|
|
233
|
+
display: inline-block;
|
|
234
|
+
width: 8px;
|
|
235
|
+
height: 8px;
|
|
236
|
+
border-radius: 50%;
|
|
237
|
+
position: relative;
|
|
238
|
+
margin-right: 2px;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
.contextmenu {
|
|
244
|
+
margin: 0;
|
|
245
|
+
background: #fff;
|
|
246
|
+
z-index: 3000;
|
|
247
|
+
position: absolute;
|
|
248
|
+
list-style-type: none;
|
|
249
|
+
padding: 5px 0;
|
|
250
|
+
border-radius: 4px;
|
|
251
|
+
font-size: 12px;
|
|
252
|
+
font-weight: 400;
|
|
253
|
+
color: #333;
|
|
254
|
+
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
|
255
|
+
li {
|
|
256
|
+
margin: 0;
|
|
257
|
+
padding: 7px 16px;
|
|
258
|
+
cursor: pointer;
|
|
259
|
+
&:hover {
|
|
260
|
+
background: #eee;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
</style>
|
|
266
|
+
|
|
267
|
+
<style lang="scss">
|
|
268
|
+
//reset element css of el-icon-close
|
|
269
|
+
.tags-view-wrapper {
|
|
270
|
+
.tags-view-item {
|
|
271
|
+
.el-icon-close {
|
|
272
|
+
width: 16px;
|
|
273
|
+
height: 16px;
|
|
274
|
+
vertical-align: 2px;
|
|
275
|
+
border-radius: 50%;
|
|
276
|
+
text-align: center;
|
|
277
|
+
transition: all .3s cubic-bezier(.645, .045, .355, 1);
|
|
278
|
+
transform-origin: 100% 50%;
|
|
279
|
+
&:before {
|
|
280
|
+
transform: scale(.6);
|
|
281
|
+
display: inline-block;
|
|
282
|
+
vertical-align: -3px;
|
|
283
|
+
}
|
|
284
|
+
&:hover {
|
|
285
|
+
background-color: #b4bccc;
|
|
286
|
+
color: #fff;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
</style>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<div class="main-container">
|
|
5
5
|
<div :class="{'fixed-header':fixedHeader}">
|
|
6
6
|
<navbar />
|
|
7
|
+
<tags-view />
|
|
7
8
|
</div>
|
|
8
9
|
<app-main />
|
|
9
10
|
</div>
|
|
@@ -11,14 +12,15 @@
|
|
|
11
12
|
</template>
|
|
12
13
|
|
|
13
14
|
<script>
|
|
14
|
-
import { Navbar, Sidebar, AppMain } from "./components";
|
|
15
|
+
import { Navbar, Sidebar, AppMain, TagsView } from "./components";
|
|
15
16
|
import { main } from '@/utils/getMenu'
|
|
16
17
|
export default {
|
|
17
18
|
name: "Layout",
|
|
18
19
|
components: {
|
|
19
20
|
Navbar,
|
|
20
21
|
Sidebar,
|
|
21
|
-
AppMain
|
|
22
|
+
AppMain,
|
|
23
|
+
TagsView
|
|
22
24
|
},
|
|
23
25
|
data() {
|
|
24
26
|
return {
|