├── .eslintignore ├── .stylelintignore ├── .browserslistrc ├── src ├── components │ ├── frames │ │ ├── Default.vue │ │ ├── Reuse.vue │ │ ├── Append.vue │ │ ├── MaxAlive.vue │ │ ├── Dragsort.vue │ │ ├── CloseLastTab.vue │ │ ├── PageScroller.vue │ │ ├── Restore.vue │ │ ├── Iframe.vue │ │ ├── Transition.vue │ │ ├── LangEn.vue │ │ ├── LangCustom.vue │ │ ├── InitialTabs.vue │ │ ├── I18n.vue │ │ ├── Contextmenu.vue │ │ └── Slot.vue │ ├── PageRouteInfo.vue │ ├── PageTimer.vue │ ├── AppAside.vue │ ├── MenuItem.vue │ ├── SiteLink.vue │ └── AppHeader.vue ├── views │ ├── Page1.vue │ ├── Page2.vue │ ├── 404.vue │ ├── ScrollMulti.vue │ ├── I18n.vue │ ├── ScrollAsync.vue │ ├── ScrollPosition.vue │ ├── TabDynamic.vue │ ├── Nest.vue │ ├── Page.vue │ ├── PageLeave.vue │ ├── IframeOperate.vue │ └── Rule.vue ├── utils │ ├── index.js │ └── extendRoutes.js ├── router │ ├── 404.js │ ├── index.js │ ├── frames.js │ └── page.js ├── mixins │ └── fullscreen.js ├── main.js ├── assets │ └── scss │ │ ├── variables.scss │ │ └── demo.scss ├── config │ └── menu.js └── App.vue ├── public ├── img │ ├── logo.png │ └── logo.psd └── index.html ├── jsconfig.json ├── .prettierrc.js ├── lib ├── config │ ├── rules.js │ ├── lang │ │ ├── index.js │ │ ├── zh.js │ │ └── en.js │ ├── routes.js │ └── contextmenu.js ├── util │ ├── decorator.js │ ├── warn.js │ ├── RouteMatch.js │ └── index.js ├── scss │ └── transition.scss ├── index.js ├── mixins │ ├── scroll.js │ ├── iframe.js │ ├── contextmenu.js │ ├── i18n.js │ ├── restore.js │ ├── routerPage.js │ └── pageLeave.js ├── components │ ├── ContextmenuItem.vue │ ├── Contextmenu.vue │ └── TabItem.js ├── page │ └── Iframe.vue └── RouterTab.vue ├── babel.config.js ├── .gitignore ├── .travis.yml ├── docs ├── zh │ ├── guide │ │ ├── advanced │ │ │ ├── initial-tabs.md │ │ │ ├── restore.md │ │ │ ├── cache.md │ │ │ ├── page-leave.md │ │ │ └── dynamic-tab-info.md │ │ ├── custom │ │ │ ├── contextmenu.md │ │ │ ├── README.md │ │ │ ├── transition.md │ │ │ ├── slot.md │ │ │ ├── scroll.md │ │ │ └── i18n.md │ │ ├── meta │ │ │ ├── faqs.md │ │ │ ├── solutions.md │ │ │ └── uninstall.md │ │ ├── essentials │ │ │ ├── iframe.md │ │ │ ├── installation.md │ │ │ ├── rule.md │ │ │ ├── nuxt.md │ │ │ ├── operate.md │ │ │ └── README.md │ │ └── README.md │ ├── README.md │ └── api │ │ └── router-alive.md ├── .vuepress │ ├── locales │ │ ├── utils │ │ │ ├── nav.js │ │ │ └── sidebar.js │ │ ├── zh.js │ │ └── en.js │ ├── components │ │ ├── DocLinks.vue │ │ └── DemoLink.vue │ ├── styles │ │ └── index.styl │ └── config.js ├── README.md ├── guide │ ├── essentials │ │ ├── iframe.md │ │ ├── installation.md │ │ ├── rule.md │ │ ├── nuxt.md │ │ ├── operate.md │ │ └── README.md │ ├── meta │ │ ├── faqs.md │ │ ├── solutions.md │ │ └── uninstall.md │ ├── advanced │ │ ├── restore.md │ │ ├── cache.md │ │ ├── dynamic-tab-info.md │ │ ├── page-leave.md │ │ └── initial-tabs.md │ ├── custom │ │ ├── README.md │ │ ├── slot.md │ │ ├── transition.md │ │ ├── scroll.md │ │ ├── i18n.md │ │ └── contextmenu.md │ └── README.md └── api │ └── router-alive.md ├── .github └── ISSUE_TEMPLATE │ ├── feature-request_zh-cn.md │ ├── feature-request.md │ ├── bug-report_zh-cn.md │ └── bug-report.md ├── .npmignore ├── vue.config.js ├── scripts └── verify-commit-msg.js ├── .stylelintrc.js ├── .eslintrc.js ├── LICENSE ├── vetur └── tags.json ├── package.json ├── dist └── lib │ └── vue-router-tab.css └── README.zh.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /src/components/frames/Default.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/frames/Reuse.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhuh12/vue-router-tab/HEAD/public/img/logo.png -------------------------------------------------------------------------------- /public/img/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhuh12/vue-router-tab/HEAD/public/img/logo.psd -------------------------------------------------------------------------------- /src/components/frames/Append.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/frames/MaxAlive.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/frames/Dragsort.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/frames/CloseLastTab.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/views/Page1.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/views/Page2.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | }, 5 | "exclude": ["node_modules", "dist"] 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | // 异步加载页面组件 2 | export const importPage = view => () => 3 | import( 4 | /* webpackChunkName: "p-[request]" */ 5 | '../views/' + view + '.vue' 6 | ) 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | eslintIntegration: true, 5 | arrowParens: 'avoid', 6 | trailingComma: 'none', 7 | endOfLine: 'auto' 8 | } 9 | -------------------------------------------------------------------------------- /src/router/404.js: -------------------------------------------------------------------------------- 1 | import { importPage } from '../utils' 2 | 3 | // 404 路由 4 | export default { 5 | path: '404', 6 | component: importPage('404'), 7 | meta: { 8 | title: '找不到页面', 9 | icon: 'rt-icon-warning' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/extendRoutes.js: -------------------------------------------------------------------------------- 1 | // ! 引入 RouterTab 扩展路由 2 | import { RouterTabRoutes } from '../../lib' 3 | import route404 from '../router/404' 4 | 5 | // 子路由扩展 6 | export default route => { 7 | route.children.push(route404, ...RouterTabRoutes) 8 | } 9 | -------------------------------------------------------------------------------- /lib/config/rules.js: -------------------------------------------------------------------------------- 1 | import { prunePath } from '../util' 2 | 3 | // 内置规则 4 | export default { 5 | // 地址,params 不一致则独立缓存 6 | path: route => route.path, 7 | 8 | // 完整地址 (忽略 hash),params 或 query 不一致则独立缓存 9 | fullpath: route => prunePath(route.fullPath) 10 | } 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // 构建目标是否为库 2 | const isBuildLib = process.env.VUE_CLI_BUILD_TARGET === 'lib' 3 | 4 | module.exports = { 5 | presets: [ 6 | [ 7 | '@vue/cli-plugin-babel/preset', 8 | { 9 | useBuiltIns: isBuildLib ? false : 'usage' 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /lib/config/lang/index.js: -------------------------------------------------------------------------------- 1 | // 引入目录下语言配置 2 | const context = require.context('./', false, /^((?!index).)*\.js$/) 3 | 4 | // 语言配置 5 | export default context.keys().reduce((map, path) => { 6 | let [, key] = /\.\/(.*).js/g.exec(path) 7 | map[key] = context(path).default 8 | return map 9 | }, {}) 10 | -------------------------------------------------------------------------------- /src/mixins/fullscreen.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data() { 3 | return { 4 | // 是否全屏 5 | fullscreen: false 6 | } 7 | }, 8 | 9 | watch: { 10 | // 切换全屏后更新滚动 11 | async fullscreen() { 12 | await this.$nextTick() 13 | this.$tabs.adjust() 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/config/routes.js: -------------------------------------------------------------------------------- 1 | import Iframe, { iframeMeta } from '../page/Iframe.vue' 2 | 3 | // 注入的路由 4 | export default [ 5 | { 6 | // iframe 路由 7 | path: 'iframe/:src/:title?/:icon?', 8 | component: Iframe, 9 | props: true, 10 | meta: iframeMeta 11 | } 12 | ] 13 | 14 | export { Iframe } 15 | -------------------------------------------------------------------------------- /src/components/frames/PageScroller.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/views/404.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import RouterTab from '../lib' 3 | 4 | import App from './App.vue' 5 | import router from './router' 6 | 7 | Object.assign(Vue.config, { 8 | productionTip: false, 9 | devtools: true 10 | }) 11 | 12 | Vue.use(RouterTab) 13 | 14 | new Vue({ 15 | router, 16 | render: h => h(App) 17 | }).$mount('#app') 18 | -------------------------------------------------------------------------------- /src/views/ScrollMulti.vue: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # local env files 4 | .env.local 5 | .env.*.local 6 | 7 | # Log files 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Editor directories and files 13 | .idea 14 | .vscode 15 | *.suo 16 | *.ntvs* 17 | *.njsproj 18 | *.sln 19 | *.sw* 20 | 21 | # npm 包 22 | node_modules 23 | 24 | # 发布输出目录 25 | dist/docs 26 | dist/lib/**.html 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | script: 5 | - npm run docs:build 6 | - npm run demo:build 7 | deploy: 8 | provider: pages 9 | skip-cleanup: true 10 | local_dir: dist/docs 11 | github-token: $GITHUB_TOKEN # a token generated on github allowing travis to push code on you repository 12 | keep-history: false 13 | on: 14 | branch: main 15 | -------------------------------------------------------------------------------- /lib/config/lang/zh.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tab: { 3 | untitled: '无标题' 4 | }, 5 | contextmenu: { 6 | refresh: '刷新', 7 | refreshAll: '刷新全部', 8 | close: '关闭', 9 | closeLefts: '关闭左侧', 10 | closeRights: '关闭右侧', 11 | closeOthers: '关闭其他' 12 | }, 13 | msg: { 14 | keepLastTab: '至少应保留1个页签', 15 | i18nProp: '请提供“i18n”方法以处理国际化内容' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/zh/guide/advanced/initial-tabs.md: -------------------------------------------------------------------------------- 1 | # 初始展示页签 2 | 3 | 通过配置 RouterTab 组件的 `tabs` 属性,可以设置进入页面时默认显示的页签。 4 | 5 | ::: warning 6 | Nuxt 项目中,页签的配置如果来自于页面 `meta`,将无法自动获取未激活页签的配置。因此,这种场景不能仅通过 `fullpath` 方式配置初始页签。 7 | ::: 8 | 9 | 10 | 11 | **示例:** 12 | 13 | <<< @/src/components/frames/InitialTabs.vue 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request_zh-cn.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能建议 3 | about: 为项目提供功能建议,让 Vue Router Tab 更强大 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | --- 8 | 9 | ## 您的需求是否与问题有关? 10 | 11 | 简明扼要地描述了问题所在。例如:[...]总是令我感到沮丧 12 | 13 | ## 描述你想要的解决方案 14 | 15 | 简明扼要地描述您想要发生的事情。 16 | 17 | ## 描述您考虑过的替代方案 18 | 19 | 对您考虑的任何替代解决方案或功能的简明扼要描述。 20 | 21 | ## 附加内容 22 | 23 | 在此处添加有关功能需求的任何其他内容或屏幕截图。 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # 忽略目录/文件 2 | dist/docs 3 | dist/lib/**.html 4 | docs 5 | lib 6 | node_modules 7 | public 8 | scripts 9 | src 10 | 11 | # 忽略指定文件 12 | yarn.lock 13 | babel.config.js 14 | tsconfig.json 15 | tslint.json 16 | jsconfig.json 17 | vue.config.js 18 | 19 | .eslintrc.js 20 | .eslintignore 21 | .prettierrc.js 22 | .browserslistrc 23 | 24 | .github 25 | .vscode 26 | .editorconfig 27 | .travis.yml 28 | -------------------------------------------------------------------------------- /src/assets/scss/variables.scss: -------------------------------------------------------------------------------- 1 | $color: #42b983; 2 | $color-primary: #409eff; 3 | 4 | $text: #2c3e50; 5 | 6 | // 移动端尺寸 7 | $smallScreen: 767.98px; 8 | 9 | // 移动端样式 10 | @mixin screen-mob { 11 | @media screen and (max-width: $smallScreen) { 12 | @content; 13 | } 14 | } 15 | 16 | // PC端样式 17 | @mixin screen-pc { 18 | @media screen and (min-width: $smallScreen) { 19 | @content; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/views/I18n.vue: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /lib/util/decorator.js: -------------------------------------------------------------------------------- 1 | /* 装饰器 */ 2 | 3 | /** 4 | * 防抖 5 | * @param {number} [delay=200] 延迟 6 | */ 7 | export const debounce = (delay = 200) => (target, name, desc) => { 8 | const fn = desc.value 9 | let timeout = null 10 | 11 | desc.value = function(...rest) { 12 | let context = this 13 | clearTimeout(timeout) 14 | timeout = setTimeout(() => { 15 | fn.call(context, ...rest) 16 | }, delay) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/frames/Restore.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /lib/config/lang/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tab: { 3 | untitled: 'Untitled' 4 | }, 5 | contextmenu: { 6 | refresh: 'Refresh', 7 | refreshAll: 'Refresh All', 8 | close: 'Close', 9 | closeLefts: 'Close to the Left', 10 | closeRights: 'Close to the Right', 11 | closeOthers: 'Close Others' 12 | }, 13 | msg: { 14 | keepLastTab: 'Keep at least 1 tab', 15 | i18nProp: 'Method "i18n" is not defined on the instance' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/views/ScrollAsync.vue: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /docs/.vuepress/locales/utils/nav.js: -------------------------------------------------------------------------------- 1 | exports.default = i18n => [ 2 | { text: i18n.guide, link: `${i18n.path}guide/` }, 3 | { 4 | text: 'API', 5 | ariaLabel: 'API Menu', 6 | items: [ 7 | { text: 'API - RouterTab', link: `${i18n.path}api/` }, 8 | { text: 'API - RouterAlive', link: `${i18n.path}api/router-alive/` } 9 | ] 10 | }, 11 | { text: 'Demo', link: '/vue-router-tab/demo/', target: '_blank' }, 12 | { text: i18n.changelog, link: `${i18n.path}guide/changelog` } 13 | ] 14 | -------------------------------------------------------------------------------- /docs/zh/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: /demo/img/logo.png 4 | heroText: Vue Router Tab 5 | tagline: 基于 Vue Router 的路由页签组件。 6 | actionText: 快速上手 → 7 | actionLink: ./guide/ 8 | features: 9 | - title: 简洁至上 10 | details: 配置简单,引入组件后通过路由的 "meta" 元信息配置页签信息即可运行。 11 | - title: 规则定制 12 | details: 方便定制页签规则。除了默认的规则,您还可以全局和精准控制每个路由的页签打开方式。 13 | - title: Vue Router 驱动 14 | details: 页签响应 "Vue Router" 路由,您可以使用 "Vue Router" 提供的强大功能。 15 | footer: MIT Licensed | Copyright (c) 2019-present, 碧海幽虹 16 | --- 17 | -------------------------------------------------------------------------------- /src/components/frames/Iframe.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /lib/util/warn.js: -------------------------------------------------------------------------------- 1 | const prefix = '[Vue Router Tab]' 2 | 3 | // 错误 4 | export function assert(condition, message) { 5 | if (!condition) { 6 | throw new Error(`${prefix} ${message}`) 7 | } 8 | } 9 | 10 | // 警告 11 | export function warn(condition, message) { 12 | if (!condition) { 13 | typeof console !== 'undefined' && console.warn(`${prefix} ${message}`) 14 | } 15 | } 16 | 17 | // 常用消息 18 | export const messages = { 19 | renamed(newName, target = '方法') { 20 | return `该${target}已更名为“${newName}”,请修改后使用` 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/zh/guide/custom/contextmenu.md: -------------------------------------------------------------------------------- 1 | # 右键菜单 2 | 3 | ## 禁用右键菜单 4 | 5 | 你可以通过配置 `:contextmenu="false"` 来禁用右键菜单 6 | 7 | **示例:** 8 | 9 | ```html 10 | 11 | ``` 12 | 13 | ## 自定义右键菜单 14 | 15 | 通过数组方式配置 `contextmenu`,可以自定义右键菜单 16 | 17 | ::: tip 18 | 参考:[内置右键菜单](https://github.com/bhuh12/vue-router-tab/blob/main/lib/config/contextmenu.js) 19 | ::: 20 | 21 | 22 | 23 | **示例:** 24 | 25 | <<< @/src/components/frames/Contextmenu.vue 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: /demo/img/logo.png 4 | heroText: Vue Router Tab 5 | tagline: Vue.js tab component, based on Vue Router. 6 | actionText: Get Started → 7 | actionLink: ./guide/ 8 | features: 9 | - title: Easy to use 10 | details: Import, set meta, and good to go. 11 | - title: Customizable 12 | details: Completely customizable on every single route. 13 | - title: Powered by Vue Router 14 | details: Take good advantage of Vue Router. 15 | footer: MIT Licensed | Copyright (c) 2019-present, 碧海幽虹 16 | --- 17 | -------------------------------------------------------------------------------- /lib/scss/transition.scss: -------------------------------------------------------------------------------- 1 | // transition 过渡样式 2 | .router-tab-zoom { 3 | &-enter-active, 4 | &-leave-active { 5 | transition: all 0.4s; 6 | } 7 | 8 | &-enter, 9 | &-leave-to { 10 | transform: scale(0); 11 | opacity: 0; 12 | } 13 | } 14 | 15 | // 页面交换 16 | .router-tab-swap { 17 | $trans: 30px; // 移动位置 18 | 19 | &-enter-active, 20 | &-leave-active { 21 | transition: all 0.5s; 22 | } 23 | 24 | &-enter, 25 | &-leave-to { 26 | opacity: 0; 27 | } 28 | 29 | &-enter { 30 | transform: translateX(-$trans); 31 | } 32 | 33 | &-leave-to { 34 | transform: translateX($trans); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | // 构建目标是否为库 2 | const isBuildLib = 3 | (process.env.npm_lifecycle_script || '').indexOf('--target lib') > 0 4 | 5 | module.exports = { 6 | publicPath: '', // 相对路径 7 | 8 | outputDir: isBuildLib ? 'dist/lib' : 'dist/docs/demo', 9 | 10 | // webpack 链式配置 11 | chainWebpack: config => { 12 | // 移除 prefetch 插件 13 | config.plugins.delete('prefetch') 14 | }, 15 | 16 | css: { 17 | loaderOptions: { 18 | sass: { 19 | // scss公共变量 20 | prependData: isBuildLib 21 | ? undefined 22 | : `@use "src/assets/scss/variables.scss" as *;` 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/zh/guide/advanced/restore.md: -------------------------------------------------------------------------------- 1 | # 刷新后还原页签 2 | 3 | 给 RouterTab 组件设置 `restore` 属性,可以设置在浏览器刷新后还原页签。 4 | 5 | RouterTab 通过 sessionStorage 来存储页签缓存信息 6 | 7 | 8 | 9 | **默认方式** 10 | 11 | ```html 12 | 13 | ``` 14 | 15 | **自定义缓存** 16 | 17 | RouterTab 支持自定义本地存储的 key,根据给定的 key 来获取对应的缓存 18 | 19 | 在实际应用中,我们希望根据当前用户来存储浏览器页签信息。 20 | 21 | ```html 22 | 23 | ``` 24 | 25 | **监听 restore 参数** 26 | 27 | 通常,我们的数据会从服务端异步获取,如果我们希望在用户数据获取到后再根据用户还原页签,可以配置 `restore-watch` 来监听 restore 参数,改变后自动还原对应用户的页签 28 | 29 | ```html 30 | 31 | ``` 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /src/components/frames/Transition.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 35 | -------------------------------------------------------------------------------- /src/components/frames/LangEn.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | -------------------------------------------------------------------------------- /docs/zh/guide/meta/faqs.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 | ### 📣 RouterTab 不支持多层嵌套路由生成页签 ([issues 32](https://github.com/bhuh12/vue-router-tab/issues/32)) 4 | 5 | RouterTab 控件是有意设计成这样的,只有包含 RouterTab 组件的路由的**直接子路由**才参与生成页签页面,再嵌套的下级路由跟 Vue Router 中一样展现。 6 | 7 | 试想一下,一个页签页面内部还有**子页签**控制页面展示,并且子页签也需要响应路由,这种场景是必须嵌套路由支持的。 8 | 9 | 所有的页签路由都直接放在同一层会很杂乱,我们可以使用 `...` 展开运算符,将不同模块的路由配置合并引入: 10 | 11 | ```javascript 12 | // RouterTab 内置路由 13 | import { RouterTabRoutes } from 'vue-router-tab' 14 | 15 | const news = [{...}] 16 | const product = [{...}] 17 | 18 | const routes = [ 19 | { 20 | path: '/', 21 | component: Frame, 22 | children: [ 23 | ...RouterTabRoutes, 24 | ...news, 25 | ...product, 26 | ] 27 | } 28 | ] 29 | ``` 30 | -------------------------------------------------------------------------------- /src/components/frames/LangCustom.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import RouterTab from './RouterTab.vue' 2 | import RouterAlive from './components/RouterAlive.vue' 3 | import RouterTabRoutes, { Iframe } from './config/routes' 4 | import routerPage from './mixins/routerPage' 5 | 6 | import './scss/routerTab.scss' 7 | import './scss/transition.scss' 8 | 9 | // 安装 10 | RouterTab.install = function install(Vue) { 11 | if (install.installed) return 12 | 13 | RouterTab.Vue = Vue 14 | install.installed = true 15 | 16 | Vue.component(RouterTab.name, RouterTab) 17 | Vue.mixin(routerPage) 18 | } 19 | 20 | // 如果浏览器环境且拥有全局Vue,则自动安装组件 21 | if (typeof window !== 'undefined' && window.Vue) { 22 | window.Vue.use(RouterTab) 23 | } 24 | 25 | export default RouterTab 26 | 27 | // 导出 28 | export { RouterAlive, RouterTabRoutes, Iframe } 29 | -------------------------------------------------------------------------------- /docs/.vuepress/locales/zh.js: -------------------------------------------------------------------------------- 1 | const nav = require('./utils/nav').default 2 | const sidebar = require('./utils/sidebar').default 3 | 4 | const i18n = { 5 | path: '/zh/', 6 | guide: '教程', 7 | changelog: '更新日志', 8 | essentials: '基础', 9 | custom: '个性化', 10 | advanced: '进阶', 11 | meta: '更多' 12 | } 13 | 14 | // 站点配置 15 | exports.default = { 16 | lang: 'zh-CN', 17 | title: 'Vue Router Tab', 18 | description: '基于 Vue Router 的路由页签组件' 19 | } 20 | 21 | // 主题配置 22 | exports.theme = { 23 | // 多语言下拉菜单的标题 24 | selectText: 'Languages', 25 | 26 | // 该语言在下拉菜单中的标签 27 | label: '简体中文', 28 | 29 | // 编辑链接文字 30 | editLinkText: '在 GitHub 上编辑此页', 31 | 32 | lastUpdated: '上次更新', 33 | 34 | // 页头导航 35 | nav: nav(i18n), 36 | 37 | // 侧边栏 38 | sidebar: sidebar(i18n) 39 | } 40 | -------------------------------------------------------------------------------- /src/components/PageRouteInfo.vue: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /docs/.vuepress/components/DocLinks.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 | 39 | -------------------------------------------------------------------------------- /docs/zh/guide/custom/README.md: -------------------------------------------------------------------------------- 1 | # 页签行为 2 | 3 | ## 拖拽排序 4 | 5 | RouterTab 默认支持页签拖拽排序,你可以通过配置 `:dragsort="false"` 来禁用该功能 6 | 7 | 8 | 9 | **示例:** 10 | 11 | ```html 12 | 13 | ``` 14 | 15 | ## 新页签插入位置 16 | 17 | RouterTab 可以通过配置 `append` 来指定新页签插入位置,支持以下两种选项: 18 | 19 | - `last` 页签末尾(默认) 20 | 21 | - `next` 当前页签下一个 22 | 23 | 24 | 25 | **示例:** 26 | 27 | ```html 28 | 29 | ``` 30 | 31 | ## 关闭最后的页签 32 | 33 | 默认情况下,RouterTab 最后一个页签不允许手动关闭。 34 | 35 | 通过配置 `:keep-last-tab="false"` 可以修改这一行为。 36 | 37 | 在关闭最后的页签后,RouterTab 会为你跳转到默认路由。 38 | 39 | 40 | 41 | **示例:** 42 | 43 | ```html 44 | 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/.vuepress/locales/en.js: -------------------------------------------------------------------------------- 1 | const nav = require('./utils/nav').default 2 | const sidebar = require('./utils/sidebar').default 3 | 4 | const i18n = { 5 | path: '/', 6 | guide: 'Guide', 7 | changelog: 'Changelog', 8 | essentials: 'Essentials', 9 | custom: 'Customized', 10 | advanced: 'Advanced', 11 | meta: 'Meta' 12 | } 13 | 14 | // 站点配置 15 | exports.default = { 16 | lang: 'en-US', // 将会被设置为 的 lang 属性 17 | title: 'Vue Router Tab', 18 | description: 'Vue.js tab component, based on Vue Router.' 19 | } 20 | 21 | // 主题配置 22 | exports.theme = { 23 | selectText: 'Languages', 24 | 25 | label: 'English', 26 | 27 | ariaLabel: 'Languages', 28 | 29 | editLinkText: 'Edit this page on GitHub', 30 | 31 | lastUpdated: 'Last updated', 32 | 33 | // 页头导航 34 | nav: nav(i18n), 35 | 36 | // 侧边栏 37 | sidebar: sidebar(i18n) 38 | } 39 | -------------------------------------------------------------------------------- /docs/zh/guide/custom/transition.md: -------------------------------------------------------------------------------- 1 | # 过渡效果 2 | 3 | 您可以通过配置 RouterTab 组件的 `tab-transition` 和 `page-transition` 属性,分别替换默认的**页签**和**页面**过渡效果 4 | 5 | ::: warning 6 | 7 | - 如果是组件作用域内的 CSS(配置了 `scoped`),需要在选择器前添加 `>>>`、 `/deep/` 或 `::v-deep` 才能生效 8 | 9 | - 页签项 `.router-tab-item` 默认设置了 `transition` 和 `transform-origin` 的样式,您可能需要覆盖它已避免影响到自定义的过渡效果 10 | 11 | ::: 12 | 13 | 14 | 15 | **示例:** 16 | 17 | <<< @/src/components/frames/Transition.vue 18 | 19 | 20 | 21 | ### 详细配置 22 | 23 | 您还可以使用对象的方式设置 `tab-transition` 和 `page-transition` 的值,以实现详细的过渡效果配置 24 | 25 | > 配置参考: [Vue - transition](https://cn.vuejs.org/v2/api/#transition) 26 | 27 | ```html 28 | 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | $color = #3eaf7c; 2 | 3 | /*滚动条*/ 4 | ::-webkit-scrollbar 5 | $w = 6px 6 | 7 | width $w 8 | height $w 9 | 10 | &-thumb 11 | background rgba(0, 0, 0, .1) 12 | border-radius 20px 13 | 14 | &:hover 15 | background rgba($color, .5) 16 | 17 | &:active 18 | background rgba($color, .8) 19 | 20 | &-corner 21 | display none 22 | 23 | 24 | // 站点名称前注入 logo 25 | .navbar .home-link 26 | &::before 27 | $s = 1.3rem 28 | content '' 29 | display inline-block 30 | margin-right .3rem 31 | margin-top .45rem 32 | width $s 33 | height $s 34 | vertical-align: top 35 | background url(/vue-router-tab/demo/img/logo.png) 36 | background-size cover 37 | 38 | 39 | // Logo 图片 40 | .home .hero img 41 | width 180px 42 | 43 | .nav-links .nav-link.external 44 | margin-right -.5em 45 | -------------------------------------------------------------------------------- /docs/zh/guide/essentials/iframe.md: -------------------------------------------------------------------------------- 1 | # Iframe 页签 2 | 3 | RouterTab 支持通过 Iframe 页签嵌入外部网站。 4 | 5 | ::: warning 6 | 该功能需要引入 RouterTab 内置路由,请参考 [基础 - 路由配置](README.md#路由配置) 7 | ::: 8 | 9 | ## Iframe 页签操作 10 | 11 | 12 | 13 | #### 打开 Iframe 页签 14 | 15 | ```js 16 | // 三个参数分别为:链接、页签标题、图标 17 | this.$tabs.openIframe('https://cn.vuejs.org', 'Vue.js', 'icon-web') 18 | ``` 19 | 20 | #### 关闭 Iframe 页签 21 | 22 | ```js 23 | this.$tabs.closeIframe('https://cn.vuejs.org') 24 | ``` 25 | 26 | #### 刷新 Iframe 页签 27 | 28 | ```js 29 | this.$tabs.refreshIframe('https://cn.vuejs.org') 30 | ``` 31 | 32 | ## Iframe 页签事件 33 | 34 | RouterTab 支持以下的 Iframe 页签事件: 35 | 36 | - `iframe-mounted` iframe 节点挂载就绪 37 | 38 | - `iframe-loaded` iframe 内容加载成功 39 | 40 | 需要注意的是,iframe 内部链接跳转也会触发 `iframe-loaded` 事件 41 | 42 | 43 | 44 | **示例:** 45 | 46 | <<< @/src/components/frames/Iframe.vue 47 | -------------------------------------------------------------------------------- /docs/.vuepress/components/DemoLink.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 38 | -------------------------------------------------------------------------------- /docs/zh/guide/custom/slot.md: -------------------------------------------------------------------------------- 1 | # 自定义插槽 2 | 3 | RouterTab 支持通过以下插槽个性化页签组件: 4 | 5 | | 插槽名称 | 作用域 | 说明 | 6 | | --------- | ------ | ---------- | 7 | | `default` | `tab` | 页签项 | 8 | | `start` | - | 页签栏开始 | 9 | | `end` | - | 页签栏结束 | 10 | 11 | ### 自定义页签项 12 | 13 | 通过 RouterTab 组件的默认作用域插槽,我们可以自定义页签显示的内容 14 | 15 | 插槽的作用域提供页签项信息 `tab` 供模板使用,包括以下属性或方法 16 | 17 | | 属性 | 类型 | 说明 | 18 | | -------- | --------- | -------------- | 19 | | base | Component | RouterTab 实例 | 20 | | data | Object | 页签数据 | 21 | | id | String | 页签 ID | 22 | | title | String | 标题 | 23 | | tips | String | 提示 | 24 | | icon | String | 图标 | 25 | | tabClass | String | 页签 class | 26 | | closable | Boolean | 是否可关闭 | 27 | | index | Number | 页签索引 | 28 | | close | Function | 页签关闭方法 | 29 | 30 | 31 | 32 | **示例:** 33 | 34 | <<< @/src/components/frames/Slot.vue 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report_zh-cn.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 反馈 3 | about: 提交问题反馈以帮助作者完善项目 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Bug 描述 10 | 11 | 针对 bug 清晰简洁的描述。 12 | 13 | ## 问题重现 14 | 15 | 重现行为的步骤: 16 | 17 | 1. 去'...' 18 | 2. 点击“...” 19 | 3. 向下滚动到“....” 20 | 4. 出现"..."错误 21 | 22 | ## 预期行为 23 | 24 | 清楚简洁地描述您期望发生的事情。 25 | 26 | ## 截图 27 | 28 | 如果可以,请添加屏幕截图以帮助解释您的问题。 29 | 30 | ## 代码重现 31 | 32 | 选择一种方式即可 33 | 34 | ### Git 重现地址(推荐) 35 | 36 | 您可以将可运行并且能复现问题的最简代码提交到 git 仓库(如 Github),然后提供给作者可访问的地址 37 | 38 | ### 关键代码 39 | 40 | 尽可能留下关键代码片段,方便作者定位问题 41 | 42 | ```html 43 | 46 | ``` 47 | 48 | ```javascript 49 | // js 代码 50 | ``` 51 | 52 | ## 开发环境(请填写以下信息) 53 | 54 | - Node.js: [例如: v12.4.0] 55 | - Vue: [例如: v2.5.22] 56 | - Vue Router: [例如: V3.0.1] 57 | - **Vue Router Tab**: [例如: v0.2.0] 58 | 59 | ## 运行环境(请填写以下信息) 60 | 61 | - 设备: [例如: PC] 62 | - 操作系统: [例如: Windows 10] 63 | - 浏览器和版本: [例如: Chrome 75.0.3770.100 x64] 64 | 65 | ## 附加内容 66 | 67 | 在此添加有关此问题的任何其他内容。 68 | -------------------------------------------------------------------------------- /scripts/verify-commit-msg.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const msgPath = process.env.GIT_PARAMS 3 | const msg = require('fs') 4 | .readFileSync(msgPath, 'utf-8') 5 | .trim() 6 | 7 | const commitRE = /^(revert: )?(feat|fix|polish|docs|style|refactor|perf|test|workflow|ci|chore|types|build)(\(.+\))?: .{1,50}/ 8 | 9 | if (!commitRE.test(msg)) { 10 | console.log() 11 | console.error( 12 | ` ${chalk.bgRed.white(' ERROR ')} ${chalk.red( 13 | `invalid commit message format.` 14 | )}\n\n` + 15 | chalk.red( 16 | ` Proper commit message format is required for automated changelog generation. Examples:\n\n` 17 | ) + 18 | ` ${chalk.green(`feat(compiler): add 'comments' option`)}\n` + 19 | ` ${chalk.green( 20 | `fix(v-model): handle events on blur (close #28)` 21 | )}\n\n` + 22 | chalk.red( 23 | ` You can also use ${chalk.cyan( 24 | `npm run commit` 25 | )} to interactively generate a commit message.\n` 26 | ) 27 | ) 28 | process.exit(1) 29 | } 30 | -------------------------------------------------------------------------------- /src/components/PageTimer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 55 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | import frameRoutes from './frames' 5 | import route404 from './404' 6 | 7 | Vue.use(Router) 8 | 9 | // 全局 404 路由 10 | const globalRoute404 = { 11 | ...route404, 12 | path: '/404' 13 | } 14 | 15 | // 路由 16 | const routes = [ 17 | { 18 | path: '/', 19 | redirect: '/default/page/1' 20 | }, 21 | 22 | // 框架子路由 23 | ...frameRoutes, 24 | 25 | // 根路由 404 26 | globalRoute404, 27 | 28 | // 未匹配的路由 404 29 | { 30 | path: '*', 31 | redirect(to) { 32 | const match = /^(\/[^/]+\/)/.exec(to.path) 33 | 34 | if (match) { 35 | const base = match[1] 36 | const matchParent = $router.options.routes.find( 37 | item => item.path === base 38 | ) 39 | 40 | // 子路由 404 41 | if (matchParent) return base + '404' 42 | } 43 | 44 | // 根路由 404 45 | return '/404' 46 | } 47 | } 48 | ] 49 | 50 | // Vue Router 实例 51 | const $router = new Router({ routes }) 52 | 53 | export default $router 54 | -------------------------------------------------------------------------------- /docs/guide/essentials/iframe.md: -------------------------------------------------------------------------------- 1 | # Iframe Tab 2 | 3 | You can open external websites with iframe tabs. 4 | 5 | ::: warning 6 | This feature requires RouterTabRoutes from RouterTab. See [Essentials - Route Config](README.md#路由配置) 7 | ::: 8 | 9 | ## Iframe Tab Operation 10 | 11 | 12 | 13 | #### Open iframe Tab 14 | 15 | ```js 16 | // the arguments are url, tab title and icon 17 | this.$tabs.openIframe('https://vuejs.org', 'Vue.js', 'icon-web') 18 | ``` 19 | 20 | #### Close iframe Tab 21 | 22 | ```js 23 | this.$tabs.closeIframe('https://vuejs.org') 24 | ``` 25 | 26 | #### Refresh iframe Tab 27 | 28 | ```js 29 | this.$tabs.refreshIframe('https://vuejs.org') 30 | ``` 31 | 32 | ## Iframe Tab Events 33 | 34 | Supported iframe tab events are listed below: 35 | 36 | - `iframe-mounted` 37 | 38 | - `iframe-loaded` 39 | 40 | Note that url jumping within iframe will also trigger `iframe-loaded` event. 41 | 42 | 43 | 44 | **Example:** 45 | 46 | <<< @/src/components/frames/Iframe.vue 47 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * stylelint 配置 3 | * stylelint: https://stylelint.io/user-guide/rules/list 4 | * stylelint-scss: https://github.com/kristerkari/stylelint-scss 5 | * stylelint-config-rational-order: https://github.com/constverum/stylelint-config-rational-order 6 | */ 7 | 8 | module.exports = { 9 | extends: [ 10 | 'stylelint-config-recommended', 11 | 'stylelint-config-rational-order', 12 | 'stylelint-config-prettier' 13 | ], 14 | plugins: [ 15 | 'stylelint-scss', 16 | 'stylelint-order', 17 | 'stylelint-config-rational-order/plugin' 18 | ], 19 | rules: { 20 | // 伪元素 21 | 'selector-pseudo-element-no-unknown': [ 22 | true, 23 | { 24 | ignorePseudoElements: ['v-deep'] 25 | } 26 | ], 27 | 28 | // 通用字体 29 | 'font-family-no-missing-generic-family-keyword': [ 30 | true, 31 | { 32 | ignoreFontFamilies: ['rt-icon', 'element-icons'] 33 | } 34 | ], 35 | 36 | 'no-descending-specificity': null, 37 | 'at-rule-no-unknown': null, 38 | 'scss/at-rule-no-unknown': true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/guide/meta/faqs.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ### 📣 Child routes don't render in tabs ([issues 78](https://github.com/bhuh12/vue-router-tab/issues/78)) 4 | 5 | RouterTab is deliberately designed in this way. Only the direct sub-routes of routes that use the RouterTab component will generate the tab page. The nested lower-level routing is displayed as in Vue Router. 6 | 7 | Imagine that there are **subtabs** inside our tab page, and the subtabs also need to respond to routing. This scenario must be supported by nested routing 8 | 9 | It would be very messy for all tab routing to be placed directly on the same layer. We can use the `...` expansion operator to merge and import the routing configuration of different modules: 10 | 11 | ```javascript 12 | // RouterTab built-in routing 13 | import {RouterTabRoutes} from'vue-router-tab' 14 | 15 | const news = [{...}] 16 | const product = [{...}] 17 | 18 | const routes = [ 19 | { 20 | path:'/', 21 | component: Frame, 22 | children: [ 23 | ...RouterTabRoutes, 24 | ...news, 25 | ...product, 26 | ] 27 | } 28 | ] 29 | ``` 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Development Environment (please complete the following information):** 27 | 28 | - Node.js: [e.g. v12.4.0] 29 | - Vue: [e.g. v2.5.22] 30 | - Vue Router [e.g. v3.0.1] 31 | - **Vue Router Tab** [e.g. v0.2.0] 32 | 33 | **Operating Environment (please complete the following information):** 34 | 35 | - Device: [e.g. Desktop] 36 | - OS: [e.g. Windows 10] 37 | - Browser [e.g. stock browser, Chrome] 38 | - Version [e.g. 75] 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const isProd = process.env.NODE_ENV === 'production' 2 | 3 | module.exports = { 4 | root: true, 5 | env: { 6 | node: true 7 | }, 8 | extends: ['plugin:vue/recommended', 'eslint:recommended', '@vue/prettier'], 9 | parserOptions: { 10 | parser: 'babel-eslint', 11 | ecmaFeatures: { 12 | // 支持装饰器 13 | legacyDecorators: true 14 | } 15 | }, 16 | // ESlint 规则:https://eslint.org/docs/rules/ 17 | // Vue 规则:https://eslint.vuejs.org/rules/ 18 | rules: { 19 | 'no-console': 'off', 20 | 'no-debugger': isProd ? 'warn' : 'off', 21 | 'no-unused-vars': 'warn', 22 | 'no-empty': ['error', { allowEmptyCatch: true }], 23 | 'no-prototype-builtins': 'off', 24 | 'vue/no-v-html': 'off', 25 | 'vue/no-unused-vars': 'warn', 26 | 'vue/require-default-prop': 'off', 27 | 'vue/no-mutating-props': 'off', 28 | // Vue 单文件块空行 29 | 'vue/padding-line-between-blocks': 2, 30 | // 多行属性添加空行 31 | 'vue/new-line-between-multi-line-property': [ 32 | 'error', 33 | { 34 | minLineOfMultilineProperty: 2 35 | } 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/mixins/scroll.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '../util/decorator' 2 | 3 | // 页签滚动 4 | export default { 5 | watch: { 6 | activeTabId: { 7 | async handler() { 8 | if (!this.$el) return 9 | 10 | // 激活页签时,如果当前页签不在可视区域,则滚动显示页签 11 | await this.$nextTick() 12 | 13 | this.adjust() 14 | }, 15 | 16 | immediate: true 17 | } 18 | }, 19 | 20 | mounted() { 21 | // 浏览器窗口大小改变时调整Tab滚动显示 22 | window.addEventListener('resize', this.adjust) 23 | }, 24 | 25 | destroyed() { 26 | // 销毁后移除监听事件 27 | window.removeEventListener('resize', this.adjust) 28 | }, 29 | 30 | methods: { 31 | // 调整页签滚动,保证当前页签在可视区域 32 | @debounce() 33 | adjust() { 34 | if (!this.$el) return 35 | 36 | const { scroll } = this.$refs 37 | const cur = this.$el.querySelector('.router-tab__item.is-active') 38 | 39 | if (scroll && cur && !scroll.isInView(cur)) scroll.scrollIntoView(cur) 40 | 41 | // 关闭右键菜单 42 | this.hideContextmenu() 43 | }, 44 | 45 | // 页签过渡 46 | onTabTrans() { 47 | this.adjust() 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/guide/advanced/restore.md: -------------------------------------------------------------------------------- 1 | # Restore Tabs 2 | 3 | You can `restore` tabs after refreshing the page or logging in from another computer by using the `restore` property. 4 | 5 | RouterTab uses sessionStorage to store the cache information of the tabs 6 | 7 | 8 | 9 | **Default mode** 10 | 11 | ```html 12 | 13 | ``` 14 | 15 | **Custom cache** 16 | 17 | RouterTab supports customizing the locally stored key, and obtains the corresponding cache according to the given key 18 | 19 | In practical applications, we want to store browser tab information based on the current user. 20 | 21 | ```html 22 | 23 | ``` 24 | 25 | **Listen to restore parameters** 26 | 27 | Usually, our data will be obtained asynchronously from the server. If we want to restore the user's tabs after the user data is obtained, we can configure `restore-watch` to monitor the restore parameters and automatically restore the corresponding user's tabs after changes. 28 | 29 | ```html 30 | 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/guide/custom/README.md: -------------------------------------------------------------------------------- 1 | # Tabs Behavior 2 | 3 | ## Drag Sort 4 | 5 | RouterTab supports tab drag sort by default, you can disable this function by configuring `:dragsort="false"`. 6 | 7 | 8 | 9 | **Example:** 10 | 11 | ```html 12 | 13 | ``` 14 | 15 | ## Insert Position 16 | 17 | RouterTab can specify the insert position of the new tab by configuring `append`, and supports the following two options: 18 | 19 | - `last` End of tabs (default) 20 | 21 | - `next` Next position of current tab 22 | 23 | 24 | 25 | **Example:** 26 | 27 | ```html 28 | 29 | ``` 30 | 31 | ## Close Last Tab 32 | 33 | By default, the last tab of RouterTab can not be closed manually. 34 | 35 | This behavior can be modified by configuring `:keep-last-tab="false"`. 36 | 37 | After closing the last tab, RouterTab will redirect to the default route. 38 | 39 | 40 | 41 | **Example:** 42 | 43 | ```html 44 | 45 | ``` 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019-present, 碧海幽虹 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. -------------------------------------------------------------------------------- /docs/.vuepress/locales/utils/sidebar.js: -------------------------------------------------------------------------------- 1 | exports.default = i18n => ({ 2 | [`${i18n.path}guide/`]: [ 3 | '', 4 | { 5 | title: i18n.essentials, 6 | collapsable: false, 7 | children: [ 8 | 'essentials/installation', 9 | 'essentials/', 10 | 'essentials/nuxt', 11 | 'essentials/operate', 12 | 'essentials/rule', 13 | 'essentials/iframe' 14 | ] 15 | }, 16 | { 17 | title: i18n.custom, 18 | collapsable: false, 19 | children: [ 20 | 'custom/transition', 21 | 'custom/slot', 22 | 'custom/contextmenu', 23 | 'custom/i18n', 24 | 'custom/', 25 | 'custom/scroll' 26 | ] 27 | }, 28 | { 29 | title: i18n.advanced, 30 | collapsable: false, 31 | children: [ 32 | 'advanced/cache', 33 | 'advanced/dynamic-tab-info', 34 | 'advanced/initial-tabs', 35 | 'advanced/restore', 36 | 'advanced/page-leave' 37 | ] 38 | }, 39 | { 40 | title: i18n.meta, 41 | collapsable: false, 42 | children: ['meta/solutions', 'meta/faqs', 'meta/uninstall'] 43 | } 44 | ] 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/frames/InitialTabs.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 43 | -------------------------------------------------------------------------------- /src/components/AppAside.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | 57 | -------------------------------------------------------------------------------- /docs/zh/guide/advanced/cache.md: -------------------------------------------------------------------------------- 1 | # 缓存控制 2 | 3 | ## 页签缓存 4 | 5 | RouterTab 默认会缓存每个页签的页面 6 | 7 | 您可以设置 RouterTab 组件的 `keep-alive` 来取消这一行为,也可以通过路由的 `meta.keepAlive` 来覆盖组件默认配置 8 | 9 | 如果取消了页签缓存,每次进入页签将重新创建组件实例 10 | 11 | **全局配置** 12 | 13 | ```html 14 | 15 | ``` 16 | 17 | **路由配置** 18 | 19 | ```javascript {6} 20 | const route = { 21 | path: '/my-page/:1', 22 | component: MyPage, 23 | meta: { 24 | title: '页面', 25 | keepAlive: false 26 | } 27 | } 28 | ``` 29 | 30 | ## 最大缓存数 31 | 32 | 你可以通过设置 RouterTab 组件的 `max-alive` 来控制页签的最大缓存数,为 `0` (默认)则不限制 33 | 34 | 页签数量超过设置值后,最初打开的页签缓存将会被清理掉 35 | 36 | ```html 37 | 38 | ``` 39 | 40 | ## 复用组件 41 | 42 | 默认情况下,再次打开同一个页签的路由,如果路由的 `params` 或 `query` 发生改变,RouterTab 会清理上次的页面缓存,重新创建页面实例 43 | 44 | 你可以设置 RouterTab 组件的 `reuse` 来取消这一行为,也可以通过路由的 `meta.reuse` 来覆盖组件默认配置 45 | 46 | ::: tip 47 | 如果设置了 `reuse` 为 `true`,你可能需要通过监听 `$route` 或 `activated` 钩子来更新页面数据 48 | ::: 49 | 50 | **全局配置** 51 | 52 | ```html 53 | 54 | ``` 55 | 56 | **路由配置** 57 | 58 | ```javascript {6} 59 | const route = { 60 | path: '/my-page/:1', 61 | component: MyPage, 62 | meta: { 63 | title: '页面, 64 | reuse: true // 以不同的 params 或 query 重新打开页签后,页面会复用上一次的,不会重新创建 65 | } 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /vetur/tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "router-tab": { 3 | "attributes": [ 4 | "tabs", 5 | "restore", 6 | "default-page", 7 | "tab-transition", 8 | "page-transition", 9 | "page-scroller", 10 | "contextmenu", 11 | "dragsort", 12 | "append", 13 | "keep-last-tab", 14 | "keep-alive", 15 | "max-alive", 16 | "reuse", 17 | "i18n", 18 | "lang", 19 | "@iframe-mounted", 20 | "@iframe-loaded" 21 | ], 22 | "description": "Vue.js tab components, based on Vue Router.\n\r👨‍💻 GitHub: https://github.com/bhuh12/vue-router-tab \n\r📝 Documentation: https://bhuh12.github.io/vue-router-tab/ \n\r📺 Online Demo: https://bhuh12.github.io/vue-router-tab/demo/ \n\r\n\rVue Router Tab 是基于 Vue Router 的路由页签组件,用来实现多页签页面的管理。\n\r🦄 Gitee: https://gitee.com/bhuh12/vue-router-tab \n\r📝 文档: https://bhuh12.gitee.io/vue-router-tab/zh/ \n\r📺 演示: https://bhuh12.gitee.io/vue-router-tab/demo/" 23 | }, 24 | 25 | "router-alive": { 26 | "attributes": [ 27 | "keep-alive", 28 | "max", 29 | "reuse", 30 | "page-class", 31 | "page-scroller", 32 | "transition", 33 | "@ready", 34 | "@change" 35 | ], 36 | "description": "Route cache component of Vue.js.\n\r\n\rVue.js 的路由缓存组件。" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vue Router Tab - Demo 9 | 13 | 26 | 27 | 28 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/guide/custom/slot.md: -------------------------------------------------------------------------------- 1 | # Slot 2 | 3 | RouterTab supports these slots: 4 | 5 | | Slot Name | Scope | Description | 6 | | --------- | ----- | ------------- | 7 | | `default` | `tab` | Tab item | 8 | | `start` | - | Tab bar start | 9 | | `end` | - | Tab bar end | 10 | 11 | ### Custom tab 12 | 13 | You can customize your tabs using the available slot `default`. Inside the slot you will have access to following properties/attributes 14 | 15 | | Properties | Type | Description | 16 | | ---------- | --------- | ----------------------------- | 17 | | base | Component | RouterTab instance | 18 | | data | Object | tab data | 19 | | id | String | tab ID | 20 | | title | String | title | 21 | | tips | String | prompt | 22 | | icon | String | icon | 23 | | tabClass | String | tab class | 24 | | closable | Boolean | Whether the tab can be closed | 25 | | index | Number | tab index | 26 | | close | Function | tab closing method | 27 | 28 | 29 | 30 | **Example:** 31 | 32 | <<< @/src/components/frames/Slot.vue 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | // 引入多语言配置 2 | const { default: localeZh, theme: localeZhTheme } = require('./locales/zh') 3 | const { default: localeEn, theme: localeEnTheme } = require('./locales/en') 4 | 5 | module.exports = { 6 | head: [ 7 | [ 8 | 'link', 9 | { 10 | rel: 'icon', 11 | href: '/demo/img/logo.png' 12 | } 13 | ] 14 | ], 15 | 16 | // 基础路径 17 | base: '/vue-router-tab/', 18 | 19 | // 输出目录 20 | dest: 'dist/docs', 21 | 22 | host: 'localhost', 23 | 24 | // 多语言 25 | locales: { 26 | '/zh/': localeZh, 27 | '/': localeEn 28 | }, 29 | 30 | // 主题配置 31 | themeConfig: { 32 | locales: { 33 | '/zh/': localeZhTheme, 34 | '/': localeEnTheme 35 | }, 36 | 37 | // Demo路径 38 | demoUrl: '/vue-router-tab/demo/', 39 | 40 | // 假定是 GitHub. 同时也可以是一个完整的 GitLab URL 41 | repo: 'bhuh12/vue-router-tab', 42 | 43 | repoLabel: 'GitHub', 44 | 45 | docsBranch: 'main', 46 | 47 | // Algolia 搜索 48 | algolia: { 49 | apiKey: 'fdd2c011c382dd55036237094d62bd9e', 50 | indexName: 'vue-router-tab' 51 | } 52 | }, 53 | 54 | // markdow 配置 55 | markdown: { 56 | lineNumbers: true 57 | }, 58 | 59 | plugins: { 60 | '@vuepress/pwa': { 61 | serviceWorker: true, 62 | updatePopup: true 63 | }, 64 | '@vuepress/back-to-top': true 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/components/ContextmenuItem.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 62 | -------------------------------------------------------------------------------- /lib/config/contextmenu.js: -------------------------------------------------------------------------------- 1 | // 菜单数据 2 | const menuMap = { 3 | // 刷新 4 | refresh: { 5 | handler({ data, $tabs }) { 6 | $tabs.refreshTab(data.id) 7 | } 8 | }, 9 | 10 | // 刷新全部 11 | refreshAll: { 12 | handler({ $tabs }) { 13 | $tabs.refreshAll() 14 | } 15 | }, 16 | 17 | // 关闭 18 | close: { 19 | enable({ target }) { 20 | return target.closable 21 | }, 22 | handler({ data, $tabs }) { 23 | $tabs.closeTab(data.id) 24 | } 25 | }, 26 | 27 | // 关闭左侧 28 | closeLefts: { 29 | enable({ $menu }) { 30 | return $menu.lefts.length 31 | }, 32 | handler({ $menu }) { 33 | $menu.closeMulti($menu.lefts) 34 | } 35 | }, 36 | 37 | // 关闭右侧 38 | closeRights: { 39 | enable({ $menu }) { 40 | return $menu.rights.length 41 | }, 42 | handler({ $menu }) { 43 | $menu.closeMulti($menu.rights) 44 | } 45 | }, 46 | 47 | // 关闭其他 48 | closeOthers: { 49 | enable({ $menu }) { 50 | return $menu.others.length 51 | }, 52 | handler({ $menu }) { 53 | $menu.closeMulti($menu.others) 54 | } 55 | } 56 | } 57 | 58 | // 遍历填充 id 59 | Object.entries(menuMap).forEach(([id, item]) => (item.id = id)) 60 | 61 | export default menuMap 62 | 63 | // 默认菜单 64 | export const defaultMenu = [ 65 | 'refresh', 66 | 'refreshAll', 67 | 'close', 68 | 'closeLefts', 69 | 'closeRights', 70 | 'closeOthers' 71 | ] 72 | -------------------------------------------------------------------------------- /src/components/frames/I18n.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 67 | -------------------------------------------------------------------------------- /src/config/menu.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | text: 'RouterTab 配置', 4 | children: [ 5 | { text: '默认配置', to: '/default' }, 6 | { text: '初始展示页签', to: '/initial-tabs' }, 7 | { text: '刷新还原页签', to: '/restore' }, 8 | { text: 'Iframe 页签', to: '/iframe' } 9 | ] 10 | }, 11 | { 12 | text: '个性化', 13 | children: [ 14 | { text: '过渡效果', to: '/transition' }, 15 | { text: '插槽', to: '/slot' }, 16 | { text: '右键菜单', to: '/contextmenu' }, 17 | { text: '拖拽排序-禁用', to: '/dragsort' }, 18 | { text: '新页签插入位置', to: '/append' }, 19 | { text: '关闭最后的页签', to: '/close-last-tab' }, 20 | { text: '滚动位置', to: '/page-scroller/' } 21 | ] 22 | }, 23 | { 24 | text: '缓存控制', 25 | children: [ 26 | { text: '页签规则', to: '/default/rule' }, 27 | { text: '页签缓存-禁用', to: '/default/no-cache' }, 28 | { text: '最大缓存数', to: '/max-alive' }, 29 | { text: '复用组件', to: '/reuse' } 30 | ] 31 | }, 32 | { 33 | text: '页面功能', 34 | children: [ 35 | { text: '动态页签配置', to: '/default/tab-dynamic' }, 36 | { text: '页面离开确认', to: '/initial-tabs/page-leave' }, 37 | { text: '嵌套路由', to: '/default/nest/1' } 38 | ] 39 | }, 40 | { 41 | text: '多语言支持', 42 | children: [ 43 | { text: '页签国际化', to: '/i18n' }, 44 | { text: '组件语言', to: '/lang-en' }, 45 | { text: '组件自定义语言', to: '/lang-custom' } 46 | ] 47 | } 48 | ] 49 | -------------------------------------------------------------------------------- /src/views/ScrollPosition.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 33 | 34 | 65 | -------------------------------------------------------------------------------- /src/views/TabDynamic.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 48 | 49 | 70 | -------------------------------------------------------------------------------- /src/components/MenuItem.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 28 | 29 | 71 | -------------------------------------------------------------------------------- /docs/zh/guide/custom/scroll.md: -------------------------------------------------------------------------------- 1 | # 滚动位置 2 | 3 | 通过设置滚动元素,已经缓存的页签在重新激活时,将会保持滚动位置。 4 | 5 | ## 全局滚动元素 6 | 7 | 当滚动条在页面节点外部时,可以通过 RouterTab 组件的`page-scroller` 属性设置全局滚动元素。 8 | 9 | RouterTab 默认设置的全局滚动元素是 `.router-tab__container`, 你也可以设置其它的滚动元素。 10 | 11 | 12 | 13 | **示例:** 14 | 15 | ```html {2} 16 | 19 | ``` 20 | 21 | ## 页面滚动元素 22 | 23 | 当滚动条在页面节点内部时,可以通过页面组件选项 `pageScroller` 设置页面滚动元素。 24 | 25 | 26 | 27 | **示例:** 28 | 29 | 一个滚动元素 30 | 31 | ```javascript {3} 32 | export default { 33 | name: 'MyPage', 34 | pageScroller: '.custom-scroller' 35 | } 36 | ``` 37 | 38 | 多个滚动元素 39 | 40 | ```javascript {3} 41 | export default { 42 | name: 'MyPage', 43 | pageScroller: ['.custom-scroller-1', '.custom-scroller-2'] 44 | } 45 | ``` 46 | 47 | ## 异步滚动 48 | 49 | 当页面内有异步加载的内容时,可以在异步完成后,通过页面实例 `$emit('page-loaded')` 来通知 RouterTab 还原滚动位置。 50 | 51 | 52 | 53 | **示例:** 54 | 55 | ```javascript {11} 56 | export default { 57 | name: 'ScrollAsync', 58 | 59 | data() { 60 | return { list: [] } 61 | }, 62 | 63 | activated() { 64 | setTimeout(() => { 65 | this.list = new Array(100) 66 | this.$emit('page-loaded') 67 | }, Math.random() * 1000) 68 | } 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/zh/guide/meta/solutions.md: -------------------------------------------------------------------------------- 1 | # 解决方案 2 | 3 | Vue Router Tab 实现过程中遇到的**问题及解决方案**,也欢迎提出更好的点子。 4 | 5 | 1. 相同路由需要根据 `route.params` 或 `route.query` 不同,根据规则生成不同的缓存: 6 | 7 | `` 添加 `key` 属性,根据 `key` 不同生成不同的实例。 8 | 9 | 2. 通过 `` 组件实例,精准控制缓存: 10 | 11 | 1. 获取 `` 实例: 12 | 13 | 在 `` 过渡组件包裹下,通过 `this._vnode.children[0].child._vnode.componentInstance` 获取 `` 组件实例。 14 | 15 | 2. 匹配并移除缓存: 16 | 17 | 1. 根据缓存 `$alive.cache[i].data.key` 来匹配缓存。 18 | 19 | 2. 销毁当前缓存组件实例:`$alive.cache[key].componentInstance.$destroy()`。 20 | 21 | 3. 从 `$alive.keys` 数组中移除当前 `key`。 22 | 23 | 3. 页面组件强制刷新: 24 | 25 | 1. 移除当前页面组件缓存。 26 | 27 | 2. `router-view` 组件通过 `v-if` 隐藏,在过渡效果结束或 `nextTick` 后再显示。 28 | 29 | 4. 获取当前组件所在的路由深度: 30 | 31 | 递归查找最近的拥有 `$vnode.data.routerViewDepth` 的父组件的对应值。 32 | 33 | 5. 嵌套路由 `` 的 `key`,如果直接从 `$route` 中获取,子路由切换时会生成不同缓存: 34 | 35 | 应该从 `$route.matched` 中匹配当前嵌套深度所在路由的 `path`。 36 | 37 | 6. 打开开启缓存的嵌套路由的一个子路由页面 **a**,然后访问其他路由页面,再直接访问嵌套路由的另一个子路由页面 **b**,此时展示的依然是 **a**,与路由地址不匹配 38 | 39 | 通过 `activated` 钩子,页面组件执行 `$forceUpdate` 强制更新。 40 | 41 | 副作用:子路由页面 **a** 仍会触发 `activated` 钩子 42 | 43 | 7. iframe 页面页签切换后会重新加载: 44 | 45 | 1. 将 ` 111 | -------------------------------------------------------------------------------- /docs/guide/essentials/nuxt.md: -------------------------------------------------------------------------------- 1 | # Nuxt 2 | 3 | ## Import RouterTab 4 | 5 | ### Plugin 6 | 7 | `plugins/routerTab.js` 8 | 9 | ```javascript 10 | import Vue from 'vue' 11 | import RouterTab from 'vue-router-tab' 12 | import 'vue-router-tab/dist/lib/vue-router-tab.css' 13 | 14 | Vue.use(RouterTab) 15 | ``` 16 | 17 | ### Iframe Page 18 | 19 | `pages/-Iframe.js` 20 | 21 | ```javascript 22 | export { Iframe as default } from 'vue-router-tab' 23 | ``` 24 | 25 | ### Nuxt Config 26 | 27 | `nuxt.config.js` 28 | 29 | ```javascript 30 | export default { 31 | // import routerTab plugin 32 | plugins: ['@/plugins/routerTab'], 33 | 34 | router: { 35 | extendRoutes(routes, resolve) { 36 | // add Iframe route 37 | routes.push({ 38 | name: 'iframe', 39 | path: '/iframe/:src/:title?/:icon?', 40 | component: resolve(__dirname, 'pages/-Iframe.js'), 41 | props: true 42 | }) 43 | } 44 | }, 45 | 46 | build: { 47 | // Babel transpile dependencies 48 | transpile: ['vue-router-tab'] 49 | } 50 | } 51 | ``` 52 | 53 | ## Use Component 54 | 55 | > More props at [RouterTab Props](../../api/README.md#router-tab-props) 56 | 57 | ::: danger 58 | RouterTab only supports singleton mode, **do not** introduce multiple RouterTab components in the same page! 59 | ::: 60 | 61 | `layouts/default.vue` 62 | 63 | ```html {5} 64 | 71 | ``` 72 | 73 | ## Tab Config 74 | 75 | ### Page Meta 76 | 77 | `pages/about.vue` 78 | 79 | ```html {7} 80 | 83 | 84 | 92 | ``` 93 | 94 | ## 👨‍💻 Sample Project 95 | 96 | **router-tab-nuxt-sample** 97 | 98 | [**Github**](https://github.com/bhuh12/router-tab-nuxt-sample) 99 | 100 | [**CodeSandbox**](https://codesandbox.io/s/github/bhuh12/router-tab-nuxt-sample) 101 | 102 | 109 | -------------------------------------------------------------------------------- /src/views/IframeOperate.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 101 | 102 | 114 | -------------------------------------------------------------------------------- /src/router/frames.js: -------------------------------------------------------------------------------- 1 | import { importPage } from '../utils' 2 | import extendRoutes from '../utils/extendRoutes' 3 | import getPageRoutes from './page' 4 | 5 | // PascalCase 转 kebab-case 6 | const pascal2Kebab = str => 7 | str 8 | .replace(/([a-z])([A-Z])/g, ($, $1, $2) => $1 + '-' + $2.toLowerCase()) 9 | .replace(/^([A-Z])/, ($, $1) => $1.toLowerCase()) 10 | 11 | // 需要自定义的框架路由 12 | const frameRoutes = { 13 | Reuse: { 14 | redirect: 'rule/default/' 15 | }, 16 | 17 | Iframe: { 18 | redirect: 'operate', 19 | children: [ 20 | { 21 | path: 'operate', 22 | component: importPage('IframeOperate'), 23 | meta: { 24 | title: 'Iframe 页签' 25 | } 26 | } 27 | ] 28 | }, 29 | 30 | I18n: { 31 | redirect: 'lang', 32 | children: [ 33 | { 34 | path: 'lang', 35 | component: importPage('I18n'), 36 | meta: { 37 | title: 'i18n:i18n', 38 | icon: 'rt-icon-doc' 39 | } 40 | }, 41 | { 42 | path: 'page/:id', 43 | component: importPage('Page'), 44 | meta: { 45 | title: 'i18n:page', 46 | icon: 'rt-icon-doc' 47 | } 48 | } 49 | ] 50 | }, 51 | 52 | PageScroller: { 53 | redirect: 'page/1', 54 | children: [ 55 | { 56 | path: 'page/:id', 57 | component: importPage('Page'), 58 | meta: { 59 | title: route => `页面外部滚动${route.params.id}`, 60 | icon: 'rt-icon-doc', 61 | key: 'path' 62 | } 63 | }, 64 | { 65 | path: 'scroll-position', 66 | component: importPage('ScrollPosition'), 67 | meta: { 68 | title: '页面内部滚动', 69 | icon: 'rt-icon-doc' 70 | } 71 | }, 72 | { 73 | path: 'scroll-multi', 74 | component: importPage('ScrollMulti'), 75 | meta: { 76 | title: '多个滚动', 77 | icon: 'rt-icon-doc' 78 | } 79 | }, 80 | { 81 | path: 'scroll-async', 82 | component: importPage('ScrollAsync'), 83 | meta: { 84 | title: '异步滚动', 85 | icon: 'rt-icon-doc' 86 | } 87 | } 88 | ] 89 | } 90 | } 91 | 92 | // 获取目录下框架路由 93 | const context = require.context('../components/frames/', false, /^.*\.vue$/) 94 | 95 | // 生成框架路由 96 | const routes = context.keys().map(filePath => { 97 | const frame = filePath.match(/\w+/)[0] 98 | const path = '/' + pascal2Kebab(frame) + '/' 99 | const { redirect, children } = frameRoutes[frame] || {} 100 | 101 | return { 102 | path, 103 | component: context(filePath).default, 104 | redirect: path + (redirect || 'page/1'), 105 | children: children || getPageRoutes() 106 | } 107 | }) 108 | 109 | routes.forEach(extendRoutes) 110 | 111 | export default routes 112 | -------------------------------------------------------------------------------- /docs/guide/custom/contextmenu.md: -------------------------------------------------------------------------------- 1 | # Contextmenu 2 | 3 | ## Disable Contextmenu 4 | 5 | You can disable the contextmenu by configuring `:contextmenu="false"` 6 | 7 | **Example:** 8 | 9 | ```html 10 | 11 | ``` 12 | 13 | ## Custom Contextmenu 14 | 15 | Configure `contextmenu` through an array to customize the contextmenu 16 | 17 | ::: tip 18 | Reference: [Built-in Contextmenu](https://github.com/bhuh12/vue-router-tab/blob/main/lib/config/contextmenu.js) 19 | ::: 20 | 21 | 22 | 23 | **Example:** 24 | 25 | **Template** 26 | 27 | ```html 28 | 32 | ``` 33 | 34 | **Javascript** 35 | 36 | ```javascript 37 | // full screen mixin 38 | import fullscreen from '../../mixins/fullscreen' 39 | 40 | export default { 41 | mixins: [fullscreen], 42 | 43 | computed: { 44 | contextmenu() { 45 | return [ 46 | // Built-in menu 47 | 'refresh', 48 | 49 | // Extend built-in menu: add icon 50 | { 51 | id: 'close', 52 | icon: 'rt-icon-close' 53 | }, 54 | 55 | // Extend built-in menu: custom handler 56 | { 57 | id: 'closeOthers', 58 | handler({ $menu }) { 59 | $menu.closeMulti($menu.others) 60 | alert('Close other tabs') 61 | } 62 | }, 63 | 64 | // custom menu 65 | { 66 | id: 'custom', 67 | title: 'Custom Action', 68 | tips: 'This is a custom action', 69 | icon: 'rt-icon-doc', 70 | class: 'custom-action', 71 | // Whether to display or not, default display 72 | visible(context) { 73 | return context.$tabs.items.length < 3 74 | }, 75 | // Whether it is enabled or not, it will be enabled by default 76 | enable(context) { 77 | return !(context.data.index % 2) 78 | }, 79 | // Click to trigger 80 | handler(context) { 81 | console.log(context) 82 | alert( 83 | 'This is a custom operation, please open the console to view the output parameters' 84 | ) 85 | } 86 | }, 87 | 88 | // Menu with status: fullscreen 89 | { 90 | id: 'fullscreen', 91 | title: () => (this.fullscreen ? 'Exit Full Screen' : 'Full Screen'), 92 | icon: () => 93 | this.fullscreen ? 'rt-icon-fullscreen-exit' : 'rt-icon-fullscreen', 94 | // Click to trigger 95 | handler: () => 96 | setTimeout(() => { 97 | this.fullscreen = !this.fullscreen 98 | }, 300) 99 | } 100 | ] 101 | } 102 | } 103 | } 104 | ``` 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-router-tab", 3 | "version": "1.2.11", 4 | "description": "Vue.js tab components, based on Vue Router", 5 | "keywords": [ 6 | "vue-router-tab", 7 | "router-tab", 8 | "tabs" 9 | ], 10 | "author": "碧海幽虹 ", 11 | "private": false, 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/bhuh12/vue-router-tab.git" 16 | }, 17 | "publishConfig": { 18 | "registry": "https://registry.npmjs.org" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/bhuh12/vue-router-tab/issues" 22 | }, 23 | "main": "dist/lib/vue-router-tab.umd.min.js", 24 | "scripts": { 25 | "demo:dev": "vue-cli-service serve --open", 26 | "demo:build": "vue-cli-service build", 27 | "docs:dev": "vuepress dev docs --open", 28 | "docs:build": "vuepress build docs", 29 | "lib:build": "vue-cli-service build --target lib lib/index.js", 30 | "lib:build:report": "vue-cli-service build --report --target lib lib/index.js", 31 | "lib:publish": "npm run lib:build && npm publish", 32 | "lint": "yarn lint:js && yarn lint:css", 33 | "lint:js": "vue-cli-service lint --fix", 34 | "lint:css": "stylelint \"**/*.{css,scss,sass,vue}\" --fix --cache --cache-location node_modules/.cache/stylelint/", 35 | "commit": "git-cz", 36 | "changelog": "conventional-changelog -p angular -i docs/zh/guide/changelog.md -s" 37 | }, 38 | "dependencies": { 39 | "vue": "^2.6.14", 40 | "vue-router": "^3.2.0" 41 | }, 42 | "devDependencies": { 43 | "@vue/cli-plugin-babel": "~4.5.15", 44 | "@vue/cli-plugin-eslint": "~4.5.15", 45 | "@vue/cli-plugin-router": "~4.5.15", 46 | "@vue/cli-service": "~4.5.15", 47 | "@vue/eslint-config-prettier": "^6.0.0", 48 | "@vuepress/plugin-back-to-top": "^1.5.2", 49 | "@vuepress/plugin-pwa": "^1.5.2", 50 | "babel-eslint": "^10.1.0", 51 | "conventional-changelog-cli": "^2.1.1", 52 | "core-js": "^3.6.5", 53 | "eslint": "^6.7.2", 54 | "eslint-plugin-prettier": "^3.1.3", 55 | "eslint-plugin-vue": "^7.9.0", 56 | "lint-staged": "^9.5.0", 57 | "prettier": "^1.19.1", 58 | "sass": "^1.26.5", 59 | "sass-loader": "^8.0.2", 60 | "stylelint": "^13.13.1", 61 | "stylelint-config-prettier": "^8.0.2", 62 | "stylelint-config-rational-order": "^0.1.2", 63 | "stylelint-config-recommended": "^5.0.0", 64 | "stylelint-order": "^4.1.0", 65 | "stylelint-scss": "^3.19.0", 66 | "vue-template-compiler": "^2.6.14", 67 | "vuepress": "^1.5.2" 68 | }, 69 | "gitHooks": { 70 | "pre-commit": "lint-staged", 71 | "commit-msg": "node scripts/verify-commit-msg.js" 72 | }, 73 | "lint-staged": { 74 | "*.{js,jsx,vue}": [ 75 | "vue-cli-service lint", 76 | "git add" 77 | ] 78 | }, 79 | "config": { 80 | "commitizen": { 81 | "path": "./node_modules/cz-conventional-changelog" 82 | } 83 | }, 84 | "vetur": { 85 | "tags": "vetur/tags.json", 86 | "attributes": "vetur/attributes.json" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /docs/zh/guide/meta/uninstall.md: -------------------------------------------------------------------------------- 1 | # 移除 RouterTab 2 | 3 | 相对于单页应用,多页签框架为用户同时处理多个业务时的跨页操作带来了更好的体验,但这也使得我们要处理更多的页面交互场景,代码相对会更加复杂。 4 | 5 | 如果你的项目不再需要使用 RouterTab,你可以通过下面的步骤来移除 RouterTab。 6 | 7 | ## 一、替换 `this.$tabs` 调用 8 | 9 | 批量查找 `$tabs` 全局调用,参考下表使用替换方案。 10 | 11 | | 方法 | 说明 | 替换方案 | 12 | | ------------------------------------------------------------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 13 | | `$tabs.open` | 全新打开页签 | `$router.push(path)` | 14 | | `$tabs.close` | 关闭页签并跳转新页面 | `$router.replace(path)` | 15 | | `$tabs.refresh`
`$tabs.refreshAll` | 刷新页签 | 组件内部提供刷新数据方法 | 16 | | `$tabs.reset` | 重置页签,回到默认或指定页 | `$router.replace(path)` | 17 | | `$tabs.openIframe`
`$tabs.closeIframe`
`$tabs.refreshIframe` | iframe 页签相关方法 | 需要添加全局 iframe 路由用于嵌入页面,并封装方法用于操作 iframe 页面 | 18 | | `beforePageLeave` | 页面离开确认 | `beforeRouteLeave(to, from, next)`
参考:[Vue-Router 组件内的守卫](https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E7%BB%84%E4%BB%B6%E5%86%85%E7%9A%84%E5%AE%88%E5%8D%AB) | 19 | 20 | ## 二、去除 `` 组件 21 | 22 | 1. 使用 `` 替换布局框架组件内的 `` 23 | 24 | 2. 参考 [入门](../essentials/README.md) 说明移除相关代码: 25 | 26 | 1. 入口文件移除 RouterTab 安装代码 27 | 28 | 2. 路由配置文件移除 RouterTab 内置路由和页签相关配置 29 | 30 | 3. 移除 RouterTab 依赖 31 | 32 | 推荐使用 yarn: 33 | 34 | ```bash 35 | yarn remove vue-router-tab 36 | ``` 37 | 38 | 你也可以用 npm: 39 | 40 | ```bash 41 | npm uninstall vue-router-tab 42 | ``` 43 | 44 | ## 三、调整页面交互方式 45 | 46 | 使用单页方式,意味着不再支持跨页操作 47 | 48 | 你需要将原来切换页签操作的流程,更改为单页内部的操作,可以使用例如弹窗、抽屉、子页面等交互方式。 49 | -------------------------------------------------------------------------------- /src/router/page.js: -------------------------------------------------------------------------------- 1 | import { importPage } from '../utils' 2 | 3 | // 页面路由 4 | export default () => [ 5 | { 6 | path: 'page/:id', 7 | component: importPage('Page'), 8 | meta: { 9 | title: route => `页面${route.params.id}`, 10 | icon: 'rt-icon-doc', 11 | key: 'path' 12 | } 13 | }, 14 | { 15 | path: 'no-cache', 16 | redirect: 'no-cache/1' 17 | }, 18 | { 19 | path: 'no-cache/:id', 20 | component: importPage('Page'), 21 | meta: { 22 | title: route => `无缓存页面${route.params.id}`, 23 | keepAlive: false, 24 | icon: 'rt-icon-doc', 25 | key: 'path' 26 | } 27 | }, 28 | { 29 | path: 'rule', 30 | redirect: 'rule/default/a/1' 31 | }, 32 | { 33 | path: 'rule/default', 34 | redirect: 'rule/default/a/1' 35 | }, 36 | { 37 | path: 'rule/default/:catalog/:type', 38 | component: importPage('Rule'), 39 | meta: { 40 | title: route => `规则:默认-${route.params.catalog}/${route.params.type}`, 41 | icon: 'rt-icon-log' 42 | } 43 | }, 44 | { 45 | path: 'rule/path', 46 | redirect: 'rule/path/a/1' 47 | }, 48 | { 49 | path: 'rule/path/:catalog/:type', 50 | component: importPage('Rule'), 51 | meta: { 52 | title: route => `规则:path-${route.params.catalog}/${route.params.type}`, 53 | icon: 'rt-icon-log', 54 | key: 'path' 55 | } 56 | }, 57 | { 58 | path: 'rule/fullPath', 59 | redirect: 'rule/fullPath/a/1' 60 | }, 61 | { 62 | path: 'rule/fullPath/:catalog/:type', 63 | component: importPage('Rule'), 64 | meta: { 65 | title: route => 66 | `规则:fullPath-${route.params.catalog}/${route.params.type}`, 67 | icon: 'rt-icon-log', 68 | key: 'fullPath' 69 | } 70 | }, 71 | { 72 | path: 'rule/custom', 73 | redirect: 'rule/custom/a/1' 74 | }, 75 | { 76 | path: 'rule/custom/:catalog/:type', 77 | component: importPage('Rule'), 78 | meta: { 79 | title: route => 80 | `规则:自定义-${route.params.catalog}/${route.params.type}`, 81 | icon: 'rt-icon-log', 82 | key: route => '/rule/custom/' + route.params.catalog 83 | } 84 | }, 85 | { 86 | path: 'tab-dynamic', 87 | component: importPage('TabDynamic'), 88 | meta: { 89 | title: '动态页签', 90 | icon: 'rt-icon-log' 91 | } 92 | }, 93 | { 94 | path: 'page-leave', 95 | component: importPage('PageLeave'), 96 | meta: { 97 | title: '页面离开确认', 98 | icon: 'rt-icon-contact' 99 | } 100 | }, 101 | { 102 | path: 'nest/:nestId', 103 | component: importPage('Nest'), 104 | redirect(route) { 105 | return route.fullPath + '/page1' 106 | }, 107 | meta: { 108 | title: '嵌套路由', 109 | icon: 'rt-icon-doc' 110 | }, 111 | children: [ 112 | { 113 | path: 'page1', 114 | component: importPage('Page1') 115 | }, 116 | { 117 | path: 'page2', 118 | component: importPage('Page2') 119 | } 120 | ] 121 | } 122 | ] 123 | -------------------------------------------------------------------------------- /lib/components/Contextmenu.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 126 | -------------------------------------------------------------------------------- /lib/util/RouteMatch.js: -------------------------------------------------------------------------------- 1 | import { prunePath } from './' 2 | import rules from '../config/rules' 3 | 4 | // 解析路由 key 5 | function parseRouteKey($route, route, key) { 6 | const defaultKey = route.path 7 | 8 | if (!key) return defaultKey 9 | 10 | if (typeof key === 'string') { 11 | // 规则 12 | const rule = rules[key.toLowerCase()] 13 | return rule ? rule($route) : key 14 | } 15 | 16 | if (typeof key === 'function') { 17 | return parseRouteKey($route, route, key($route)) 18 | } 19 | 20 | return defaultKey 21 | } 22 | 23 | // 解析匹配的路径 24 | function parsePath(path, params) { 25 | return Object.entries(params).reduce((p, [key, val]) => { 26 | return p.replace(':' + key, val) 27 | }, path) 28 | } 29 | 30 | // 匹配路由数据 31 | export default class RouteMatch { 32 | constructor(vm, $route) { 33 | this.vm = vm 34 | this.$route = $route 35 | } 36 | 37 | // 设置路由 38 | set $route($route) { 39 | if ($route && !$route.matched) { 40 | const { $router } = this.vm 41 | $route = $router.match($route, $router.currentRoute) 42 | } 43 | this._$route = $route 44 | } 45 | 46 | // 获取路由 47 | get $route() { 48 | return this._$route || this.vm.$route 49 | } 50 | 51 | // 页面路由索引 52 | get routeIndex() { 53 | return this.vm.routeIndex 54 | } 55 | 56 | // 下级路由 57 | get route() { 58 | return this.$route.matched[this.routeIndex] 59 | } 60 | 61 | // 根路径 62 | get basePath() { 63 | if (this.routeIndex < 1) return '/' 64 | 65 | const baseRoute = this.$route.matched[this.routeIndex - 1] || {} 66 | const { path } = baseRoute 67 | 68 | return (path && parsePath(path, this.$route.params)) || '/' 69 | } 70 | 71 | // 缓存路径,用于判断是否复用 72 | get alivePath() { 73 | const { $route } = this 74 | // 嵌套路由 75 | if (this.nest) { 76 | return parsePath(this.route.path, $route.params) 77 | } 78 | 79 | return prunePath($route.fullPath) 80 | } 81 | 82 | // 路由元 83 | get meta() { 84 | const { 85 | route, 86 | vm: { $nuxt } 87 | } = this 88 | 89 | // Nuxt 优先从页面配置获取 meta 90 | if ($nuxt) { 91 | try { 92 | const { meta: metas = [] } = $nuxt.context.route 93 | const meta = metas[this.routeIndex] 94 | if (meta && Object.keys(meta).length) { 95 | return meta 96 | } 97 | } catch (e) { 98 | console.error(e) 99 | } 100 | } 101 | 102 | return (route && route.meta) || {} 103 | } 104 | 105 | // 是否嵌套路由 106 | get nest() { 107 | return this.$route.matched.length > this.routeIndex + 1 108 | } 109 | 110 | // 路由 key 111 | get key() { 112 | if (!this.route) return '' 113 | 114 | return parseRouteKey(this.$route, this.route, this.meta.key) 115 | } 116 | 117 | // 是否缓存 118 | get alive() { 119 | const { keepAlive } = this.meta 120 | return typeof keepAlive === 'boolean' ? keepAlive : this.vm.keepAlive 121 | } 122 | 123 | // 是否复用组件 124 | get reusable() { 125 | const { reuse } = this.meta 126 | return typeof reuse === 'boolean' ? reuse : this.vm.reuse 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/views/Rule.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 114 | 115 | 134 | -------------------------------------------------------------------------------- /src/assets/scss/demo.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | overflow: hidden; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | color: $text; 11 | font-size: 16px; 12 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 13 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | /* PC 滚动条 */ 19 | @include screen-pc { 20 | ::-webkit-scrollbar { 21 | $w: 6px; 22 | $mw: 3px; 23 | width: $w; 24 | height: $w; 25 | 26 | &-thumb { 27 | background: rgba(0, 0, 0, 0.1); 28 | border-radius: 20px; 29 | 30 | &:hover { 31 | background: rgba($color, 0.5); 32 | } 33 | 34 | &:active { 35 | background: rgba($color, 0.8); 36 | } 37 | } 38 | 39 | &-corner { 40 | display: none; 41 | } 42 | } 43 | } 44 | 45 | // 图标 46 | [class^='rt-icon-'], 47 | [class*=' rt-icon-'] { 48 | font-size: 16px; 49 | font-family: 'rt-icon' !important; 50 | font-style: normal; 51 | -webkit-font-smoothing: antialiased; 52 | -moz-osx-font-smoothing: grayscale; 53 | } 54 | 55 | a:link { 56 | transition: color 0.3s ease-in-out; 57 | } 58 | 59 | code { 60 | margin: 0; 61 | padding: 0.25rem 0.5rem; 62 | color: #f40; 63 | font-size: 0.85em; 64 | font-family: source-code-pro, Menlo, Monaco, Consolas, Courier New, monospace; 65 | background-color: rgba(27, 31, 35, 0.05); 66 | border-radius: 3px; 67 | } 68 | 69 | // 强提示 70 | .text-strong { 71 | color: #f50; 72 | font-weight: 600; 73 | font-size: 1.2em; 74 | } 75 | 76 | // 按钮 77 | .demo-btn { 78 | $activeColor: mix(#000, $color, 20%); 79 | 80 | display: inline-block; 81 | margin: 0 1em 0.5em 0; 82 | padding: 3px 8px; 83 | color: #333; 84 | font-size: 14px; 85 | text-decoration: none; 86 | border: 1px solid #ccc; 87 | border-radius: 3px; 88 | cursor: pointer; 89 | transition: all 0.3s ease; 90 | 91 | &:hover { 92 | color: $color; 93 | border-color: $color; 94 | } 95 | 96 | &:active { 97 | color: $activeColor; 98 | border-color: $activeColor; 99 | } 100 | 101 | &.link { 102 | &:hover { 103 | color: $color-primary; 104 | border-color: $color-primary; 105 | } 106 | 107 | &.router-link-exact-active { 108 | color: #fff; 109 | background-color: $color-primary; 110 | border-color: rgba(0, 0, 0, 0.05); 111 | } 112 | } 113 | 114 | &.primary { 115 | color: #fff; 116 | background: $color; 117 | border-color: $color; 118 | 119 | &:hover { 120 | border-color: $activeColor; 121 | } 122 | 123 | &:active { 124 | background: $activeColor; 125 | } 126 | } 127 | } 128 | 129 | // 表格 130 | .demo-table { 131 | $bg: #e5fff3; 132 | min-width: 300px; 133 | border-collapse: collapse; 134 | 135 | th, 136 | td { 137 | padding: 5px 8px; 138 | border: 1px solid #ddd; 139 | } 140 | 141 | th { 142 | font-weight: 400; 143 | text-align: left; 144 | background-color: #f7f7f7; 145 | } 146 | 147 | tr:hover { 148 | background-color: rgba($bg, 0.6); 149 | } 150 | 151 | .on { 152 | background-color: $bg; 153 | } 154 | } 155 | 156 | // 页签组件 - 全屏 157 | .router-tab.is-fullscreen { 158 | position: fixed; 159 | top: 0; 160 | right: 0; 161 | bottom: 0; 162 | left: 0; 163 | z-index: 999; 164 | background: #fff; 165 | } 166 | -------------------------------------------------------------------------------- /docs/zh/guide/essentials/operate.md: -------------------------------------------------------------------------------- 1 | # 页签操作 2 | 3 | ## 打开/切换页签 4 | 5 | RouterTab 通过响应路由变化来新增或切换页签,您可以使用以下两种方式。 6 | 7 | ### 1. Vue Router 原生方式(推荐) 8 | 9 | 使用 Vue Router 内置的方式打开页签,如果您之前访问过该地址,您打开的将是缓存的页签页面。 10 | 11 | 参考文档:[Vue Router 导航](https://router.vuejs.org/zh/guide/essentials/navigation.html) 12 | 13 | 使用 `` 组件 14 | 15 | ```html 16 | 页面1 17 | ``` 18 | 19 | 使用 `router.push`、`router.replace`、`router.go` 等方法 20 | 21 | ```javascript 22 | this.$router.push('/page/1') 23 | ``` 24 | 25 | ### 2. RouterTab 内置方法 26 | 27 | `open (path, isReplace = false, refresh = true)` 28 | 29 | 此方法默认会刷新已有页签,如果希望**全新打开页签**,您可以尝试此方法。 30 | 31 | 32 | 33 | **全新打开页签** 34 | 35 | ```javascript 36 | this.$tabs.open('/page/2') 37 | ``` 38 | 39 | ## 关闭页签 40 | 41 | `close({id, path, match = true, force = true, to, refresh = false})` 42 | 43 | 您可以通过 RouterTab 的实例方法 [`routerTab.close`](../../api/README.md#routertab-close) 来关闭指定页签 44 | 45 | 46 | 47 | **关闭当前页签** 48 | 49 | ```js 50 | this.$tabs.close() 51 | ``` 52 | 53 | **关闭指定页签** 54 | 55 | ```js 56 | // 关闭指定 fullPath 的页签 57 | this.$tabs.close('/page/1') 58 | 59 | // 关闭指定 location 的页签 60 | this.$tabs.close({ 61 | name: 'page', 62 | params: { 63 | id: 2 64 | } 65 | }) 66 | ``` 67 | 68 | **关闭页签后跳转地址** 69 | 70 | ```js 71 | // 关闭 '/page/1' 跳转到 '/page/2' 72 | this.$tabs.close('/page/1', '/page/2') 73 | 74 | // 关闭当前页签跳转到 '/page/2' 75 | this.$tabs.close({ 76 | to: '/page/2' 77 | }) 78 | ``` 79 | 80 | **完整选项说明** 81 | 82 | ```js 83 | this.$tabs.close({ 84 | id: '', // 通过页签 id (即 key 返回值)关闭页签, 与 path 二选一即可 85 | path: '/page/2', // 通过路由路径关闭页签,可 location 对象方式传入。如果未配置 id 和 path 则关闭当前页签 86 | match: false, // path 方式关闭时,是否匹配 path 完整路径,默认 true 87 | force: false, // 是否强制关闭,默认 true 88 | to: '/page/1', // 关闭后跳转的地址,可 location 对象方式传入。(未设置则跳转上一个页签,最后一个页签默认关闭后跳转默认页) 89 | refresh: true // 是否全新打开跳转地址 默认 false 90 | }) 91 | ``` 92 | 93 | ## 刷新页签 94 | 95 | `refresh (path, match = true, force = true)` 96 | 97 | 您可以通过 RouterTab 的实例方法 [`routerTab.refresh`](../../api/README.md#routertab-refresh) 来刷新指定页签 98 | 99 | 100 | 101 | **刷新当前页签** 102 | 103 | ```js 104 | this.$tabs.refresh() 105 | ``` 106 | 107 | **刷新指定页签** 108 | 109 | ```js 110 | // 刷新指定 fullPath 的页签 111 | this.$tabs.refresh('/page/1') 112 | 113 | // 刷新指定 location 的页签 114 | this.$tabs.refresh({ 115 | name: 'page', 116 | params: { 117 | id: 2 118 | } 119 | }) 120 | ``` 121 | 122 | **模糊刷新页签** 123 | 124 | ```js 125 | // 刷新与给定地址共用页签的地址,即使地址不完全匹配 126 | // 默认规则下,类似 '/page/1?query=2' 这样的页签也能被匹配刷新 127 | this.$tabs.refresh('/page/1', false) 128 | ``` 129 | 130 | ## 刷新所有页签 131 | 132 | `refreshAll (force = false)` 133 | 134 | 您可以通过 RouterTab 的实例方法 [`routerTab.refreshAll`](../../api/README.md#routertab-refreshall) 来刷新所有页签 135 | 136 | **刷新所有页签** 137 | 138 | ```js 139 | this.$tabs.refreshAll() 140 | ``` 141 | 142 | **强制刷新所有页签**,忽略页面组件的 `beforePageLeave` 配置 143 | 144 | ```js 145 | this.$tabs.refreshAll(true) 146 | ``` 147 | 148 | ## 重置页签 149 | 150 | `reset (to = this.defaultPath)` 151 | 152 | 通常,在用户重新登录或者切换角色的情况下,我们需要关闭用户所有页签并恢复页签初始状态,包括恢复初始页签、跳转到指定的默认页面等 153 | 154 | 针对这些场景,您可以使用 [`routerTab.reset`](../../api/README.md#routertab-reset) 方法来重置页签到初始状态 155 | 156 | ```js 157 | // 重置页签并跳转默认页面 158 | // 程序会自动获取页签父路由地址为默认页面 159 | // 您也可以通过 RouterTab 的 'default-page' 来指定 160 | this.$tabs.reset() 161 | 162 | // 重置页签并跳转 /page/2 163 | this.$tabs.reset('/page/2') 164 | ``` 165 | -------------------------------------------------------------------------------- /docs/guide/meta/uninstall.md: -------------------------------------------------------------------------------- 1 | # Uninstall 2 | 3 | Compared with single-page applications, the multi-tab framework brings a better experience for users to cross-page operations when processing multiple businesses at the same time, but it also makes us deal with more page interaction scenarios, and the code is relatively more complicated. 4 | 5 | If your project no longer needs to use RouterTab, you can use the following steps to remove RouterTab. 6 | 7 | ## 1. Replace `this.$tabs` call 8 | 9 | Find all the global calls of `$tabs` in your project, refer to the table below to use the replacement scheme. 10 | 11 | | Method | Description | Alternative Plan | 12 | | ------------------------------------------------------------------ | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 13 | | `$tabs.open` | Newly opened tab | `$router.push(path)` | 14 | | `$tabs.close` | Close tab and jump to a new page | `$router.replace(path)` | 15 | | `$tabs.refresh`
`$tabs.refreshAll` | Refresh tab | Provide refresh data method inside the component | 16 | | `$tabs.reset` | Reset tabs and jump to the default or specified page | `$router.replace(path)` | 17 | | `$tabs.openIframe`
`$tabs.closeIframe`
`$tabs.refreshIframe` | Iframe tab related methods | Need to add a global iframe route to embed the page, and encapsulate method to operate the iframe page 面 | 18 | | `beforePageLeave` | Page leave confirmation | `beforeRouteLeave(to, from, next)`
Reference:[Vue-Router In-Component Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards) | 19 | 20 | ## 2. Remove the `` component 21 | 22 | 1. Use `` to replace `` in the layout frame component 23 | 24 | 2. Refer to [Getting Started](../essentials/README.md) instructions to remove related codes: 25 | 26 | 1. Remove the RouterTab installation code from the entry file 27 | 28 | 2. Remove RouterTab built-in routing and tab related configuration from routing configuration file 29 | 30 | 3. Remove RouterTab dependency 31 | 32 | yarn (recommended): 33 | 34 | ```bash 35 | yarn remove vue-router-tab 36 | ``` 37 | 38 | 你也可以用 npm: 39 | 40 | ```bash 41 | npm uninstall vue-router-tab 42 | ``` 43 | 44 | ## 3. Adjust the page interaction 45 | 46 | Using single-page mode means that cross-page operations are no longer supported 47 | 48 | You need to change the cross-page operations to the internal operations of a single page. You can use UI components such as dialogs, drawers, and subpages. 49 | -------------------------------------------------------------------------------- /lib/util/index.js: -------------------------------------------------------------------------------- 1 | // 空对象和数组 2 | export const emptyObj = Object.create(null) 3 | export const emptyArray = [] 4 | 5 | // 从数组删除项 6 | export function remove(arr, item) { 7 | if (arr.length) { 8 | const index = arr.indexOf(item) 9 | if (index > -1) { 10 | return arr.splice(index, 1) 11 | } 12 | } 13 | } 14 | 15 | /** 16 | * 滚动到指定位置 17 | * @export 18 | * @param {Element} wrap 滚动区域 19 | * @param {number} [left=0] 20 | * @param {number} [top=0] 21 | */ 22 | export function scrollTo({ wrap, left = 0, top = 0, smooth = true }) { 23 | if (!wrap) return 24 | 25 | if (wrap.scrollTo) { 26 | wrap.scrollTo({ 27 | left, 28 | top, 29 | behavior: smooth ? 'smooth' : 'auto' 30 | }) 31 | } else { 32 | wrap.scrollLeft = left 33 | wrap.scrollTop = top 34 | } 35 | } 36 | 37 | /** 38 | * 指定元素滚动到可视区域 39 | * @export 40 | * @param {Element} el 目标元素 41 | * @param {Element} wrap 滚动区域 42 | * @param {String} block 垂直方向的对齐,可选:'start', 'center', 'end', 或 'nearest' 43 | * @param {String} inline 水平方向的对齐,可选值同上 44 | */ 45 | export function scrollIntoView({ 46 | el, 47 | wrap, 48 | block = 'start', 49 | inline = 'nearest' 50 | }) { 51 | if (!el || !wrap) return 52 | 53 | if (el.scrollIntoView) { 54 | el.scrollIntoView({ behavior: 'smooth', block, inline }) 55 | } else { 56 | let { offsetLeft, offsetTop } = el 57 | let left, top 58 | 59 | if (block === 'center') { 60 | top = offsetTop + (el.clientHeight - wrap.clientHeight) / 2 61 | } else { 62 | top = offsetTop 63 | } 64 | 65 | if (inline === 'center') { 66 | left = offsetLeft + (el.clientWidth - wrap.clientWidth) / 2 67 | } else { 68 | left = offsetLeft 69 | } 70 | 71 | scrollTo({ wrap, left, top }) 72 | } 73 | } 74 | 75 | // 获取滚动条宽度 76 | export const getScrollbarWidth = (function() { 77 | let width = null 78 | 79 | return function() { 80 | if (width !== null) return width 81 | 82 | const div = document.createElement('div') 83 | 84 | div.style.cssText = 'width: 100px; height: 100px;overflow-y: scroll' 85 | document.body.appendChild(div) 86 | width = div.offsetWidth - div.clientWidth 87 | div.parentElement.removeChild(div) 88 | 89 | return width 90 | } 91 | })() 92 | 93 | /** 94 | * 提取计算属性 95 | * @export 96 | * @param {String} origin 来源属性 97 | * @param {Array|Object} props 需要提取的计算属性 98 | * @param {String} context 来源选项为 function 时的入参 99 | * @returns {Object} 100 | */ 101 | export function mapGetters(origin, props, context) { 102 | const map = {} 103 | 104 | const each = (prop, option) => { 105 | if (option === null || typeof option !== 'object') { 106 | option = { default: option } 107 | } 108 | 109 | const { default: def, alias } = option 110 | 111 | map[alias || prop] = function() { 112 | const val = this[origin][prop] 113 | if (context && typeof val === 'function') { 114 | // 函数返回 115 | return val(this[context]) 116 | } else if (def !== undefined && val === undefined) { 117 | // 默认值 118 | if (typeof def === 'function') { 119 | return def.bind(this)() 120 | } 121 | return def 122 | } 123 | return val 124 | } 125 | } 126 | 127 | if (Array.isArray(props)) { 128 | props.forEach(prop => each(prop)) 129 | } else { 130 | Object.entries(props).forEach(([prop, def]) => each(prop, def)) 131 | } 132 | 133 | return map 134 | } 135 | 136 | // 去除路径中的 hash 137 | export const prunePath = path => path.split('#')[0] 138 | 139 | // 解析过渡配置 140 | export function getTransOpt(trans) { 141 | return typeof trans === 'string' ? { name: trans } : trans 142 | } 143 | 144 | // 获取组件 id 145 | export function getCtorId(vm) { 146 | return vm.$vnode.componentOptions.Ctor.cid 147 | } 148 | -------------------------------------------------------------------------------- /docs/zh/guide/essentials/README.md: -------------------------------------------------------------------------------- 1 | # 入门 2 | 3 | ## 引入组件 4 | 5 | **示例:** 6 | 7 | `main.js` 入口文件 8 | 9 | ```javascript {6,7,13} 10 | // router-tab 组件依赖 vue 和 vue-router 11 | import Vue from 'vue' 12 | import Router from 'vue-router' 13 | 14 | // 引入组件和样式 15 | import RouterTab from 'vue-router-tab' 16 | import 'vue-router-tab/dist/lib/vue-router-tab.css' 17 | 18 | import App from './App.vue' 19 | import router from './router' 20 | 21 | Vue.config.productionTip = false 22 | Vue.use(RouterTab) 23 | 24 | new Vue({ 25 | router, 26 | render: h => h(App) 27 | }).$mount('#app') 28 | ``` 29 | 30 | ## 应用组件 31 | 32 | > 配置参考: [RouterTab 配置参数](../../api/README.md#routertab-配置参数) 33 | 34 | ::: danger 35 | RouterTab 仅支持单例模式,请勿在同一个页面中引入多个 RouterTab 组件! 36 | ::: 37 | 38 | **示例:** 39 | 40 | `components/layout/Frame.vue` 布局框架 41 | 42 | ```html {5} 43 | 50 | ``` 51 | 52 | ## 路由配置 53 | 54 | 1. 引入 RouterTab 内置路由以支持 [Iframe 页签](iframe.md) 55 | 2. 通过路由的 `meta` 信息,来设置页签的**标题**、**图标**、**提示**和**路由页签规则** 56 | 57 | > 配置参考: [Route.meta 路由元信息](../../api/README.md#route-meta-路由元信息) 58 | 59 | ::: warning 60 | RouterTab 所在父路由必须提供能访问的默认路由,您可以通过两种方式实现: 61 | 62 | 1. 配置 `redirect` 重定向到子路由 63 | 2. 默认访问路由与父路由路径保持一致。(示例采用当前方案) 64 | 65 | ::: 66 | 67 | **示例:** 68 | 69 | `router.js` 路由 70 | 71 | ```javascript {5,8,17,19,21,23,25,38,39,40,41,42,43} 72 | import Vue from 'vue' 73 | import Router from 'vue-router' 74 | 75 | // RouterTab 内置路由 76 | import { RouterTabRoutes } from 'vue-router-tab' 77 | 78 | // 引入布局框架组件 79 | import Frame from './components/layout/Frame.vue' 80 | 81 | // 异步加载页面组件 82 | const importPage = view => () => 83 | import(/* webpackChunkName: "p-[request]" */ `./views/${view}.vue`) 84 | 85 | Vue.use(Router) 86 | 87 | export default new Router({ 88 | routes: [ 89 | { 90 | path: '/', 91 | // 父路由组件内必须包含 92 | component: Frame, 93 | // 子路由里配置需要通过页签打开的页面路由 94 | children: [ 95 | // 引入 RouterTab 内置路由以支持 Iframe 页签 96 | ...RouterTabRoutes, 97 | { 98 | path: '/', // 默认页和父级路由一致 99 | name: 'Home', 100 | component: importPage('Home'), 101 | meta: { 102 | title: '首页' // 页签标题 103 | } 104 | }, 105 | { 106 | path: '/page/:id', 107 | component: importPage('Page'), 108 | meta: { 109 | title: '页面', // 页签标题 110 | icon: 'icon-user', // 页签图标,可选 111 | tabClass: 'custom-tab', // 自定义页签 class,可选 112 | tips: '这是一个页面', // 页签提示,可选,如未设置则跟 title 一致 113 | key: 'path', // 路由打开页签规则,可选 114 | closable: false // 页签是否允许关闭,默认 `true` 115 | } 116 | }, 117 | { 118 | path: '/404', 119 | component: importPage('404'), 120 | meta: { 121 | title: '找不到页面', 122 | icon: 'icon-page' 123 | } 124 | } 125 | ] 126 | }, 127 | { 128 | // 其他路由 404 129 | path: '*', 130 | redirect: '/404' 131 | } 132 | ] 133 | }) 134 | ``` 135 | 136 | ## 👨‍💻 示例项目 137 | 138 | **router-tab-sample** 139 | 140 | [**Github**](https://github.com/bhuh12/router-tab-sample) 141 | 142 | [**CodeSandbox**](https://codesandbox.io/s/github/bhuh12/router-tab-sample) 143 | 144 | 151 | -------------------------------------------------------------------------------- /docs/guide/essentials/operate.md: -------------------------------------------------------------------------------- 1 | # Tab Operations 2 | 3 | ## Open/Switch 4 | 5 | RouterTab responds to route change, thus, there are two ways to open/switch tabs. 6 | 7 | ### 1. Native Vue Router way (recommended) 8 | 9 | Open tabs in a native Vue Router way. If the link is visited earlier, the exsiting cached tab will be brought to front. 10 | 11 | See [Vue Router Navigation](https://router.vuejs.org/guide/essentials/navigation.html) 12 | 13 | Via `` 14 | 15 | ```html 16 | Page 1 17 | ``` 18 | 19 | Via `router.push`, `router.replace`, `router.go`, etc. 20 | 21 | ```javascript 22 | this.$router.push('/page/1') 23 | ``` 24 | 25 | ### 2. RouterTab built-in method 26 | 27 | `open (path, isReplace = false, refresh = true)` 28 | 29 | This method will reload the existing cached tab by default, which might be usefule if you intend to **force-new-open** a tab. 30 | 31 | 32 | 33 | **Force-new-open** 34 | 35 | ```javascript 36 | this.$tabs.open('/page/2') 37 | ``` 38 | 39 | ## Close 40 | 41 | `close({id, path, match = true, force = true, to, refresh = false})` 42 | 43 | You can close a tab with [`routerTab.close`](../../api/README.md#routertab-close) 44 | 45 | 46 | 47 | **Current tab** 48 | 49 | ```js 50 | this.$tabs.close() 51 | ``` 52 | 53 | **Designated tab** 54 | 55 | ```js 56 | // fullPath 57 | this.$tabs.close('/page/1') 58 | 59 | // location 60 | this.$tabs.close({ 61 | name: 'page', 62 | params: { 63 | id: 2 64 | } 65 | }) 66 | ``` 67 | 68 | **Jump after closed** 69 | 70 | ```js 71 | // close '/page/1', then jump to '/page/2' 72 | this.$tabs.close('/page/1', '/page/2') 73 | 74 | // close current, then jump to '/page/2' 75 | this.$tabs.close({ 76 | to: '/page/2' 77 | }) 78 | ``` 79 | 80 | **Options** 81 | 82 | ```js 83 | this.$tabs.close({ 84 | id: '', // close by tab id, i.e., key. equivalent to path 85 | path: '/page/2', // close by route path, accepts location object. Will close current tab if neither id nor path is provided. 86 | match: false, // should match full path or not in path mode, defaults to true 87 | force: false, // should force close or not, defaults to true 88 | to: '/page/1', // url to jump to after closed, accepts location object. 89 | refresh: true // should refresh the `to` url or not, defaults to false 90 | }) 91 | ``` 92 | 93 | ## Refresh 94 | 95 | `refresh (path, match = true, force = true)` 96 | 97 | You can refresh a tab with [`routerTab.refresh`](../../api/README.md#routertab-refresh) 98 | 99 | 100 | 101 | **Current tab** 102 | 103 | ```js 104 | this.$tabs.refresh() 105 | ``` 106 | 107 | **Designated tab** 108 | 109 | ```js 110 | // fullPath 111 | this.$tabs.refresh('/page/1') 112 | 113 | // location 114 | this.$tabs.refresh({ 115 | name: 'page', 116 | params: { 117 | id: 2 118 | } 119 | }) 120 | ``` 121 | 122 | **Fuzzy matching** 123 | 124 | ```js 125 | // refresh tabs in fuzzy mode 126 | // e.g., '/page/1?query=2' will get refreshed by this rule 127 | this.$tabs.refresh('/page/1', false) 128 | ``` 129 | 130 | ## Refresh All 131 | 132 | `refreshAll (force = false)` 133 | 134 | You can refresh all tabs with [`routerTab.refreshAll`](../../api/README.md#routertab-refreshall) 135 | 136 | **Refresh all** 137 | 138 | ```js 139 | this.$tabs.refreshAll() 140 | ``` 141 | 142 | **Force-refresh all**, ignoring `beforePageLeave` in tab components 143 | 144 | ```js 145 | this.$tabs.refreshAll(true) 146 | ``` 147 | 148 | ## Reset 149 | 150 | `reset (to = this.defaultPath)` 151 | 152 | Generally, when user logged out, we need to reset all tabs to initial state, e.g., close all tabs and restore welcome page. 153 | 154 | You can do that with [`routerTab.reset`](../../api/README.md#routertab-reset) 155 | 156 | ```js 157 | // reset tabs and jump to default page 158 | // (RouterTab will use parent route as default page, 159 | // or you can configure this with `default-page`.) 160 | this.$tabs.reset() 161 | 162 | // reset tabs and jump to /page/2 163 | this.$tabs.reset('/page/2') 164 | ``` 165 | -------------------------------------------------------------------------------- /docs/guide/essentials/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Import RouterTab 4 | 5 | **Example:** 6 | 7 | `main.js` entry file 8 | 9 | ```javascript {6,7,13} 10 | // router-tab requires vue and vue-router 11 | import Vue from 'vue' 12 | import Router from 'vue-router' 13 | 14 | // import RouterTab and styles 15 | import RouterTab from 'vue-router-tab' 16 | import 'vue-router-tab/dist/lib/vue-router-tab.css' 17 | 18 | import App from './App.vue' 19 | import router from './router' 20 | 21 | Vue.config.productionTip = false 22 | Vue.use(RouterTab) 23 | 24 | new Vue({ 25 | router, 26 | render: h => h(App) 27 | }).$mount('#app') 28 | ``` 29 | 30 | ## Use Component 31 | 32 | > More props at [RouterTab Props](../../api/README.md#router-tab-props) 33 | 34 | ::: danger 35 | RouterTab only supports singleton mode, **do not** introduce multiple RouterTab components in the same page! 36 | ::: 37 | 38 | **Example:** 39 | 40 | `components/layout/Frame.vue` layout file 41 | 42 | ```html {5} 43 | 50 | ``` 51 | 52 | ## Router Config 53 | 54 | 1. Integrate RouterTabRoutes into your router config to support [iframe Tab](iframe.md) 55 | 2. Set **title**, **icon**, **tooltip** and **cache rule** in `meta`. 56 | 57 | > Details at [Route.meta](../../api/README.md#route-meta) 58 | 59 | ::: warning 60 | RouterTab need a default route, we can do this in two ways: 61 | 62 | 1. `redirect`: redirect to the default route 63 | 2. the path of default route must keep the same with parent route。 64 | ::: 65 | 66 | **Example:** 67 | 68 | `router.js` router 69 | 70 | ```javascript {5,8,17,19,21,23,25,38,39,40,41,42,43} 71 | import Vue from 'vue' 72 | import Router from 'vue-router' 73 | 74 | // RouterTabRoutes 75 | import { RouterTabRoutes } from 'vue-router-tab' 76 | 77 | // layout component 78 | import Frame from './components/layout/Frame.vue' 79 | 80 | // lazy load 81 | const importPage = view => () => 82 | import(/* webpackChunkName: "p-[request]" */ `./views/${view}.vue`) 83 | 84 | Vue.use(Router) 85 | 86 | export default new Router({ 87 | routes: [ 88 | { 89 | path: '/', 90 | // parent component must contain 91 | component: Frame, 92 | // routes that serve as tab contents 93 | children: [ 94 | // integrate RouterTabRoutes to support iframe tabs 95 | ...RouterTabRoutes, 96 | { 97 | path: '/', // the same with parent route 98 | name: 'Home', 99 | component: importPage('Home'), 100 | meta: { 101 | title: 'Home' 102 | } 103 | }, 104 | { 105 | path: '/page/:id', 106 | component: importPage('Page'), 107 | meta: { 108 | title: 'Page', // tab title 109 | icon: 'icon-user', // tab icon, optional 110 | tabClass: 'custom-tab', // custom class, optional 111 | tips: 'This is a tab', // tab tooltip, optional. defaults to `meta.title` 112 | key: 'path', // tab cache rule, optional 113 | closable: false // is tab closable, defaults to `true` 114 | } 115 | }, 116 | { 117 | path: '/404', 118 | component: importPage('404'), 119 | meta: { 120 | title: 'Page Not Found', 121 | icon: 'icon-page' 122 | } 123 | } 124 | ] 125 | }, 126 | { 127 | // others 128 | path: '*', 129 | redirect: '/404' 130 | } 131 | ] 132 | }) 133 | ``` 134 | 135 | ## 👨‍💻 Sample Project 136 | 137 | **router-tab-sample** 138 | 139 | [**Github**](https://github.com/bhuh12/router-tab-sample) 140 | 141 | [**CodeSandbox**](https://codesandbox.io/s/github/bhuh12/router-tab-sample) 142 | 143 | 150 | -------------------------------------------------------------------------------- /dist/lib/vue-router-tab.css: -------------------------------------------------------------------------------- 1 | .router-tab{display:flex;flex-direction:column;min-height:300px}.router-tab__header{position:relative;z-index:9;display:flex;flex:none;box-sizing:border-box;height:40px;border-bottom:1px solid #eaecef;transition:all .2s ease-in-out}.router-tab__scroll{position:relative;flex:1 1 0px;height:40px;overflow:hidden}.router-tab__scroll-container{width:100%;height:100%;overflow:hidden}.router-tab__scroll-container.is-mobile{overflow-x:auto;overflow-y:hidden}.router-tab__scrollbar{position:absolute;right:0;bottom:0;left:0;height:3px;background-color:rgba(0,0,0,.1);border-radius:3px;opacity:0;transition:opacity .3s ease-in-out}.router-tab__scroll:hover .router-tab__scrollbar,.router-tab__scrollbar.is-dragging{opacity:1}.router-tab__scrollbar-thumb{position:absolute;top:0;left:0;height:100%;background-color:rgba(0,0,0,.1);border-radius:3px;transition:background-color .3s ease-in-out}.router-tab__scrollbar-thumb:hover,.router-tab__scrollbar.is-dragging .router-tab__scrollbar-thumb{background-color:rgba(66,185,131,.8)}.router-tab__nav{position:relative;display:inline-flex;flex-wrap:nowrap;height:100%;margin:0;padding:0;list-style:none}.router-tab__item{position:relative;display:flex;flex:none;align-items:center;padding:0 20px;color:#4d4d4d;font-size:14px;border:1px solid #eaecef;border-left:none;transform-origin:left bottom;cursor:pointer;transition:all .3s ease-in-out;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.router-tab__item:first-child{border-left:1px solid #eaecef}.router-tab__item.is-contextmenu{color:#000}.router-tab__item.is-active,.router-tab__item:hover{color:#42b983}.router-tab__item.is-active.is-closable,.router-tab__item:hover.is-closable{padding:0 11.5px}.router-tab__item.is-active .router-tab__item-close,.router-tab__item:hover .router-tab__item-close{width:13px;margin-left:4px}.router-tab__item.is-active .router-tab__item-close:after,.router-tab__item.is-active .router-tab__item-close:before,.router-tab__item:hover .router-tab__item-close:after,.router-tab__item:hover .router-tab__item-close:before{border-color:#42b983}.router-tab__item.is-active{border-bottom-color:#fff}.router-tab__item.is-drag-over{background:rgba(0,0,0,.05);transition:background .15s ease}.router-tab__item-title{min-width:30px;max-width:100px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.router-tab__item-icon{margin-right:5px;font-size:16px}.router-tab__item-close{position:relative;display:block;width:0;height:13px;margin-left:0;overflow:hidden;border-radius:50%;cursor:pointer;transition:all .3s ease-in-out}.router-tab__item-close:after,.router-tab__item-close:before{position:absolute;top:6px;left:50%;display:block;width:8px;height:1px;margin-left:-4px;background-color:#4d4d4d;transition:background-color .2s ease-in-out;content:""}.router-tab__item-close:before{transform:rotate(-45deg)}.router-tab__item-close:after{transform:rotate(45deg)}.router-tab__item-close:hover{background-color:#a6a6a6}.router-tab__item-close:hover:after,.router-tab__item-close:hover:before{background-color:#fff}.router-tab__container{position:relative;flex:1;overflow-x:hidden;overflow-y:auto;background:#fff;transition:all .4s ease-in-out}.router-tab__container>.router-alive{height:100%}.router-tab__iframe{position:absolute;top:0;left:0;width:100%;height:100%}.router-tab__contextmenu{position:fixed;z-index:999;min-width:120px;padding:8px 0;font-size:14px;background:#fff;border:1px solid #eaecef;box-shadow:1px 1px 4px 0 rgba(0,0,0,.1);transform-origin:left top;transition:all .25s ease-in}.router-tab__contextmenu-item{position:relative;display:block;padding:0 20px;color:#4d4d4d;line-height:30px;cursor:pointer;transition:all .2s ease-in-out;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.router-tab__contextmenu-item:active,.router-tab__contextmenu-item:hover{color:#42b983}.router-tab__contextmenu-item[disabled]{color:#aaa;background:none;cursor:default;pointer-events:none}.has-icon .router-tab__contextmenu-item{padding-left:30px}.router-tab__contextmenu-icon{position:absolute;top:0;left:8px;display:none;line-height:30px}.has-icon .router-tab__contextmenu-icon{display:block}.router-tab-zoom-enter-active,.router-tab-zoom-leave-active{transition:all .4s}.router-tab-zoom-enter,.router-tab-zoom-leave-to{transform:scale(0);opacity:0}.router-tab-swap-enter-active,.router-tab-swap-leave-active{transition:all .5s}.router-tab-swap-enter,.router-tab-swap-leave-to{opacity:0}.router-tab-swap-enter{transform:translateX(-30px)}.router-tab-swap-leave-to{transform:translateX(30px)} -------------------------------------------------------------------------------- /docs/zh/guide/README.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | Vue Router Tab 是基于 Vue.js 和 Vue Router 的路由页签组件,用来实现多页签页面的管理。 4 | 5 | ### 包含的功能 6 | 7 | ✅ 响应路由变化来打开或切换页签 8 | 9 | ✅ 页签过多鼠标滚轮滚动 10 | 11 | ✅ 页签拖拽排序 12 | 13 | ✅ 支持页签打开、切换、关闭、刷新、重置等[操作](essentials/operate.md) 14 | 15 | ✅ [Iframe 页签](essentials/iframe.md)嵌入外部网站 16 | 17 | ✅ 组件个性化设置:[过渡效果](custom/transition.md)、[自定义插槽](custom/slot.md)、[页签右键菜单](custom/contextmenu.md) 18 | 19 | ✅ [多语言支持](custom/i18n.md) 20 | 21 | ✅ 页签切换后[保留滚动位置](custom/scroll.md) 22 | 23 | ✅ [缓存控制](advanced/cache.md):页签规则、页签是否缓存、最大缓存数、是否复用组件等 24 | 25 | ✅ [动态页签信息](advanced/dynamic-tab-info.md):标题、图标、提示 26 | 27 | ✅ [初始页签数据](advanced/initial-tabs.md),进入页面时默认显示的页签 28 | 29 | ✅ [页签刷新还原](advanced/restore.md),在浏览器刷新后恢复页签 30 | 31 | ✅ [页面离开前确认](advanced/page-leave.md) 32 | 33 | ✅ [Nuxt 支持](essentials/nuxt.md) 34 | 35 | ### 浏览器支持 36 | 37 | | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [iOS Safari](http://godban.github.io/browsers-support-badges/)
iOS Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | 38 | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 39 | | IE10, IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | last 2 versions | 40 | -------------------------------------------------------------------------------- /lib/components/TabItem.js: -------------------------------------------------------------------------------- 1 | import { mapGetters } from '../util' 2 | 3 | // 拖拽传输数据前缀 4 | const TRANSFER_PREFIX = 'RouterTabDragSortIndex:' 5 | 6 | // 排序拖拽数据 7 | // 用以解决双核浏览器兼容模式下无法通过 dataTransfer.getData 获取数据 8 | let dragSortData = null 9 | 10 | // 页签项 11 | // @vue/component 12 | export default { 13 | name: 'TabItem', 14 | inject: ['$tabs'], 15 | 16 | props: { 17 | // 页签原始数据,方便 slot 插槽自定义数据 18 | data: { 19 | type: Object, 20 | required: true 21 | }, 22 | 23 | // 页签项索引 24 | index: Number 25 | }, 26 | 27 | data() { 28 | return { 29 | onDragSort: false, // 是否正在拖拽 30 | isDragOver: false // 是否拖拽经过 31 | } 32 | }, 33 | 34 | computed: { 35 | // 从 this.data 提取计算属性 36 | ...mapGetters('data', ['id', 'to', 'icon', 'tabClass', 'target', 'href']), 37 | 38 | // class 39 | classList() { 40 | return [ 41 | 'router-tab__item', 42 | this.tabClass, 43 | { 44 | 'is-active': this.$tabs.activeTabId === this.id, 45 | 'is-closable': this.closable, 46 | 'is-contextmenu': this.$tabs.contextData.id === this.id, 47 | 'is-drag-over': this.isDragOver && !this.onDragSort 48 | } 49 | ] 50 | }, 51 | 52 | // 国际化 53 | i18nText() { 54 | return this.$tabs.i18nText 55 | }, 56 | 57 | // 未命名页签 58 | untitled() { 59 | return this.$tabs.langs.tab.untitled 60 | }, 61 | 62 | // 页签标题 63 | title() { 64 | return this.i18nText(this.data.title) || this.untitled 65 | }, 66 | 67 | // 页签提示 68 | tips() { 69 | return this.i18nText(this.data.tips || this.data.title) || this.untitled 70 | }, 71 | 72 | // 是否可关闭 73 | closable() { 74 | const { keepLastTab, items } = this.$tabs 75 | return this.data.closable !== false && !(keepLastTab && items.length < 2) 76 | } 77 | }, 78 | 79 | methods: { 80 | // 插槽默认内容 81 | slotDefault() { 82 | return [ 83 | this.icon && , 84 | 85 | {this.title} 86 | , 87 | this.closable && ( 88 | 92 | ) 93 | ] 94 | }, 95 | 96 | // 关闭当前页签 97 | close() { 98 | this.$tabs.closeTab(this.id) 99 | }, 100 | 101 | // 拖拽 102 | onDragStart(e) { 103 | this.onDragSort = this.$tabs.onDragSort = true 104 | dragSortData = TRANSFER_PREFIX + this.index 105 | e.dataTransfer.setData('text', dragSortData) 106 | e.dataTransfer.effectAllowed = 'move' 107 | }, 108 | 109 | // 拖拽悬浮 110 | onDragOver(e) { 111 | this.isDragOver = true 112 | e.dataTransfer.dropEffect = 'move' 113 | }, 114 | 115 | // 拖拽结束 116 | onDragEnd() { 117 | this.onDragSort = this.$tabs.onDragSort = false 118 | dragSortData = null 119 | }, 120 | 121 | // 释放后排序 122 | onDrop(e) { 123 | const { items } = this.$tabs 124 | const raw = e.dataTransfer.getData('text') || dragSortData 125 | 126 | this.isDragOver = false 127 | 128 | if (typeof raw !== 'string' || !raw.startsWith(TRANSFER_PREFIX)) return 129 | 130 | const fromIndex = raw.replace(TRANSFER_PREFIX, '') 131 | const tab = items[fromIndex] 132 | 133 | items.splice(fromIndex, 1) 134 | items.splice(this.index, 0, tab) 135 | } 136 | }, 137 | 138 | // 渲染组件 139 | // 使用 jsx render 模式替换 template,避免 Vue 2.5.22 版本不支持子组件使用父组件的 slot 导致出错。 140 | render() { 141 | const { default: slot = this.slotDefault } = this.$tabs.$scopedSlots 142 | 143 | return ( 144 | { 149 | return ( 150 |
  • (this.isDragOver = false)} 158 | vOn:drop_stop_prevent={this.onDrop} 159 | vOn:click_middle={() => this.closable && this.close()} 160 | > 161 | {slot(this)} 162 |
  • 163 | ) 164 | } 165 | }} 166 | >
    167 | ) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Vue.js tab components, based on Vue Router. 4 | 5 | ### Features 6 | 7 | ✅ Open or switch to tabs responding to route change 8 | 9 | ✅ Tabs mouse wheel scrolling 10 | 11 | ✅ Tabs drag sort 12 | 13 | ✅ [Tab Operations](essentials/operate.md): open, switch, close, refresh, reset 14 | 15 | ✅ [Iframe tab](essentials/iframe.md): for external website 16 | 17 | ✅ Customized:[transition](custom/transition.md), [slot](custom/slot.md), [contextmenu](custom/contextmenu.md) 18 | 19 | ✅ [I18n](custom/i18n.md) 20 | 21 | ✅ [Keep scroll position](custom/scroll.md) after tab switching 22 | 23 | ✅ [Cache control](advanced/cache.md): tab rules, cacheable, maximum keep alive, reusable 24 | 25 | ✅ [Dynamic Tab Info](advanced/dynamic-tab-info.md): title, icon, tooltip 26 | 27 | ✅ [Initial Tabs](advanced/initial-tabs.md): initially opened tabs when entering page 28 | 29 | ✅ [Restore Tabs](advanced/restore.md): reopen tabs after browser refresh 30 | 31 | ✅ [Page Leave Confirm](advanced/page-leave.md) 32 | 33 | ✅ [Nuxt Support](essentials/nuxt.md) 34 | 35 | ### Browser compatibility 36 | 37 | | [IE / Edge](http://godban.github.io/browsers-support-badges/)
    IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
    Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
    Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
    Safari | [iOS Safari](http://godban.github.io/browsers-support-badges/)
    iOS Safari | [Opera](http://godban.github.io/browsers-support-badges/)
    Opera | 38 | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 39 | | IE10, IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | last 2 versions | 40 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | 简体中文 2 | 3 |

    4 | 5 | vue-router-tab logo 6 | 7 |

    8 | 9 |

    10 | 11 | Build 12 | 13 | 14 | 15 | vue 16 | 17 | 18 | 19 | vue-router 20 | 21 | 22 | 23 | GitHub last commit 24 | 25 |

    26 | 27 |

    28 | 29 | Version 30 | 31 | 32 | 33 | Downloads 34 | 35 | 36 | 37 | npm bundle size 38 | 39 | 40 | 41 | gzip size: css 42 | 43 | 44 | 45 | License 46 | 47 |

    48 | 49 |

    Vue Router Tab

    50 | 51 | Vue Router Tab 是基于 Vue Router 的路由页签组件,用来实现多页签页面的管理。 52 | 53 | ## 📌 功能 54 | 55 | ✅ 响应路由变化来打开或切换页签 56 | 57 | ✅ 页签过多鼠标滚轮滚动 58 | 59 | ✅ 页签拖拽排序 60 | 61 | ✅ 支持页签打开、切换、关闭、刷新、重置等[操作](https://bhuh12.gitee.io/vue-router-tab/zh/guide/essentials/operate.html) 62 | 63 | ✅ [Iframe 页签](https://bhuh12.gitee.io/vue-router-tab/zh/guide/essentials/iframe.html)嵌入外部网站 64 | 65 | ✅ 组件个性化设置:[过渡效果](https://bhuh12.gitee.io/vue-router-tab/zh/guide/custom/transition.html)、[自定义插槽](https://bhuh12.gitee.io/vue-router-tab/zh/guide/custom/slot.html)、[页签右键菜单](https://bhuh12.gitee.io/vue-router-tab/zh/guide/custom/contextmenu.html) 66 | 67 | ✅ [多语言支持](https://bhuh12.gitee.io/vue-router-tab/zh/guide/custom/i18n.html) 68 | 69 | ✅ 页签切换后[保留滚动位置](https://bhuh12.gitee.io/vue-router-tab/zh/guide/custom/scroll.html) 70 | 71 | ✅ [缓存控制](https://bhuh12.gitee.io/vue-router-tab/zh/guide/advanced/cache.html):页签规则、页签是否缓存、最大缓存数、是否复用组件等 72 | 73 | ✅ [动态页签信息](https://bhuh12.gitee.io/vue-router-tab/zh/guide/advanced/dynamic-tab-info.html):标题、图标、提示 74 | 75 | ✅ [初始页签数据](https://bhuh12.gitee.io/vue-router-tab/zh/guide/advanced/initial-tabs.html),进入页面时默认显示的页签 76 | 77 | ✅ [页签刷新还原](https://bhuh12.gitee.io/vue-router-tab/zh/guide/advanced/restore.html),在浏览器刷新后恢复页签 78 | 79 | ✅ [页面离开前确认](https://bhuh12.gitee.io/vue-router-tab/zh/guide/advanced/page-leave.html) 80 | 81 | ✅ [Nuxt 支持](https://bhuh12.gitee.io/vue-router-tab/zh/guide/essentials/nuxt.html) 82 | 83 | ## 🔗 链接 84 | 85 | ### [🛠 安装](https://bhuh12.gitee.io/vue-router-tab/zh/guide/essentials/installation.html) 86 | 87 | ### [📝 文档](https://bhuh12.gitee.io/vue-router-tab/zh/) 88 | 89 | - [介绍](https://bhuh12.gitee.io/vue-router-tab/zh/guide/) 90 | 91 | - [入门](https://bhuh12.gitee.io/vue-router-tab/zh/guide/essentials/) 92 | 93 | - [API](https://bhuh12.gitee.io/vue-router-tab/zh/api/) 94 | 95 | ### [📺 演示](https://bhuh12.gitee.io/vue-router-tab/demo/) 96 | 97 | ### [👨‍💻 示例项目](https://github.com/bhuh12/router-tab-sample) 98 | 99 | ### [📃 更新日志](https://bhuh12.gitee.io/vue-router-tab/zh/guide/changelog.html) 100 | 101 | --- 102 | 103 | ## 🏷 NPM 任务 104 | 105 | | 任务 | 命令 | 备注 | 106 | | ------------------ | ----------------------- | ----------------------------------------------------- | 107 | | 插件构建 | `yarn lib:build` | 108 | | 插件构建并生成报告 | `yarn lib:build:report` | 109 | | 插件发布 | `yarn lib:publish` | 操作前更改 `package.json` 中的 `version` 为新的版本号 | 110 | | Demo 开发 | `yarn demo:dev` | 111 | | Demo 构建 | `yarn demo:build` | 112 | | 文档开发 | `yarn docs:dev` | 113 | | 文档构建 | `yarn docs:build` | 114 | | 代码风格检查并修复 | `yarn lint` | 115 | | 代码提交 | `yarn commit` | 116 | 117 | ## License 118 | 119 | [MIT](http://opensource.org/licenses/MIT) 120 | 121 | Copyright (c) 2019-present, 碧海幽虹 122 | --------------------------------------------------------------------------------