Skip to content

技术改造 - v27

TIP

由于直接上 3 需要太多的工作,先迁移到 2.7, 后续在考虑升级到 3

Vue 2.7 是 Vue 2 最新的次级版本。其提供了内置的组合式 API 支持。

在 Vue 2.7 中,从 Vue 3 移植回了最重要的一些特性,使得 Vue 2 用户也可以享有这些便利。

  • 组合式 API
  • setup
  • v-bind
  • 更多

以下列举了一些需要改造的点:

Vue CLI / webpack

  1. 将本地的 @vue/cli-xxx 依赖升级至所在主版本范围内的最新版本 (如有):
    • v4 升级至 ~4.5.18
  2. 移除 vue-template-compiler
json
{
  "name": "@mgcloud/demo-ui",
  "version": "3.2.0",
  "description": "模板项目",
  "author": "wudi",
  "scripts": {
    "dev": "vue-cli-service serve",
    "test": "vue-cli-service build --mode test",
    "build:prod": "vue-cli-service build",
    "lint": "eslint --ext .js,.vue src",
    "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
  },
  "repository": {
    "type": "git",
    "url": "http://39.108.189.115:81/general/frontend/demo-ui.git"
  },
  "dependencies": {
    "axios": "0.18.1",
    "core-js": "3.25.2",
    "element-ui": "2.15.14",
    "moment": "^2.29.1",
    "normalize.css": "7.0.0",
    "nprogress": "0.2.0",
    "vue": "2.6.14", 
    "vue-router": "3.0.2", 
    "vuex": "3.1.0", 
    "vue": "2.7.16", 
    "vue-router": "3.6.5", 
    "vuex": "3.6.2"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "4.4.4", 
    "@vue/cli-plugin-eslint": "4.4.4", 
    "@vue/cli-plugin-unit-jest": "4.4.4", 
    "@vue/cli-service": "4.4.4", 
    "@vue/cli-plugin-babel": "~4.5.18", 
    "@vue/cli-plugin-eslint": "~4.5.18", 
    "@vue/cli-plugin-unit-jest": "~4.5.18", 
    "@vue/cli-service": "~4.5.18", 
    "autoprefixer": "9.5.1",
    "babel-eslint": "10.1.0",
    "babel-jest": "^26.6.1",
    "babel-plugin-dynamic-import-node": "2.3.3",
    "eslint": "6.7.2",
    "eslint-plugin-vue": "6.2.2",
    "html-webpack-plugin": "3.2.0",
    "husky": "1.3.1",
    "lint-staged": "8.1.5",
    "sass": "1.27.0",
    "sass-loader": "^10.1.1",
    "script-ext-html-webpack-plugin": "2.1.3",
    "script-loader": "0.7.2",
    "svg-sprite-loader": "4.1.3",
    "vue-template-compiler": "2.6.14"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions"
  ]
}

项目内改造

vue
<style scoped lang="scss">
  ::v-deep.app-btn {  // [!code --]
  :deep(.app-btn) { // [!code ++]
    width: 33%;
    padding: 20px 0;
    border-radius: 10px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    font-size: 16px;
    color: #333;

    &:hover {
      :deep(.txt) {
        color: #1890ff;
      }
      :deep(.icon-box) {
        box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.45);
      }
    }

    &:nth-child(3n) {
      margin-right: 0;
    }

    &:last-child {
      margin-right: 0;
    }

    :deep(.el-card__body) {
      padding: 0;
    }
  }
</style>

遵循 vue3 新规范

移除 mixin

vue
<script>
  import AppLink from './Link.vue'
  import FixiOSBug from './FixiOSBug'
  import { isExternal } from '@/utils/validate'
  import { getNormalPath } from '@/utils/ruoyi'

  export default {
    name: 'SidebarItem',
    components: {
      AppLink,
    },
    mixins: [FixiOSBug], 
    props: {
      // route object
      item: {
        type: Object,
        required: true,
      },
      isNest: {
        type: Boolean,
        default: false,
      },
      basePath: {
        type: String,
        default: '',
      },
    },
    data() {
      this.onlyOneChild = null
      return {}
    },
    computed: { 
      device() { 
        return this.$store.state.uiSetting.device
      }, 
    }, 
    mounted() { 
      // In order to fix the click on menu on the ios device will trigger the mouseleave bug
      this.fixBugIniOS() 
    }, 
    methods: {
      fixBugIniOS() { 
        const $subMenu = this.$refs.subMenu
        if ($subMenu) { 
          const handleMouseleave = $subMenu.handleMouseleave
          $subMenu.handleMouseleave = (e) => { 
            if (this.device === 'mobile') { 
              return
            } 
            handleMouseleave(e) 
          } 
        } 
      }, 
      hasOneShowingChild(children = [], parent) {
        const showingChildren = children.filter((item) => {
          if (item.hidden) {
            return false
          } else {
            // Temp set(will be used if only has one showing child)
            this.onlyOneChild = item
            return true
          }
        })

        // When there is only one child router, the child router is displayed by default
        if (showingChildren.length === 1) {
          return true
        }

        // Show parent if there are no child router to display
        if (showingChildren.length === 0) {
          this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
          return true
        }

        return false
      },
      resolvePath(routePath) {
        if (isExternal(routePath)) {
          return routePath
        }
        if (isExternal(this.basePath)) {
          return this.basePath
        }
        // return path.resolve(this.basePath, routePath)
        return getNormalPath(`${this.basePath}/${routePath}`)
      },
    },
  }
