xs-common-plugins 1.1.5 → 1.2.0

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 CHANGED
@@ -268,4 +268,24 @@
268
268
  ```
269
269
  1.1.5
270
270
  1. common.js common.exportExcel (报表导出) 方法, 移除空条件
271
+ ```
272
+ ```
273
+ 1.1.6
274
+ 1. 增加多租户却换
275
+ 2. 兼容相同路由跳转警告问题
276
+ 3. common.js 增加公用方法 this.$common.dic() 用于查询字典单条/多条数据
277
+ ```
278
+ ```
279
+ 1.1.7
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. 项目增加多页签功能, 方便切换
271
291
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xs-common-plugins",
3
- "version": "1.1.5",
3
+ "version": "1.2.0",
4
4
  "private": false,
5
5
  "description": "向上公共代码部分",
6
6
  "author": "祝玲云 <1902733517@qq.com>",
@@ -4,7 +4,8 @@ import router from '@/router/index'
4
4
  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
+ import moduleCfg from '@/modules/module.config.js'
8
9
  const common = {}
9
10
 
10
11
  /**
@@ -399,28 +400,45 @@ common.requiredList = (list) => {
399
400
  /**
400
401
  * 格式化数据样式
401
402
  * @param {*} value 值
402
- * @param {*} type 要格式化的类型 [date, price]
403
+ * @param {*} type 要格式化的类型 [date, price, price, money, enum, list(前端自定义枚举)]
403
404
  * @returns
404
405
  */
405
406
  common.format = (value, type="date") => {
406
- switch (type) {
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);
407
411
  case 'date':
408
- if(value) return value.replace("T", ' ').substr(0, 19)
409
- else return ''
410
- break;
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) : ''
411
417
  case 'price':
412
418
  if(typeof(value) === 'number') {
413
419
  return value.toFixed(2).toString().replace(/,/g,'').replace(/\d+/, function (n) { // 先提取整数部分
414
420
  return n.replace(/(\d)(?=(\d{3})+$)/g, function ($1) { // 对整数部分添加分隔符
415
- return $1 + ",";
421
+ return $1 + ",";
416
422
  });
417
423
  });
418
424
  }
419
425
  else return ''
420
- break;
426
+ case 'enum':
427
+ return getValueByType('enum', type, value)
428
+ case 'list':
429
+ return getValueByType('list', type, value)
421
430
  default:
422
431
  break;
423
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
+ }
424
442
  }
425
443
 
426
444
  /**
@@ -473,4 +491,59 @@ common.getReportList = (that, url, query, callBack) => {
473
491
  })
474
492
  }
475
493
 
494
+
495
+ /**
496
+ * 字典表 里通过Id 查 name, 或者查询多条数据 (单条返回 name, 多条返回数组)
497
+ * @param {Array} props ['服务名', '表名', '字段名']
498
+ * @param {String, Number, Array} val 值, 传入id, 或者 [id1, id2, ...]
499
+ *
500
+ * 使用案例 (异步方法)
501
+ * async getValue () {
502
+ * await this.$common.dic(['qccuser', 'Brand'], '10010')
503
+ * }
504
+ *
505
+ */
506
+
507
+ common.dic = async (props, val) => {
508
+ let isArray = Array.isArray(val)
509
+ let server = props[0] || ''
510
+ let tableName = props[1] || ''
511
+ let prop = props[2] || 'name'
512
+ let key = server + tableName
513
+ if(store.getters.dic[key]) {
514
+ if(isArray) return await getDicVal()
515
+ let row = store.getters.dic[key].find(item => item.id == val)
516
+ if(row && (row.isNull || row[prop] === null)) { return '' }
517
+ if(row) return row[prop]
518
+ if(!row) return await getDicVal()
519
+
520
+ } else {
521
+ return await getDicVal()
522
+ }
523
+ async function getDicVal () {
524
+ let servers = props[0] || "";
525
+ let tableName = props[1] || "";
526
+ let key = servers + tableName;
527
+ let url = servers + "/UserAll/Search/Dic/" + tableName;
528
+ let res = await request.post(url, isArray ? val : [val])
529
+ if (res.code !== 0) return;
530
+ store.commit("dic/SET_ASK", { key, val: res.data });
531
+ if(isArray) return res.data
532
+ else {
533
+ let row = res.data.find(item => item.id == val)
534
+ return row ? row[prop] : ''
535
+ }
536
+ }
537
+ }
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
+ }
476
549
  export default common
@@ -3,7 +3,22 @@ import VueRouter from 'vue-router'
3
3
  import autoRouter from '../automatically/router'
4
4
  Vue.use(VueRouter)
5
5
 
6
+ const originalPush = VueRouter.prototype.push;
7
+ VueRouter.prototype.push = function push(location) {
8
+ return originalPush.call(this, location).catch(err => err)
9
+ }
6
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
+ },
7
22
  {
8
23
  path: '/',
9
24
  name: 'layout',
@@ -16,7 +31,7 @@ const routes = [
16
31
  path: '/home',
17
32
  name: 'home',
18
33
  component: () => import('../views/layout/index'),
19
- meta: { title: '首页', icon: 'example' },
34
+ meta: { title: '首页', icon: 'example', affix: true,},
20
35
  children: [{
21
36
  path:'/',
22
37
  component: () => import('../views/home/index'),
@@ -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
+ }
@@ -2,11 +2,16 @@ const filterRules = ({ required, type, min, max }) => {
2
2
  let validate = [];
3
3
  if (required) {
4
4
  // 必填项
5
- validate.push({
6
- required: true,
7
- message: "该输入项为必填项",
8
- trigger: "change",
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 = "";
@@ -77,6 +77,7 @@ function getApis () {
77
77
 
78
78
  export {
79
79
 
80
- main
80
+ main,
81
+ initMenu
81
82
 
82
83
  }
@@ -25,10 +25,10 @@ export default {
25
25
  .app-main {
26
26
  display: flex;
27
27
  /*50 = navbar */
28
- height: calc(100vh - 50px);
28
+ height: calc(100vh - 85px);
29
29
  width: 100%;
30
30
  position: relative;
31
- overflow: hidden;
31
+ overflow: auto;
32
32
  }
33
33
  .footer {
34
34
  position: absolute;
@@ -31,6 +31,7 @@
31
31
  <AllSearch />
32
32
  <div class="is-dot">
33
33
  <el-badge
34
+ style="margin: 0 3px;"
34
35
  v-if="isShowKF === 'isv' || isShowKF === 'erp'"
35
36
  is-dot
36
37
  class="item"
@@ -45,7 +46,7 @@
45
46
  </el-badge>
46
47
  </div>
47
48
  <div>
48
- <el-tag>{{
49
+ <el-tag style="margin: 0 3px;">{{
49
50
  userProfile &&
50
51
  userProfile.hasOwnProperty("real_name") &&
51
52
  userProfile.real_name
@@ -61,6 +62,17 @@
61
62
  : "--未绑定--"
62
63
  }}
63
64
  </el-tag>
65
+ </div>
66
+ <!-- 租户 -->
67
+ <div class="tenant" v-if="userProfile && userProfile.TId">
68
+ <el-dropdown @command="changeTenant">
69
+ <span class="el-dropdown-link">
70
+ {{tenantList && tenantList[userProfile.TId]}}
71
+ </span>
72
+ <el-dropdown-menu slot="dropdown">
73
+ <el-dropdown-item v-for="(value, key) in tenantList" :key="key" :command="key">{{value}}</el-dropdown-item>
74
+ </el-dropdown-menu>
75
+ </el-dropdown>
64
76
  </div>
65
77
  <el-dropdown class="avatar-container" trigger="click">
66
78
  <div class="avatar-wrapper">
@@ -107,7 +119,9 @@ import AllSearch from "./AllSearch";
107
119
  import { getConfig } from "@/utils/global-config";
108
120
  import { getToken, removeToken, removeLocalToken } from "@/utils/auth"; // get token from cookie
109
121
  import ImCom from "@/components/im";
110
- import { getLocalStorage } from "@/utils/localStorage";
122
+ import { getLocalStorage, setLocalStorage } from "@/utils/localStorage";
123
+ import { setToken } from "@/utils/auth"; // get token from cookie
124
+ import { initMenu } from '@/utils/getMenu'
111
125
 
112
126
  export default {
113
127
  components: {
@@ -136,6 +150,7 @@ export default {
136
150
  LoginName: false,
137
151
  isShowKF: getConfig("GROUP"),
138
152
  defaultParentId: "887904812217cca9bc2b9adb875daf42",
153
+ tenantList: [], // 租户列表
139
154
  };
140
155
  },
141
156
  computed: {
@@ -146,8 +161,29 @@ export default {
146
161
  if (userName === "admin") {
147
162
  this.LoginName = true;
148
163
  }
164
+ if(this.userProfile &&this.userProfile.TId) this.getTenantList()
149
165
  },
150
166
  methods: {
167
+ changeTenant (id) {
168
+ if(id === this.userProfile.TId) return
169
+ this.$router.push("/home")
170
+ this.$ask.ucmng.api.sysmng.Tenant.SwitchTenant({clientId: getConfig('CLIENT_ID'), newTId: id}).then(res => {
171
+ let newuser = this.userProfile
172
+ newuser.TId = id
173
+ setLocalStorage('userProfile', newuser)
174
+ this.userProfile = getLocalStorage('userProfile')
175
+ let user = res.data
176
+ setToken(user.accessToken);
177
+ localStorage.setItem("token", user.accessToken);
178
+ initMenu()
179
+
180
+ })
181
+ },
182
+ getTenantList () {
183
+ this.$ask.ucmng.api.sysmng.Tenant.GetTenants().then(res => {
184
+ this.tenantList = res.data
185
+ })
186
+ },
151
187
  toggleSideBar() {
152
188
  this.$store.dispatch("app/toggleSideBar");
153
189
  },
@@ -279,6 +315,10 @@ export default {
279
315
  }
280
316
  }
281
317
 
318
+ .tenant {
319
+ padding: 0 10px;
320
+ }
321
+
282
322
  .breadcrumb-container {
283
323
  float: left;
284
324
  }
@@ -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>
@@ -1,3 +1,4 @@
1
1
  export { default as Navbar } from './Navbar'
2
2
  export { default as Sidebar } from './Sidebar'
3
3
  export { default as AppMain } from './AppMain'
4
+ export { default as TagsView } from './TagsView/index.vue'
@@ -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 {
@@ -0,0 +1,12 @@
1
+ <script>
2
+ export default {
3
+ created() {
4
+ const { params, query } = this.$route
5
+ const { path } = params
6
+ this.$router.replace({ path: '/' + path, query })
7
+ },
8
+ render: function(h) {
9
+ return h() // avoid warning message
10
+ }
11
+ }
12
+ </script>