├── .env.github ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── App.vue ├── main.js ├── router │ ├── index.js │ └── routes.js ├── store.js ├── utils │ ├── base.js │ ├── config.js │ ├── dynamic-router │ │ ├── delete-local-router.js │ │ ├── insert-router.js │ │ ├── refresh-router.js │ │ ├── remove-router.js │ │ └── reset-router.js │ └── index.js └── views │ ├── AsideArea │ └── index.vue │ ├── DashBoard │ └── index.vue │ ├── Docs │ └── index.vue │ ├── GoodDetail │ └── GoodDetail.vue │ ├── HeaderArea │ └── index.vue │ ├── Main.vue │ └── TabView │ └── index.vue ├── tests └── unit │ ├── .eslintrc.js │ └── example.spec.js └── vue.config.js /.env.github: -------------------------------------------------------------------------------- 1 | NODE_ENV = 'production' 2 | BASE_URL = 'vue-multiple-tabs' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: stable 3 | 4 | cache: 5 | directories: 6 | - node_modules 7 | 8 | before_install: 9 | - export TZ='Asia/Beijing' # 更改时区 10 | 11 | install: 12 | - npm install 13 | 14 | script: 15 | - npm run build:github 16 | 17 | after_script: 18 | - cd ./dist 19 | - git init 20 | - git config user.name "BiYuqi" 21 | - git config user.email "biyuqiwan@163.com" 22 | - git add . 23 | - git commit -m "Travis CI Auto Builder at `date +"%Y-%m-%d %H:%M"`" 24 | - git push --force --quiet "https://${VueMulityTab}@${GH_REF}" master:gh-pages 25 | 26 | branches: 27 | only: 28 | - master 29 | env: 30 | global: 31 | - GH_REF: github.com/BiYuqi/vue-multiple-tabs.git -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 LoadingMore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [vue-multiple-tabs](http://loadingmore.com/vue-multiple-tabs) 2 | ## 介绍 3 | > 该工程是基于vue的动态路由案例,复用一个组件,打开多个tab标签页,实现商品类组件复用,可以多tab打开 4 | 5 | ## TODO 6 | - [x] refactor base on vue-cli3 2019.9.21 7 | 8 | ## 特性 9 | - [x] 复用组件多tab打开 10 | - [x] 刷新动态路由页面不丢失 11 | - [x] 支持路由传参 12 | - [x] 修复刷新时,动态路由参数丢失 13 | - [x] 删除tab页签(包含注入的动态路由) 14 | - [x] 功能实现文档编写 15 | - [ ] 代码优化,整合,方便快速部署该功能 16 | - [ ] 代码Eslint 规则修改,消除warning 17 | 18 | ## 使用 19 | Placeholder... 20 | 21 | ## 预览 22 | [页面预览](vue-multiple-tabs) 23 | ![](http://oiukswkar.bkt.clouddn.com/dynamic-router.gif) 24 | 25 | ## 本地使用 26 | 27 | ``` bash 28 | # download 29 | git clone https://github.com/BiYuqi/vue-multiple-tabs-use-one-component.git 30 | # install dependencies 31 | npm install 32 | 33 | # serve with hot reload at localhost:8080 34 | npm run serve 35 | 36 | # build for production with minification 37 | npm run build 38 | ``` 39 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mutiple-tabs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "build:github": "vue-cli-service build --mode github", 9 | "lint": "vue-cli-service lint", 10 | "test:unit": "vue-cli-service test:unit" 11 | }, 12 | "dependencies": { 13 | "core-js": "^2.6.5", 14 | "element-ui": "^2.12.0", 15 | "vue": "^2.6.10", 16 | "vue-router": "^3.0.3", 17 | "vuex": "^3.0.1" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "^3.0.3", 21 | "@vue/cli-plugin-eslint": "^3.0.3", 22 | "@vue/cli-plugin-unit-jest": "^3.0.3", 23 | "@vue/cli-service": "^3.0.3", 24 | "@vue/eslint-config-prettier": "^5.0.0", 25 | "@vue/test-utils": "1.0.0-beta.29", 26 | "babel-core": "7.0.0-bridge.0", 27 | "babel-eslint": "^10.0.1", 28 | "babel-jest": "^23.6.0", 29 | "eslint": "^5.16.0", 30 | "eslint-plugin-prettier": "^3.1.0", 31 | "eslint-plugin-vue": "^5.0.0", 32 | "node-sass": "^4.13.1", 33 | "prettier": "^1.18.2", 34 | "sass-loader": "^7.1.0", 35 | "vue-template-compiler": "^2.6.10" 36 | }, 37 | "eslintConfig": { 38 | "root": true, 39 | "env": { 40 | "node": true 41 | }, 42 | "extends": [ 43 | "plugin:vue/essential", 44 | "@vue/prettier" 45 | ], 46 | "rules": {}, 47 | "parserOptions": { 48 | "parser": "babel-eslint" 49 | }, 50 | "overrides": [ 51 | { 52 | "files": [ 53 | "**/__tests__/*.{j,t}s?(x)" 54 | ], 55 | "env": { 56 | "jest": true 57 | } 58 | } 59 | ] 60 | }, 61 | "postcss": { 62 | "plugins": { 63 | "autoprefixer": {} 64 | } 65 | }, 66 | "browserslist": [ 67 | "> 1%", 68 | "last 2 versions" 69 | ], 70 | "jest": { 71 | "moduleFileExtensions": [ 72 | "js", 73 | "jsx", 74 | "json", 75 | "vue" 76 | ], 77 | "transform": { 78 | "^.+\\.vue$": "vue-jest", 79 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": "jest-transform-stub", 80 | "^.+\\.jsx?$": "babel-jest" 81 | }, 82 | "transformIgnorePatterns": [ 83 | "/node_modules/" 84 | ], 85 | "moduleNameMapper": { 86 | "^@/(.*)$": "/src/$1" 87 | }, 88 | "snapshotSerializers": [ 89 | "jest-serializer-vue" 90 | ], 91 | "testMatch": [ 92 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" 93 | ], 94 | "testURL": "http://localhost/", 95 | "watchPlugins": [ 96 | "jest-watch-typeahead/filename", 97 | "jest-watch-typeahead/testname" 98 | ] 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vue项目多tab标签实战 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 43 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | import ElementUI from "element-ui"; 6 | import "element-ui/lib/theme-chalk/index.css"; 7 | 8 | Vue.use(ElementUI); 9 | 10 | // 百度统计code 11 | if (process.env.NODE_ENV === "production") { 12 | const _hmt = _hmt || []; 13 | (function() { 14 | const hm = document.createElement("script"); 15 | hm.src = "https://hm.baidu.com/hm.js?417e3468daf68dcfa33329790b8f8fbf"; 16 | const s = document.getElementsByTagName("script")[0]; 17 | s.parentNode.insertBefore(hm, s); 18 | })(); 19 | } 20 | 21 | new Vue({ 22 | router, 23 | store, 24 | render: h => h(App) 25 | }).$mount("#app"); 26 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | import utils from "@/utils/base"; 4 | import routes from "./routes"; 5 | 6 | Vue.use(VueRouter); 7 | 8 | const router = new VueRouter({ 9 | mode: "hash", 10 | routes 11 | }); 12 | 13 | router.beforeEach((to, from, next) => { 14 | next(); 15 | }); 16 | 17 | router.afterEach((to, from, next) => { 18 | utils.addOpendPage( 19 | router.app, 20 | to.name, 21 | to.params, 22 | to.query, 23 | to.meta, 24 | to.path 25 | ); 26 | }); 27 | 28 | export default router; 29 | -------------------------------------------------------------------------------- /src/router/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param isTabView 是否放入Tabs管理 3 | */ 4 | export default [ 5 | { 6 | path: "/", 7 | name: "home_index", 8 | component: () => import("@views/Main.vue"), 9 | redirect: "/dashboard", 10 | children: [ 11 | { 12 | path: "dashboard", 13 | name: "dashboard_index", 14 | component: () => import("@views/DashBoard/index.vue"), 15 | meta: { 16 | isTabView: true, 17 | title: "首页" 18 | } 19 | }, 20 | { 21 | path: "docs", 22 | name: "docs_index", 23 | component: () => import("@views/Docs/index.vue"), 24 | meta: { 25 | isTabView: true, 26 | title: "文档使用" 27 | } 28 | } 29 | ] 30 | } 31 | ]; 32 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import { storeSetting } from "./utils/config"; 4 | 5 | Vue.use(Vuex); 6 | 7 | const { storeName } = storeSetting 8 | 9 | const state = { 10 | /** 11 | * 默认tabview 首页 12 | */ 13 | pageOpendList: [ 14 | { 15 | path: "/dashboard", 16 | name: "dashboard_index", 17 | component: () => import("@views/DashBoard"), 18 | meta: { 19 | title: "首页", 20 | isTabView: true 21 | } 22 | } 23 | ] 24 | }; 25 | const mutations = { 26 | /** 27 | * 初始化设置tab 一般默认首页,页面加载时调用 28 | * @method tabOpendListInit 29 | */ 30 | setOpenedList(state) { 31 | const local = 32 | sessionStorage[storeName] && 33 | JSON.parse(sessionStorage[storeName]).length > 0; 34 | if (local) { 35 | state.pageOpendList = JSON.parse(sessionStorage[storeName]); 36 | } 37 | }, 38 | /** 39 | * 每次打开页面都会经过此方法,用于合并参数 40 | * @method setPageOpendList 41 | */ 42 | setPageOpendList(state, res) { 43 | const { index, query, params, meta, path } = res; 44 | let opendPage = state.pageOpendList[index]; 45 | if (params) { 46 | opendPage.params = params; 47 | } 48 | if (query) { 49 | opendPage.query = query; 50 | } 51 | if (meta) { 52 | opendPage.meta = meta; 53 | } 54 | if (path) { 55 | opendPage.path = path; 56 | } 57 | state.pageOpendList.splice(index, 1, opendPage); 58 | sessionStorage[storeName] = JSON.stringify(state.pageOpendList); 59 | }, 60 | increateTag(state, tag) { 61 | state.pageOpendList.push(tag); 62 | }, 63 | /** 64 | * @param {*} state 65 | * @param {当前页签信息} obj 66 | * @param { 当前实例 } obj.vm 67 | * @param { 路由name} obj.name 68 | */ 69 | closeOpendList(state, obj) { 70 | // 临时解决方案 后续再完善 71 | const lists = state.pageOpendList; 72 | if (obj.name === "dashboard_index") { 73 | return; 74 | } 75 | for (let i = 0; i < lists.length; i++) { 76 | if (lists[i].name === obj.name) { 77 | const lastName = state.pageOpendList[i - 1].name; 78 | state.pageOpendList.splice(i, 1); 79 | sessionStorage.setItem( 80 | storeName, 81 | JSON.stringify(state.pageOpendList) 82 | ); 83 | obj.vm.$router.push({ 84 | name: lastName 85 | }); 86 | } 87 | } 88 | } 89 | }; 90 | 91 | const store = new Vuex.Store({ 92 | state, 93 | mutations 94 | }); 95 | 96 | export default store; 97 | -------------------------------------------------------------------------------- /src/utils/base.js: -------------------------------------------------------------------------------- 1 | import store from "@/store"; 2 | export default { 3 | /** 4 | * @method addOpendPage 5 | * @param vm 当前实例 6 | * @param name 当前路由name 7 | * @param query 查询参数 8 | * @param param 查询参数 9 | * 一般放在router BeforeAfter(BeforeEach) 执行 10 | */ 11 | addOpendPage: (vm, name, params = "", query = "", meta = "", path = "") => { 12 | let pageOpendList = store.state.pageOpendList; 13 | let opendLen = pageOpendList.length; 14 | let i = 0; 15 | let tagHasOpened = false; 16 | if (opendLen > 0) { 17 | for (; i < opendLen; i++) { 18 | if (name === pageOpendList[i].name) { 19 | vm.$store.commit("setPageOpendList", { 20 | index: i, 21 | params, 22 | query, 23 | meta 24 | }); 25 | tagHasOpened = true; 26 | break; 27 | } 28 | } 29 | } 30 | /** 31 | * 注入参数 32 | */ 33 | if (!tagHasOpened && name) { 34 | let tag = { 35 | name: name 36 | }; 37 | if (params) { 38 | tag.params = params; 39 | } 40 | if (query) { 41 | tag.query = query; 42 | } 43 | if (meta && meta.isTabView) { 44 | tag.meta = meta; 45 | } else if (meta && !meta.isTabView) { 46 | return; 47 | } 48 | if (path) { 49 | tag.path = path; 50 | } 51 | store.commit("increateTag", tag); 52 | } 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @storeSetting 3 | * 本地存储sessionStorage键值 4 | * @param {storeName} String 打开tab的本地存储name 5 | * @param {dynamicName} String 打开tab的动态路由存储name 6 | */ 7 | export const storeSetting = { 8 | storeName: "tab-view-router-list", 9 | dynamicName: "dynamic-router-list" 10 | }; 11 | 12 | /** 13 | * @description 14 | * 动态路由配置在此处 15 | * 可以全局使用 16 | */ 17 | export default { 18 | GoodDetail: () => import("@views/GoodDetail/GoodDetail.vue") 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/dynamic-router/delete-local-router.js: -------------------------------------------------------------------------------- 1 | import { storeSetting } from "../config"; 2 | 3 | /** 4 | * @param {name} 动态路由编号,提交后删除本地存储的路由 5 | */ 6 | export const deleteLocalRouter = name => { 7 | const { dynamicName } = storeSetting; 8 | const localRoutes = 9 | sessionStorage.getItem(dynamicName) && 10 | JSON.parse(sessionStorage.getItem(dynamicName)); 11 | if (localRoutes && localRoutes.length > 0) { 12 | for (let i = 0; i < localRoutes.length; i++) { 13 | if (localRoutes[i].name === name) { 14 | localRoutes.splice(i, 1); 15 | sessionStorage.setItem(dynamicName, JSON.stringify(localRoutes)); 16 | break; 17 | } 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/utils/dynamic-router/insert-router.js: -------------------------------------------------------------------------------- 1 | import Main from "@views/Main.vue"; 2 | import configMapping from "../config"; 3 | import { storeSetting } from "../config"; 4 | 5 | const { dynamicName } = storeSetting; 6 | 7 | const addRouter = param => { 8 | const vm = param.vm; 9 | const com = param.com; 10 | const name = param.name; 11 | const params = param.params; 12 | const query = param.query; 13 | const component = param.component; 14 | const tab = { 15 | name: name, 16 | path: "/" + name, 17 | component: component, 18 | meta: { 19 | component: com, 20 | title: name, 21 | isTabView: true, 22 | params, 23 | query 24 | } 25 | }; 26 | /** 27 | * 动态路由 28 | */ 29 | // 判断是否已经存在该路由 30 | let flag = false; 31 | const routes = []; 32 | const routerItem = { 33 | path: "/", 34 | name: dynamicName, 35 | component: Main, 36 | children: [] 37 | }; 38 | const dynamic = 39 | (sessionStorage.getItem(dynamicName) && 40 | JSON.parse(sessionStorage.getItem(dynamicName))) || 41 | []; 42 | if (dynamic.length > 0) { 43 | const len = dynamic.length; 44 | for (let i = 0; i < len; i++) { 45 | if (dynamic[i].name === name) { 46 | flag = true; 47 | break; 48 | } 49 | } 50 | } 51 | /** 52 | * 如果未打开,则新增路由 53 | */ 54 | if (!flag) { 55 | routerItem.children.push(tab); 56 | routes.push(routerItem); 57 | dynamic.push(tab); 58 | sessionStorage.setItem(dynamicName, JSON.stringify(dynamic)); 59 | vm.$router.addRoutes(routes); 60 | } 61 | /** 62 | * 跳转路由 63 | */ 64 | vm.$router.push({ 65 | name: name, 66 | params, 67 | query 68 | }); 69 | }; 70 | 71 | /** 72 | * 这里只是针对一个组件进行复用,实可根据业务进行动态传入组件name 73 | * 具体请看@/pages/DashBoard/index.vue 具体用法 74 | * @param {路由信息对象} message 75 | */ 76 | export const insertRouter = message => { 77 | var obj = { 78 | vm: message.vm, 79 | component: configMapping[message.component], 80 | com: message.com, 81 | name: message.name, 82 | params: message.params, 83 | query: message.query 84 | }; 85 | addRouter(obj); 86 | }; 87 | -------------------------------------------------------------------------------- /src/utils/dynamic-router/refresh-router.js: -------------------------------------------------------------------------------- 1 | import Main from "@views/Main.vue"; 2 | import configMapping from "../config"; 3 | import { storeSetting } from "../config"; 4 | 5 | const { dynamicName } = storeSetting; 6 | 7 | /** 8 | * 防止页面刷新时, 路由丢失问题 9 | * @param { routerMap } 动态路由模板映射 10 | * @param { routes } 空数组,因为addRoutes只支持数组 11 | * @param { childrens } children 不多解释 12 | * @param {当前实例} vm 13 | */ 14 | export const refreshRouterSync = vm => { 15 | // 此处正则提取的是路由的文件夹名,请注意,是为了map映射取key, 会得到GoodDetail 16 | // GoodDetail: () => import('@views/GoodDetail/GoodDetail.vue') 17 | const dynamic = 18 | (sessionStorage.getItem(dynamicName) && 19 | JSON.parse(sessionStorage.getItem(dynamicName))) || 20 | []; 21 | const routes = []; 22 | const routerItem = { 23 | path: "/", 24 | name: dynamicName, 25 | component: Main, 26 | children: [] 27 | }; 28 | if (dynamic.length > 0) { 29 | for (let i = 0; i < dynamic.length; i++) { 30 | const FullPath = dynamic[i].meta.component; 31 | const mapName = FullPath.substring( 32 | FullPath.lastIndexOf("/") + 1, 33 | FullPath.lastIndexOf(".") 34 | ); 35 | dynamic[i].component = configMapping[mapName]; 36 | routerItem.children.push(dynamic[i]); 37 | } 38 | routes.push(routerItem); 39 | vm.$router.addRoutes(routes); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/utils/dynamic-router/remove-router.js: -------------------------------------------------------------------------------- 1 | import { resetRouter } from "./reset-router"; 2 | import { deleteLocalRouter } from "./delete-local-router"; 3 | import { refreshRouterSync } from "./refresh-router"; 4 | import store from "@/store"; 5 | 6 | export function removeRouter(vm, name) { 7 | deleteLocalRouter(name); 8 | resetRouter(vm); 9 | refreshRouterSync(vm); 10 | store.commit("closeOpendList", { 11 | vm, 12 | name 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/dynamic-router/reset-router.js: -------------------------------------------------------------------------------- 1 | import VueRouter from "vue-router"; 2 | import mainRoutes from "@/router/routes"; 3 | 4 | export const resetRouter = vm => { 5 | const routes = [...mainRoutes]; 6 | let newRouter = new VueRouter({ 7 | mode: "hash", 8 | routes 9 | }); 10 | vm.$router.matcher = newRouter.matcher; 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { removeRouter } from "./dynamic-router/remove-router"; 2 | import { insertRouter } from "./dynamic-router/insert-router"; 3 | import { refreshRouterSync } from "./dynamic-router/refresh-router"; 4 | 5 | export default { 6 | removeRouter, 7 | insertRouter, 8 | refreshRouterSync 9 | }; 10 | -------------------------------------------------------------------------------- /src/views/AsideArea/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 32 | -------------------------------------------------------------------------------- /src/views/DashBoard/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 53 | 54 | 60 | -------------------------------------------------------------------------------- /src/views/Docs/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /src/views/GoodDetail/GoodDetail.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /src/views/HeaderArea/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 33 | -------------------------------------------------------------------------------- /src/views/Main.vue: -------------------------------------------------------------------------------- 1 | 11 | 26 | 40 | -------------------------------------------------------------------------------- /src/views/TabView/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 64 | 65 | 89 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from "@vue/test-utils"; 2 | 3 | describe("Null", () => { 4 | it("renders correct", () => { 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const resolve = dir => path.join(__dirname, dir); 3 | const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV); 4 | 5 | module.exports = { 6 | publicPath: IS_PROD ? process.env.BASE_URL : "/", 7 | productionSourceMap: false, 8 | chainWebpack: config => { 9 | // 添加别名 10 | config.resolve.alias 11 | .set("vue$", "vue/dist/vue.esm.js") 12 | .set("@", resolve("src")) 13 | .set("@assets", resolve("src/assets")) 14 | .set("@views", resolve("src/views")) 15 | .set("@router", resolve("src/router")); 16 | } 17 | }; 18 | --------------------------------------------------------------------------------