</script>

<template>
  <div v-if="!item.hidden">
    <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
      <AppLink
        v-if="onlyOneChild.meta"
        :to="resolvePath(onlyOneChild.path)"
      >
        <el-menu-item
          :index="resolvePath(onlyOneChild.path)"
          :class="{ 'submenu-title-noDropdown': !isNest }"
        >
          <svg-icon
            v-if="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"
            :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"
          />
          <span v-if="onlyOneChild.meta.title" slot="title">
            {{ onlyOneChild.meta.title }}
          </span>
        </el-menu-item>
      </AppLink>
    </template>

    <el-submenu
      v-else
      :index="resolvePath(item.path)"
      popper-append-to-body
    >
      <template slot="title">
        <template v-if="item.meta">
          <svg-icon v-if="item.meta && item.meta.icon" :icon-class="item.meta && item.meta.icon" />
          <span v-if="item.meta.title" slot="title">
            {{ item.meta.title }}
          </span>
        </template>
      </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>
vue
<script>
  import { mapState } from 'vuex'
  import { AppMain, Navbar, Sidebar, TagsView } from './components'
  import ResizeMixin from './mixin/ResizeHandler'
  import store from '@/store'

  const { body } = document
  const WIDTH = 992 // refer to Bootstrap's responsive design

  export default {
    name: 'Layout',
    components: {
      AppMain,
      Navbar,
      Sidebar,
      TagsView,
    },
    mixins: [ResizeMixin], 
    computed: {
      ...mapState({
        sidebar: state => state.uiSetting.sidebar,
        device: state => state.uiSetting.device,
        needTagsView: state => state.uiSetting.tagsView,
      }),
      classObj() {
        return {
          hideSidebar: !this.sidebar.opened,
          openSidebar: this.sidebar.opened,
          withoutAnimation: this.sidebar.withoutAnimation,
          mobile: this.device === 'mobile',
        }
      },
    },
    watch: { 
      $route(_route) { 
        if (this.device === 'mobile' && this.sidebar.opened) { 
          store.dispatch('uiSetting/closeSideBar', { withoutAnimation: false }) 
        } 
      }, 
    }, 
    beforeMount() { 
      window.addEventListener('resize', this.$_resizeHandler) 
    }, 
    beforeDestroy() { 
      window.removeEventListener('resize', this.$_resizeHandler) 
    }, 
    methods: {
      $_isMobile() { 
        const rect = body.getBoundingClientRect() 
        return rect.width - 1 < WIDTH
      }, 
      $_resizeHandler() { 
        if (!document.hidden) { 
          const isMobile = this.$_isMobile() 
          store.dispatch('uiSetting/toggleDevice', isMobile ? 'mobile' : 'desktop') 

          if (isMobile) { 
            store.dispatch('uiSetting/closeSideBar', { withoutAnimation: true }) 
          } 
        } 
      }, 
      handleClickOutside() {
        this.$store.dispatch('app/closeSideBar', {
          withoutAnimation: false,
        })
      },
    },
  }
</script>

<template>
  <div
    :class="classObj"
    class="app-wrapper"
  >
    <div
      v-if="device === 'mobile' && sidebar.opened"
      class="drawer-bg"
      @click="handleClickOutside"
    ></div>
    <Sidebar class="sidebar-container" />
    <div
      :class="{ hasTagsView: needTagsView }"
      class="main-container"
    >
      <div>
        <Navbar />
        <TagsView v-if="needTagsView" />
      </div>
      <AppMain />
    </div>
  </div>
</template>