├── .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 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/frames/Reuse.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/frames/MaxAlive.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/frames/Dragsort.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/frames/CloseLastTab.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/views/Page1.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
页面1
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/views/Page2.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
页面2
4 |
5 |
6 |
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 |
2 |
11 |
12 |
--------------------------------------------------------------------------------
/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
页面找不到了!!!
10 |
11 |
12 |
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 |
2 |
18 |
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 |
2 |
3 |
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 |
2 |
3 |
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 |
2 |
3 |
4 |
5 |
35 |
--------------------------------------------------------------------------------
/src/components/frames/LangEn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
3 |
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 |
2 |
3 |
4 | |
5 | name
6 | |
7 | {{ $route.name }} |
8 |
9 |
10 | | path |
11 | {{ $route.path }} |
12 |
13 |
14 | | params |
15 | {{ $route.params }} |
16 |
17 |
18 | | query |
19 | {{ $route.query }} |
20 |
21 |
22 | | hash |
23 | {{ $route.hash }} |
24 |
25 |
26 | | fullPath |
27 | {{ $route.fullPath }} |
28 |
29 |
30 | | meta |
31 | {{ $route.meta }} |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/DocLinks.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | API
4 | Demo
5 |
6 |
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 |
2 |
7 | Demo
8 |
27 |
28 |
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 |
44 | html 代码
45 |
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 |
2 |
3 | 你在 {{ pageTime }} 秒前打开本页面
4 |
5 |
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 |
2 |
3 |
4 |
5 |
43 |
--------------------------------------------------------------------------------
/src/components/AppAside.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
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 |
2 |
14 |
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 |
2 |
3 |
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 |
2 |
16 |
17 |
18 |
33 |
34 |
65 |
--------------------------------------------------------------------------------
/src/views/TabDynamic.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
动态页签
4 |
5 |
11 |
12 |
18 |
19 |
30 |
31 |
32 |
33 |
48 |
49 |
70 |
--------------------------------------------------------------------------------
/src/components/MenuItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
18 |
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 |
17 |
18 |
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. 将 `