Browse Source

feat[litemall-admin]: 更新vue-element-admin框架版本4.2.1

Junling Bu 6 years ago
parent
commit
ed9f946289

+ 2 - 2
litemall-admin/package.json

@@ -13,8 +13,7 @@
     "lint": "eslint --ext .js,.vue src",
     "test:unit": "jest --clearCache && vue-cli-service test:unit",
     "test:ci": "npm run lint && npm run test:unit",
-    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
-    "new": "plop"
+    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
   },
   "husky": {
     "hooks": {
@@ -56,6 +55,7 @@
     "jszip": "3.2.1",
     "normalize.css": "7.0.0",
     "nprogress": "0.2.0",
+    "path-to-regexp": "2.4.0",
     "screenfull": "4.2.0",
     "vue": "2.6.10",
     "vue-count-to": "1.0.13",

+ 47 - 24
litemall-admin/src/components/Breadcrumb/index.vue

@@ -1,9 +1,9 @@
 <template>
   <el-breadcrumb class="app-breadcrumb" separator="/">
     <transition-group name="breadcrumb">
-      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
-        <span v-if="item.redirect==='noredirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
-        <router-link v-else :to="item.redirect||item.path">{{ item.meta.title }}</router-link>
+      <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
+        <span v-if="item.redirect === 'noredirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
       </el-breadcrumb-item>
     </transition-group>
   </el-breadcrumb>
@@ -19,7 +19,11 @@ export default {
     }
   },
   watch: {
-    $route() {
+    $route(route) {
+      // if you go to the redirect page, do not update the breadcrumbs
+      if (route.path.startsWith('/redirect/')) {
+        return
+      }
       this.getBreadcrumb()
     }
   },
@@ -28,34 +32,53 @@ export default {
   },
   methods: {
     getBreadcrumb() {
-      const { params } = this.$route
-      let matched = this.$route.matched.filter(item => {
-        if (item.name) {
-          // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
-          var toPath = pathToRegexp.compile(item.path)
-          item.path = toPath(params)
-          return true
-        }
-      })
+      // only show routes with meta.title
+      let matched = this.$route.matched.filter(
+        item => item.meta && item.meta.title
+      )
       const first = matched[0]
-      if (first && first.name.trim().toLocaleLowerCase() !== 'Dashboard'.toLocaleLowerCase()) {
+      if (!this.isDashboard(first)) {
         matched = [{ path: '/dashboard', meta: { title: '首页' }}].concat(matched)
       }
-      this.levelList = matched
+      this.levelList = matched.filter(
+        item => item.meta && item.meta.title && item.meta.breadcrumb !== false
+      )
+    },
+    isDashboard(route) {
+      const name = route && route.name
+      if (!name) {
+        return false
+      }
+      return (
+        name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase())
+    },
+    pathCompile(path) {
+      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
+      const { params } = this.$route
+      var toPath = pathToRegexp.compile(path)
+      return toPath(params)
+    },
+    handleLink(item) {
+      const { redirect, path } = item
+      if (redirect) {
+        this.$router.push(redirect)
+        return
+      }
+      this.$router.push(this.pathCompile(path))
     }
   }
 }
 </script>
 
 <style rel="stylesheet/scss" lang="scss" scoped>
-  .app-breadcrumb.el-breadcrumb {
-    display: inline-block;
-    font-size: 14px;
-    line-height: 50px;
-    margin-left: 10px;
-    .no-redirect {
-      color: #97a8be;
-      cursor: text;
-    }
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  margin-left: 10px;
+  .no-redirect {
+    color: #97a8be;
+    cursor: text;
   }
+}
 </style>

+ 19 - 2
litemall-admin/src/components/SvgIcon/index.vue

@@ -1,10 +1,13 @@
 <template>
-  <svg :class="svgClass" aria-hidden="true">
-    <use :xlink:href="iconName"/>
+  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
+  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
+    <use :xlink:href="iconName" />
   </svg>
 </template>
 
 <script>
+// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
+import { isExternal } from '@/utils/validate'
 export default {
   name: 'SvgIcon',
   props: {
@@ -18,6 +21,9 @@ export default {
     }
   },
   computed: {
+    isExternal() {
+      return isExternal(this.iconClass)
+    },
     iconName() {
       return `#icon-${this.iconClass}`
     },
@@ -27,6 +33,12 @@ export default {
       } else {
         return 'svg-icon'
       }
+    },
+    styleExternalIcon() {
+      return {
+        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
+        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
+      }
     }
   }
 }
@@ -40,4 +52,9 @@ export default {
   fill: currentColor;
   overflow: hidden;
 }
+.svg-external-icon {
+  background-color: currentColor;
+  mask-size: cover!important;
+  display: inline-block;
+}
 </style>

+ 1 - 1
litemall-admin/src/router/index.js

@@ -65,7 +65,7 @@ export const constantRouterMap = [
         path: 'dashboard',
         component: () => import('@/views/dashboard/index'),
         name: 'Dashboard',
-        meta: { title: '首页', icon: 'dashboard', noCache: true }
+        meta: { title: '首页', icon: 'dashboard', affix: true }
       }
     ]
   }

+ 140 - 142
litemall-admin/src/store/modules/tagsView.js

