xs-common-plugins 1.1.8 → 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
@@ -284,4 +284,8 @@
284
284
  1. common.js 增加 方法
285
285
  2. common.js commom.format() 拓展类型枚举
286
286
  3. 表单校验增加 min, max 参数
287
+ ```
288
+ ```
289
+ 1.2.0
290
+ 1. 项目增加多页签功能, 方便切换
287
291
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xs-common-plugins",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "private": false,
5
5
  "description": "向上公共代码部分",
6
6
  "author": "祝玲云 <1902733517@qq.com>",
@@ -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'),
@@ -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
+ }
@@ -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;
@@ -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>