@@ -1,162 +1,160 @@
-const tagsView = {
-  state: {
-    visitedViews: [],
-    cachedViews: []
-  },
-  mutations: {
-    ADD_VISITED_VIEW: (state, view) => {
-      if (state.visitedViews.some(v => v.path === view.path)) return
-      state.visitedViews.push(
-        Object.assign({}, view, {
-          title: view.meta.title || 'no-name'
-        })
-      )
-    },
-    ADD_CACHED_VIEW: (state, view) => {
-      if (state.cachedViews.includes(view.name)) return
-      if (!view.meta.noCache) {
-        state.cachedViews.push(view.name)
-      }
-    },
+const state = {
+  visitedViews: [],
+  cachedViews: []
+}
 
-    DEL_VISITED_VIEW: (state, view) => {
-      for (const [i, v] of state.visitedViews.entries()) {
-        if (v.path === view.path) {
-          state.visitedViews.splice(i, 1)
-          break
-        }
-      }
-    },
-    DEL_CACHED_VIEW: (state, view) => {
-      for (const i of state.cachedViews) {
-        if (i === view.name) {
-          const index = state.cachedViews.indexOf(i)
-          state.cachedViews.splice(index, 1)
-          break
-        }
-      }
-    },
+const mutations = {
+  ADD_VISITED_VIEW: (state, view) => {
+    if (state.visitedViews.some(v => v.path === view.path)) return
+    state.visitedViews.push(
+      Object.assign({}, view, {
+        title: view.meta.title || 'no-name'
+      })
+    )
+  },
+  ADD_CACHED_VIEW: (state, view) => {
+    if (state.cachedViews.includes(view.name)) return
+    if (!view.meta.noCache) {
+      state.cachedViews.push(view.name)
+    }
+  },
 
-    DEL_OTHERS_VISITED_VIEWS: (state, view) => {
-      for (const [i, v] of state.visitedViews.entries()) {
-        if (v.path === view.path) {
-          state.visitedViews = state.visitedViews.slice(i, i + 1)
-          break
-        }
+  DEL_VISITED_VIEW: (state, view) => {
+    for (const [i, v] of state.visitedViews.entries()) {
+      if (v.path === view.path) {
+        state.visitedViews.splice(i, 1)
+        break
       }
-    },
-    DEL_OTHERS_CACHED_VIEWS: (state, view) => {
-      for (const i of state.cachedViews) {
-        if (i === view.name) {
-          const index = state.cachedViews.indexOf(i)
-          state.cachedViews = state.cachedViews.slice(index, index + 1)
-          break
-        }
-      }
-    },
+    }
+  },
+  DEL_CACHED_VIEW: (state, view) => {
+    const index = state.cachedViews.indexOf(view.name)
+    index > -1 && state.cachedViews.splice(index, 1)
+  },
 
-    DEL_ALL_VISITED_VIEWS: state => {
-      state.visitedViews = []
-    },
-    DEL_ALL_CACHED_VIEWS: state => {
+  DEL_OTHERS_VISITED_VIEWS: (state, view) => {
+    state.visitedViews = state.visitedViews.filter(v => {
+      return v.meta.affix || v.path === view.path
+    })
+  },
+  DEL_OTHERS_CACHED_VIEWS: (state, view) => {
+    const index = state.cachedViews.indexOf(view.name)
+    if (index > -1) {
+      state.cachedViews = state.cachedViews.slice(index, index + 1)
+    } else {
+      // if index = -1, there is no cached tags
       state.cachedViews = []
-    },
+    }
+  },
 
-    UPDATE_VISITED_VIEW: (state, view) => {
-      for (let v of state.visitedViews) {
-        if (v.path === view.path) {
-          v = Object.assign(v, view)
-          break
-        }
+  DEL_ALL_VISITED_VIEWS: state => {
+    // keep affix tags
+    const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
+    state.visitedViews = affixTags
+  },
+  DEL_ALL_CACHED_VIEWS: state => {
+    state.cachedViews = []
+  },
+
+  UPDATE_VISITED_VIEW: (state, view) => {
+    for (let v of state.visitedViews) {
+      if (v.path === view.path) {
+        v = Object.assign(v, view)
+        break
       }
     }
+  }
+}
 
+const actions = {
+  addView({ dispatch }, view) {
+    dispatch('addVisitedView', view)
+    dispatch('addCachedView', view)
+  },
+  addVisitedView({ commit }, view) {
+    commit('ADD_VISITED_VIEW', view)
+  },
+  addCachedView({ commit }, view) {
+    commit('ADD_CACHED_VIEW', view)
   },
-  actions: {
-    addView({ dispatch }, view) {
-      dispatch('addVisitedView', view)
-      dispatch('addCachedView', view)
-    },
-    addVisitedView({ commit }, view) {
-      commit('ADD_VISITED_VIEW', view)
-    },
-    addCachedView({ commit }, view) {
-      commit('ADD_CACHED_VIEW', view)
-    },
 
-    delView({ dispatch, state }, view) {
-      return new Promise(resolve => {
-        dispatch('delVisitedView', view)
-        dispatch('delCachedView', view)
-        resolve({
-          visitedViews: [...state.visitedViews],
-          cachedViews: [...state.cachedViews]
-        })
-      })
-    },
-    delVisitedView({ commit, state }, view) {
-      return new Promise(resolve => {
-        commit('DEL_VISITED_VIEW', view)
-        resolve([...state.visitedViews])
+  delView({ dispatch, state }, view) {
+    return new Promise(resolve => {
+      dispatch('delVisitedView', view)
+      dispatch('delCachedView', view)
+      resolve({
+        visitedViews: [...state.visitedViews],
+        cachedViews: [...state.cachedViews]
       })
-    },
-    delCachedView({ commit, state }, view) {
-      return new Promise(resolve => {
-        commit('DEL_CACHED_VIEW', view)
-        resolve([...state.cachedViews])
-      })
-    },
+    })
+  },
+  delVisitedView({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_VISITED_VIEW', view)
+      resolve([...state.visitedViews])
+    })
+  },
+  delCachedView({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_CACHED_VIEW', view)
+      resolve([...state.cachedViews])
+    })
+  },
 
-    delOthersViews({ dispatch, state }, view) {
-      return new Promise(resolve => {
-        dispatch('delOthersVisitedViews', view)
-        dispatch('delOthersCachedViews', view)
-        resolve({
-          visitedViews: [...state.visitedViews],
-          cachedViews: [...state.cachedViews]
-        })
-      })
-    },
-    delOthersVisitedViews({ commit, state }, view) {
-      return new Promise(resolve => {
-        commit('DEL_OTHERS_VISITED_VIEWS', view)
-        resolve([...state.visitedViews])
+  delOthersViews({ dispatch, state }, view) {
+    return new Promise(resolve => {
+      dispatch('delOthersVisitedViews', view)
+      dispatch('delOthersCachedViews', view)
+      resolve({
+        visitedViews: [...state.visitedViews],
+        cachedViews: [...state.cachedViews]
       })
-    },
-    delOthersCachedViews({ commit, state }, view) {
-      return new Promise(resolve => {
-        commit('DEL_OTHERS_CACHED_VIEWS', view)
-        resolve([...state.cachedViews])
-      })
-    },
+    })
+  },
+  delOthersVisitedViews({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_OTHERS_VISITED_VIEWS', view)
+      resolve([...state.visitedViews])
+    })
+  },
+  delOthersCachedViews({ commit, state }, view) {
+    return new Promise(resolve => {
+      commit('DEL_OTHERS_CACHED_VIEWS', view)
+      resolve([...state.cachedViews])
+    })
+  },
 
-    delAllViews({ dispatch, state }, view) {
-      return new Promise(resolve => {
-        dispatch('delAllVisitedViews', view)
-        dispatch('delAllCachedViews', view)
-        resolve({
-          visitedViews: [...state.visitedViews],
-          cachedViews: [...state.cachedViews]
-        })
-      })
-    },
-    delAllVisitedViews({ commit, state }) {
-      return new Promise(resolve => {
-        commit('DEL_ALL_VISITED_VIEWS')
-        resolve([...state.visitedViews])
+  delAllViews({ dispatch, state }, view) {
+    return new Promise(resolve => {
+      dispatch('delAllVisitedViews', view)
+      dispatch('delAllCachedViews', view)
+      resolve({
+        visitedViews: [...state.visitedViews],
+        cachedViews: [...state.cachedViews]
       })
-    },
-    delAllCachedViews({ commit, state }) {
-      return new Promise(resolve => {
-        commit('DEL_ALL_CACHED_VIEWS')
-        resolve([...state.cachedViews])
-      })
-    },
+    })
+  },
+  delAllVisitedViews({ commit, state }) {
+    return new Promise(resolve => {
+      commit('DEL_ALL_VISITED_VIEWS')
+      resolve([...state.visitedViews])
+    })
+  },
+  delAllCachedViews({ commit, state }) {
+    return new Promise(resolve => {
+      commit('DEL_ALL_CACHED_VIEWS')
+      resolve([...state.cachedViews])
+    })
+  },
 
-    updateVisitedView({ commit }, view) {
-      commit('UPDATE_VISITED_VIEW', view)
-    }
+  updateVisitedView({ commit }, view) {
+    commit('UPDATE_VISITED_VIEW', view)
   }
 }
 
-export default tagsView
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}

+ 6 - 1
litemall-admin/src/styles/btn.scss

@@ -2,8 +2,10 @@
 
 @mixin colorBtn($color) {
   background: $color;
+
   &:hover {
     color: $color;
+
     &:before,
     &:after {
       background: $color;
@@ -49,14 +51,17 @@
   transition: 600ms ease all;
   position: relative;
   display: inline-block;
+
   &:hover {
     background: #fff;
+
     &:before,
     &:after {
       width: 100%;
       transition: 600ms ease all;
     }
   }
+
   &:before,
   &:after {
     content: '';
@@ -67,6 +72,7 @@
     width: 0;
     transition: 400ms ease all;
   }
+
   &::after {
     right: inherit;
     top: inherit;
@@ -91,4 +97,3 @@
   font-size: 14px;
   border-radius: 4px;
 }
-

+ 66 - 64
litemall-admin/src/styles/element-ui.scss

@@ -1,82 +1,84 @@
- //覆盖一些element-ui样式
+// cover some element-ui styles
 
- .el-breadcrumb__inner, .el-breadcrumb__inner a{
-  font-weight: 400!important;
+.el-breadcrumb__inner,
+.el-breadcrumb__inner a {
+  font-weight: 400 !important;
 }
 
- .el-upload {
-   input[type="file"] {
-     display: none !important;
-   }
- }
+.el-upload {
+  input[type="file"] {
+    display: none !important;
+  }
+}
 
- .el-upload__input {
-   display: none;
- }
+.el-upload__input {
+  display: none;
+}
 
- .cell {
-   .el-tag {
-     margin-right: 0px;
-   }
- }
+.cell {
+  .el-tag {
+    margin-right: 0px;
+  }
+}
 
- .small-padding {
-   .cell {
-     padding-left: 5px;
-     padding-right: 5px;
-   }
- }
+.small-padding {
+  .cell {
+    padding-left: 5px;
+    padding-right: 5px;
+  }
+}
 
- .fixed-width{
-  .el-button--mini{
+.fixed-width {
+  .el-button--mini {
     padding: 7px 10px;
     width: 60px;
   }
- }
+}
+
+.status-col {
+  .cell {
+    padding: 0 10px;
+    text-align: center;
 
- .status-col {
-   .cell {
-     padding: 0 10px;
-     text-align: center;
-     .el-tag {
-       margin-right: 0px;
-     }
-   }
- }
+    .el-tag {
+      margin-right: 0px;
+    }
+  }
+}
 
- //暂时性解决dialog 问题 https://github.com/ElemeFE/element/issues/2461
- .el-dialog {
-   transform: none;
-   left: 0;
-   position: relative;
-   margin: 0 auto;
- }
+// to fixed https://github.com/ElemeFE/element/issues/2461
+.el-dialog {
+  transform: none;
+  left: 0;
+  position: relative;
+  margin: 0 auto;
+}
 
- //文章页textarea修改样式
- .article-textarea {
-   textarea {
-     padding-right: 40px;
-     resize: none;
-     border: none;
-     border-radius: 0px;
-     border-bottom: 1px solid #bfcbd9;
-   }
- }
+// refine element ui upload
+.upload-container {
+  .el-upload {
+    width: 100%;
 
- //element ui upload
- .upload-container {
-   .el-upload {
-     width: 100%;
-     .el-upload-dragger {
-       width: 100%;
-       height: 200px;
-     }
-   }
- }
+    .el-upload-dragger {
+      width: 100%;
+      height: 200px;
+    }
+  }
+}
 
-//dropdown
- .el-dropdown-menu{
-  a{
+// dropdown
+.el-dropdown-menu {
+  a {
     display: block
   }
 }
+
+// fix date-picker ui bug in filter-item
+.el-range-editor.el-input__inner {
+  display: inline-flex !important;
+}
+
+// to fix el-date-picker css style
+.el-range-separator {
+  box-sizing: content-box;
+}

+ 19 - 21
litemall-admin/src/styles/index.scss

@@ -22,7 +22,7 @@ html {
   box-sizing: border-box;
 }
 
-#app{
+#app {
   height: 100%;
 }
 
@@ -53,9 +53,9 @@ a:hover {
   text-decoration: none;
 }
 
-div:focus{
+div:focus {
   outline: none;
- }
+}
 
 .fr {
   float: right;
@@ -96,36 +96,29 @@ div:focus{
   }
 }
 
-code {
+aside {
   background: #eef1f6;
-  padding: 15px 16px;
+  padding: 8px 24px;
   margin-bottom: 20px;
+  border-radius: 2px;
   display: block;
-  line-height: 36px;
-  font-size: 15px;
-  font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
+  line-height: 32px;
+  font-size: 16px;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+  color: #2c3e50;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+
   a {
     color: #337ab7;
     cursor: pointer;
+
     &:hover {
       color: rgb(32, 160, 255);
     }
   }
 }
 
-.warn-content{
-  background: rgba(66,185,131,.1);
-  border-radius: 2px;
-  padding: 16px;
-  padding: 1rem;
-  line-height: 1.6rem;
-  word-spacing: .05rem;
-  a{
-    color: #42b983;
-    font-weight: 600;
-  }
-}
-
 //main-container全局样式
 .app-container {
   padding: 20px;
@@ -153,13 +146,16 @@ code {
   padding-right: 20px;
   transition: 600ms ease position;
   background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
+
   .subtitle {
     font-size: 20px;
     color: #fff;
   }
+
   &.draft {
     background: #d0d0d0;
   }
+
   &.deleted {
     background: #d0d0d0;
   }
@@ -169,6 +165,7 @@ code {
 .link-type:focus {
   color: #337ab7;
   cursor: pointer;
+
   &:hover {
     color: rgb(32, 160, 255);
   }
@@ -176,6 +173,7 @@ code {
 
 .filter-container {
   padding-bottom: 10px;
+
   .filter-item {
     display: inline-block;
     vertical-align: middle;

+ 6 - 0
litemall-admin/src/styles/mixin.scss

@@ -10,9 +10,11 @@
   &::-webkit-scrollbar-track-piece {
     background: #d3dce6;
   }
+
   &::-webkit-scrollbar {
     width: 6px;
   }
+
   &::-webkit-scrollbar-thumb {
     background: #99a9bf;
     border-radius: 20px;
@@ -37,21 +39,25 @@
   $transparent-border-style: $width solid transparent;
   height: 0;
   width: 0;
+
   @if $direction==up {
     border-bottom: $color-border-style;
     border-left: $transparent-border-style;
     border-right: $transparent-border-style;
   }
+
   @else if $direction==right {
     border-left: $color-border-style;
     border-top: $transparent-border-style;
     border-bottom: $transparent-border-style;
   }
+
   @else if $direction==down {
     border-top: $color-border-style;
     border-left: $transparent-border-style;
     border-right: $transparent-border-style;
   }
+
   @else if $direction==left {
     border-right: $color-border-style;
     border-top: $transparent-border-style;

+ 108 - 32
litemall-admin/src/styles/sidebar.scss

@@ -1,15 +1,16 @@
 #app {
-  // 主体区域
+
   .main-container {
     min-height: 100%;
     transition: margin-left .28s;
-    margin-left: 180px;
+    margin-left: $sideBarWidth;
     position: relative;
   }
-  // 侧边栏
+
   .sidebar-container {
     transition: width 0.28s;
-    width: 180px !important;
+    width: $sideBarWidth !important;
+    background-color: $menuBg;
     height: 100%;
     position: fixed;
     font-size: 0px;
@@ -18,62 +19,111 @@
     left: 0;
     z-index: 1001;
     overflow: hidden;
-    //reset element-ui css
+
+    // reset element-ui css
     .horizontal-collapse-transition {
       transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
     }
+
     .scrollbar-wrapper {
-      overflow-x: hidden!important;
-      .el-scrollbar__view {
-        height: 100%;
-      }
+      overflow-x: hidden !important;
     }
-    .el-scrollbar__bar.is-vertical{
+
+    .el-scrollbar__bar.is-vertical {
       right: 0px;
     }
+
+    .el-scrollbar {
+      height: 100%;
+    }
+
+    &.has-logo {
+      .el-scrollbar {
+        height: calc(100% - 50px);
+      }
+    }
+
     .is-horizontal {
       display: none;
     }
+
     a {
       display: inline-block;
       width: 100%;
       overflow: hidden;
     }
+
     .svg-icon {
       margin-right: 16px;
     }
+
     .el-menu {
       border: none;
       height: 100%;
       width: 100% !important;
     }
-    .is-active > .el-submenu__title{
-        color: #f4f4f5!important;
+
+    // menu hover
+    .submenu-title-noDropdown,
+    .el-submenu__title {
+      &:hover {
+        background-color: $menuHover !important;
+      }
+    }
+
+    .is-active>.el-submenu__title {
+      color: $subMenuActiveText !important;
+    }
+
+    & .nest-menu .el-submenu>.el-submenu__title,
+    & .el-submenu .el-menu-item {
+      min-width: $sideBarWidth !important;
+      background-color: $subMenuBg !important;
+
+      &:hover {
+        background-color: $subMenuHover !important;
+      }
     }
   }
+
   .hideSidebar {
     .sidebar-container {
-      width: 36px !important;
+      width: 54px !important;
     }
+
     .main-container {
-      margin-left: 36px;
+      margin-left: 54px;
     }
+
     .submenu-title-noDropdown {
-      padding-left: 10px !important;
+      padding: 0 !important;
       position: relative;
+
       .el-tooltip {
-        padding: 0 10px !important;
+        padding: 0 !important;
+
+        .svg-icon {
+          margin-left: 20px;
+        }
       }
     }
+
     .el-submenu {
       overflow: hidden;
+
       &>.el-submenu__title {
-        padding-left: 10px !important;
+        padding: 0 !important;
+
+        .svg-icon {
+          margin-left: 20px;
+        }
+
         .el-submenu__icon-arrow {
           display: none;
         }
       }
     }
+
     .el-menu--collapse {
       .el-submenu {
         &>.el-submenu__title {
@@ -88,35 +138,33 @@
       }
     }
   }
-  .sidebar-container .nest-menu .el-submenu>.el-submenu__title,
-  .sidebar-container .el-submenu .el-menu-item {
-    min-width: 180px !important;
-    background-color: $subMenuBg !important;
-    &:hover {
-      background-color: $menuHover !important;
-    }
-  }
+
   .el-menu--collapse .el-menu .el-submenu {
-    min-width: 180px !important;
+    min-width: $sideBarWidth !important;
   }
 
-  //适配移动端
+  // mobile responsive
   .mobile {
     .main-container {
       margin-left: 0px;
     }
+
     .sidebar-container {
       transition: transform .28s;
-      width: 180px !important;
+      width: $sideBarWidth !important;
     }
+
     &.hideSidebar {
       .sidebar-container {
+        pointer-events: none;
         transition-duration: 0.3s;
-        transform: translate3d(-180px, 0, 0);
+        transform: translate3d(-$sideBarWidth, 0, 0);
       }
     }
   }
+
   .withoutAnimation {
+
     .main-container,
     .sidebar-container {
       transition: none;
@@ -124,10 +172,38 @@
   }
 }
 
-.el-menu--vertical{
-  & >.el-menu{
-    .svg-icon{
+// when menu collapsed
+.el-menu--vertical {
+  &>.el-menu {
+    .svg-icon {
       margin-right: 16px;
     }
   }
+
+  .nest-menu .el-submenu>.el-submenu__title,
+  .el-menu-item {
+    &:hover {
+      // you can use $subMenuHover
+      background-color: $menuHover !important;
+    }
+  }
+
+  // the scroll bar appears when the subMenu is too long
+  >.el-menu--popup {
+    max-height: 100vh;
+    overflow-y: auto;
+
+    &::-webkit-scrollbar-track-piece {
+      background: #d3dce6;
+    }
+
+    &::-webkit-scrollbar {
+      width: 6px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: #99a9bf;
+      border-radius: 20px;
+    }
+  }
 }

+ 6 - 5
litemall-admin/src/styles/transition.scss

@@ -1,6 +1,6 @@
-//globl transition css
+// global transition css
 
-/*fade*/
+/* fade */
 .fade-enter-active,
 .fade-leave-active {
   transition: opacity 0.28s;
@@ -11,21 +11,23 @@
   opacity: 0;
 }
 
-/*fade-transform*/
+/* fade-transform */
 .fade-transform-leave-active,
 .fade-transform-enter-active {
   transition: all .5s;
 }
+
 .fade-transform-enter {
   opacity: 0;
   transform: translateX(-30px);
 }
+
 .fade-transform-leave-to {
   opacity: 0;
   transform: translateX(30px);
 }
 
-/*breadcrumb transition*/
+/* breadcrumb transition */
 .breadcrumb-enter-active,
 .breadcrumb-leave-active {
   transition: all .5s;
@@ -44,4 +46,3 @@
 .breadcrumb-leave-active {
   position: absolute;
 }
-

+ 24 - 2
litemall-admin/src/styles/variables.scss

@@ -1,3 +1,4 @@
+// base color
 $blue:#324157;
 $light-blue:#3A71A8;
 $red:#C03639;
@@ -7,7 +8,28 @@ $tiffany: #4AB7BD;
 $yellow:#FEC171;
 $panGreen: #30B08F;
 
-//sidebar
+// sidebar
+$menuText:#bfcbd9;
+$menuActiveText:#409EFF;
+$subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951
+
 $menuBg:#304156;
+$menuHover:#263445;
+
 $subMenuBg:#1f2d3d;
-$menuHover:#001528;
+$subMenuHover:#001528;
+
+$sideBarWidth: 210px;
+
+// the :export directive is the magic sauce for webpack
+// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
+:export {
+  menuText: $menuText;
+  menuActiveText: $menuActiveText;
+  subMenuActiveText: $subMenuActiveText;
+  menuBg: $menuBg;
+  menuHover: $menuHover;
+  subMenuBg: $subMenuBg;
+  subMenuHover: $subMenuHover;
+  sideBarWidth: $sideBarWidth;
+}

+ 0 - 8
litemall-admin/src/utils/createUniqueString.js

@@ -1,8 +0,0 @@
-/**
- * Created by jiachenpan on 17/3/8.
- */
-export default function createUniqueString() {
-  const timestamp = +new Date() + ''
-  const randomNum = parseInt((1 + Math.random()) * 65536) + ''
-  return (+(randomNum + timestamp)).toString(32)
-}

+ 67 - 17
litemall-admin/src/utils/validate.js

@@ -1,37 +1,87 @@
 /**
- * Created by jiachenpan on 16/11/18.
+ * Created by PanJiaChen on 16/11/18.
  */
 
-/* 合法uri*/
-export function validateURL(textval) {
-  const urlregex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
-  return urlregex.test(textval)
+/**
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+  return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUsername(str) {
+  const valid_map = ['admin', 'editor']
+  return valid_map.indexOf(str.trim()) >= 0
 }
 
-/* 小写字母*/
-export function validateLowerCase(str) {
+/**
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export function validURL(url) {
+  const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+  return reg.test(url)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validLowerCase(str) {
   const reg = /^[a-z]+$/
   return reg.test(str)
 }
 
-/* 大写字母*/
-export function validateUpperCase(str) {
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUpperCase(str) {
   const reg = /^[A-Z]+$/
   return reg.test(str)
 }
 
-/* 大小写字母*/
-export function validateAlphabets(str) {
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validAlphabets(str) {
   const reg = /^[A-Za-z]+$/
   return reg.test(str)
 }
 
 /**
- * validate email
- * @param email
- * @returns {boolean}
+ * @param {string} email
+ * @returns {Boolean}
+ */
+export function validEmail(email) {
+  const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+  return reg.test(email)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function isString(str) {
+  if (typeof str === 'string' || str instanceof String) {
+    return true
+  }
+  return false
+}
+
+/**
+ * @param {Array} arg
+ * @returns {Boolean}
  */
-export function validateEmail(email) {
-  const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
-  return re.test(email)
+export function isArray(arg) {
+  if (typeof Array.isArray === 'undefined') {
+    return Object.prototype.toString.call(arg) === '[object Array]'
+  }
+  return Array.isArray(arg)
 }

+ 25 - 4
litemall-admin/src/views/layout/components/AppMain.vue

@@ -2,7 +2,7 @@
   <section class="app-main">
     <transition name="fade-transform" mode="out-in">
       <keep-alive :include="cachedViews">
-        <router-view :key="key"/>
+        <router-view :key="key" />
       </keep-alive>
     </transition>
   </section>
@@ -22,13 +22,34 @@ export default {
 }
 </script>
 
-<style scoped>
+<style lang="scss" scoped>
 .app-main {
-  /*84 = navbar + tags-view = 50 +34 */
-  min-height: calc(100vh - 84px);
+  /* 50= navbar  50  */
+  min-height: calc(100vh - 50px);
   width: 100%;
   position: relative;
   overflow: hidden;
 }
+.fixed-header+.app-main {
+  padding-top: 50px;
+}
+.hasTagsView {
+  .app-main {
+    /* 84 = navbar + tags-view = 50 + 34 */
+    min-height: calc(100vh - 84px);
+  }
+  .fixed-header+.app-main {
+    padding-top: 84px;
+  }
+}
+</style>
+
+<style lang="scss">
+// fix css style bug in open el-dialog
+.el-popup-parent--hidden {
+  .fixed-header {
+    padding-right: 15px;
+  }
+}
 </style>
 

+ 85 - 0
litemall-admin/src/views/layout/components/ScrollPane.vue

@@ -0,0 +1,85 @@
+<template>
+  <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
+    <slot />
+  </el-scrollbar>
+</template>
+
+<script>
+const tagAndTagSpacing = 4 // tagAndTagSpacing
+
+export default {
+  name: 'ScrollPane',
+  data() {
+    return {
+      left: 0
+    }
+  },
+  computed: {
+    scrollWrapper() {
+      return this.$refs.scrollContainer.$refs.wrap
+    }
+  },
+  methods: {
+    handleScroll(e) {
+      const eventDelta = e.wheelDelta || -e.deltaY * 40
+      const $scrollWrapper = this.scrollWrapper
+      $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
+    },
+    moveToTarget(currentTag) {
+      const $container = this.$refs.scrollContainer.$el
+      const $containerWidth = $container.offsetWidth
+      const $scrollWrapper = this.scrollWrapper
+      const tagList = this.$parent.$refs.tag
+
+      let firstTag = null
+      let lastTag = null
+
+      // find first tag and last tag
+      if (tagList.length > 0) {
+        firstTag = tagList[0]
+        lastTag = tagList[tagList.length - 1]
+      }
+
+      if (firstTag === currentTag) {
+        $scrollWrapper.scrollLeft = 0
+      } else if (lastTag === currentTag) {
+        $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
+      } else {
+        // find preTag and nextTag
+        const currentIndex = tagList.findIndex(item => item === currentTag)
+        const prevTag = tagList[currentIndex - 1]
+        const nextTag = tagList[currentIndex + 1]
+
+        // the tag's offsetLeft after of nextTag
+        const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
+
+        // the tag's offsetLeft before of prevTag
+        const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
+
+        if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
+          $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
+        } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
+          $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
+        }
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.scroll-container {
+  white-space: nowrap;
+  position: relative;
+  overflow: hidden;
+  width: 100%;
+  /deep/ {
+    .el-scrollbar__bar {
+      bottom: 0px;
+    }
+    .el-scrollbar__wrap {
+      height: 49px;
+    }
+  }
+}
+</style>

+ 23 - 33
litemall-admin/src/views/layout/components/Sidebar/SidebarItem.vue

@@ -1,42 +1,31 @@
 <template>
-  <div v-if="!item.hidden&&item.children" class="menu-wrapper">
-
+  <div v-if="!item.hidden" class="menu-wrapper">
     <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
-      <app-link :to="resolvePath(onlyOneChild.path)">
+      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
         <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
-          <item v-if="onlyOneChild.meta" :icon="onlyOneChild.meta.icon||item.meta.icon" :title="onlyOneChild.meta.title" />
+          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
         </el-menu-item>
       </app-link>
     </template>
-
-    <el-submenu v-else ref="submenu" :index="resolvePath(item.path)">
+    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
       <template slot="title">
-        <item v-if="item.meta" :icon="item.meta.icon" :title="item.meta.title" />
-      </template>
-
-      <template v-for="child in item.children" v-if="!child.hidden">
-        <sidebar-item
-          v-if="child.children&&child.children.length>0"
-          :is-nest="true"
-          :item="child"
-          :key="child.path"
-          :base-path="resolvePath(child.path)"
-          class="nest-menu" />
-
-        <app-link v-else :to="resolvePath(child.path)" :key="child.name">
-          <el-menu-item :index="resolvePath(child.path)">
-            <item v-if="child.meta" :icon="child.meta.icon" :title="child.meta.title" />
-          </el-menu-item>
-        </app-link>
+        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
       </template>
+      <sidebar-item
+        v-for="child in item.children"
+        :key="child.path"
+        :is-nest="true"
+        :item="child"
+        :base-path="resolvePath(child.path)"
+        class="nest-menu"
+      />
     </el-submenu>
-
   </div>
 </template>
 
 <script>
 import path from 'path'
-import { isExternal } from '@/utils'
+import { isExternal } from '@/utils/validate'
 import Item from './Item'
 import AppLink from './Link'
 import FixiOSBug from './FixiOSBug'
@@ -61,12 +50,13 @@ export default {
     }
   },
   data() {
-    return {
-      onlyOneChild: null
-    }
+    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
+    // TODO: refactor with render function
+    this.onlyOneChild = null
+    return {}
   },
   methods: {
-    hasOneShowingChild(children, parent) {
+    hasOneShowingChild(children = [], parent) {
       const showingChildren = children.filter(item => {
         if (item.hidden) {
           return false
@@ -91,13 +81,13 @@ export default {
       return false
     },
     resolvePath(routePath) {
-      if (this.isExternalLink(routePath)) {
+      if (isExternal(routePath)) {
         return routePath
       }
+      if (isExternal(this.basePath)) {
+        return this.basePath
+      }
       return path.resolve(this.basePath, routePath)
-    },
-    isExternalLink(routePath) {
-      return isExternal(routePath)
     }
   }
 }

+ 85 - 33
litemall-admin/src/views/layout/components/TagsView.vue

@@ -1,31 +1,33 @@
 <template>
-  <div class="tags-view-container">
+  <div id="tags-view-container" class="tags-view-container">
     <scroll-pane ref="scrollPane" class="tags-view-wrapper">
       <router-link
         v-for="tag in visitedViews"
         ref="tag"
+        :key="tag.path"
         :class="isActive(tag)?'active':''"
         :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
-        :key="tag.path"
         tag="span"
         class="tags-view-item"
-        @click.middle.native="closeSelectedTag(tag)"
-        @contextmenu.prevent.native="openMenu(tag,$event)">
+        @click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
+        @contextmenu.prevent.native="openMenu(tag,$event)"
+      >
         {{ tag.title }}
-        <span class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
+        <span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
       </router-link>
     </scroll-pane>
     <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
       <li @click="refreshSelectedTag(selectedTag)">刷新</li>
-      <li @click="closeSelectedTag(selectedTag)">关闭</li>
-      <li @click="closeOthersTags">关闭其</li>
-      <li @click="closeAllTags">关闭所有</li>
+      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
+      <li @click="closeOthersTags">关闭其</li>
+      <li @click="closeAllTags(selectedTag)">关闭所有</li>
     </ul>
   </div>
 </template>
 
 <script>
-import ScrollPane from '@/components/ScrollPane'
+import ScrollPane from './ScrollPane'
+import path from 'path'
 
 export default {
   components: { ScrollPane },
@@ -34,17 +36,21 @@ export default {
       visible: false,
       top: 0,
       left: 0,
-      selectedTag: {}
+      selectedTag: {},
+      affixTags: []
     }
   },
   computed: {
     visitedViews() {
       return this.$store.state.tagsView.visitedViews
+    },
+    routes() {
+      return this.$store.state.permission.routers
     }
   },
   watch: {
     $route() {
-      this.addViewTags()
+      this.addTags()
       this.moveToCurrentTag()
     },
     visible(value) {
@@ -56,16 +62,50 @@ export default {
     }
   },
   mounted() {
-    this.addViewTags()
+    this.initTags()
+    this.addTags()
   },
   methods: {
     isActive(route) {
       return route.path === this.$route.path
     },
-    addViewTags() {
+    isAffix(tag) {
+      return tag.meta && tag.meta.affix
+    },
+    filterAffixTags(routes, basePath = '/') {
+      let tags = []
+      routes.forEach(route => {
+        if (route.meta && route.meta.affix) {
+          const tagPath = path.resolve(basePath, route.path)
+          tags.push({
+            fullPath: tagPath,
+            path: tagPath,
+            name: route.name,
+            meta: { ...route.meta }
+          })
+        }
+        if (route.children) {
+          const tempTags = this.filterAffixTags(route.children, route.path)
+          if (tempTags.length >= 1) {
+            tags = [...tags, ...tempTags]
+          }
+        }
+      })
+      return tags
+    },
+    initTags() {
+      const affixTags = this.affixTags = this.filterAffixTags(this.routes)
+      for (const tag of affixTags) {
+        // Must have tag name
+        if (tag.name) {
+          this.$store.dispatch('tagsView/addVisitedView', tag)
+        }
+      }
+    },
+    addTags() {
       const { name } = this.$route
       if (name) {
-        this.$store.dispatch('addView', this.$route)
+        this.$store.dispatch('tagsView/addView', this.$route)
       }
       return false
     },
@@ -75,19 +115,17 @@ export default {
         for (const tag of tags) {
           if (tag.to.path === this.$route.path) {
             this.$refs.scrollPane.moveToTarget(tag)
-
             // when query is different then update
             if (tag.to.fullPath !== this.$route.fullPath) {
-              this.$store.dispatch('updateVisitedView', this.$route)
+              this.$store.dispatch('tagsView/updateVisitedView', this.$route)
             }
-
             break
           }
         }
       })
     },
     refreshSelectedTag(view) {
-      this.$store.dispatch('delCachedView', view).then(() => {
+      this.$store.dispatch('tagsView/delCachedView', view).then(() => {
         const { fullPath } = view
         this.$nextTick(() => {
           this.$router.replace({
@@ -97,26 +135,40 @@ export default {
       })
     },
     closeSelectedTag(view) {
-      this.$store.dispatch('delView', view).then(({ visitedViews }) => {
+      this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
         if (this.isActive(view)) {
-          const latestView = visitedViews.slice(-1)[0]
-          if (latestView) {
-            this.$router.push(latestView)
-          } else {
-            this.$router.push('/')
-          }
+          this.toLastView(visitedViews, view)
         }
       })
     },
     closeOthersTags() {
       this.$router.push(this.selectedTag)
-      this.$store.dispatch('delOthersViews', this.selectedTag).then(() => {
+      this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
         this.moveToCurrentTag()
       })
     },
-    closeAllTags() {
-      this.$store.dispatch('delAllViews')
-      this.$router.push('/')
+    closeAllTags(view) {
+      this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
+        if (this.affixTags.some(tag => tag.path === view.path)) {
+          return
+        }
+        this.toLastView(visitedViews, view)
+      })
+    },
+    toLastView(visitedViews, view) {
+      const latestView = visitedViews.slice(-1)[0]
+      if (latestView) {
+        this.$router.push(latestView.fullPath)
+      } else {
+        // now the default is to redirect to the home page if there is no tags-view,
+        // you can adjust it according to your needs.
+        if (view.name === 'Dashboard') {
+          // to reload home page
+          this.$router.replace({ path: '/redirect' + view.fullPath })
+        } else {
+          this.$router.push('/')
+        }
+      }
     },
     openMenu(tag, e) {
       const menuMinWidth = 105
@@ -130,8 +182,8 @@ export default {
       } else {
         this.left = left
       }
-      this.top = e.clientY
 
+      this.top = e.clientY
       this.visible = true
       this.selectedTag = tag
     },
@@ -142,7 +194,7 @@ export default {
 }
 </script>
 
-<style rel="stylesheet/scss" lang="scss" scoped>
+<style lang="scss" scoped>
 .tags-view-container {
   height: 34px;
   width: 100%;
@@ -189,7 +241,7 @@ export default {
   .contextmenu {
     margin: 0;
     background: #fff;
-    z-index: 100;
+    z-index: 3000;
     position: absolute;
     list-style-type: none;
     padding: 5px 0;
@@ -210,7 +262,7 @@ export default {
 }
 </style>
 
-<style rel="stylesheet/scss" lang="scss">
+<style lang="scss">
 //reset element css of el-icon-close
 .tags-view-wrapper {
   .tags-view-item {

+ 1 - 1
litemall-admin/src/views/redirect/index.vue

@@ -1,6 +1,6 @@
 <script>
 export default {
-  beforeCreate() {
+  created() {
     const { params, query } = this.$route
     const { path } = params
     this.$router.replace({ path: '/' + path, query })