-
Pro
+
Ant Design
diff --git a/src/components/MultiTab/APIEnums.js b/src/components/MultiTab/APIEnums.js
new file mode 100644
index 0000000000..596a376402
--- /dev/null
+++ b/src/components/MultiTab/APIEnums.js
@@ -0,0 +1,17 @@
+export const TAB_BINDING = {
+ TAB_CLOSE: 'hook:tab:ck',
+ TAB_CLOSE_ALL: 'hook:tab:ca',
+ TAB_CLOSE_LEFT: 'hook:tab:cl',
+ TAB_CLOSE_RIGHT: 'hook:tab:cr',
+ TAB_CLOSE_OTHER: 'hook:tab:co',
+ TAB_NAME: 'hook:tab:rename',
+ TAB_ACTIVE: 'hook:tab:active'
+}
+
+export const ROUTE_BINDING = {
+ R_OPEN: 'hook:open',
+ R_CLOSE: 'hook:close',
+ R_ACTIVE: 'hook:active',
+ R_REFRESH: 'hook:refresh',
+ R_GET_CACHES: 'hook:caches'
+}
diff --git a/src/components/MultiTab/MultiTab.jsx b/src/components/MultiTab/MultiTab.jsx
new file mode 100644
index 0000000000..ae7210c985
--- /dev/null
+++ b/src/components/MultiTab/MultiTab.jsx
@@ -0,0 +1,183 @@
+import AppEvent from './events'
+import { TAB_BINDING } from './APIEnums'
+import { Menu, Dropdown, Button, Icon, Tabs } from 'ant-design-vue'
+// import { i18nRender } from '@/locales'
+import './index.less'
+
+const customStyle = {
+ background: '#FFF',
+ margin: 0,
+ paddingLeft: '16px',
+ paddingTop: '1px'
+}
+
+const i18nRender = (context) => context
+
+const renderTabMenu = (h, path) => {
+ const props = {
+ on: {
+ click: ({ key, item, domEvent }) => {
+ // console.log('key', path)
+ }
+ }
+ }
+ return (
+
+ )
+}
+
+const renderTabDropDown = (h, title, keyPath, handles) => {
+ const handleReload = () => {
+ handles['reload'](keyPath)
+ }
+
+ const menus = renderTabMenu(h, keyPath)
+ return (
+
+
+ { title }
+
+
+
+ )
+}
+
+const MultiTab = {
+ name: 'MultiTab',
+ props: {
+ contentWidth: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data () {
+ return {
+ activeKey: '',
+ pages: [],
+ fullPathList: []
+ }
+ },
+ render (h) {
+ const { $data: { pages }, contentWidth } = this
+ const handles = {
+ cls: (keyPath) => {
+ // console.log('close', keyPath)
+ // this.closeThat(keyPath)
+ this.$tab.close(keyPath, false)
+ },
+ reload: (keyPath) => {
+ // console.log('reload', keyPath)
+ this.$tab.refresh(keyPath)
+ }
+ }
+ const tabPanels = pages.map(page => {
+ const title = page.meta.customTitle || page.meta.title
+ return (
+
1}
+ >
+ )
+ })
+
+ const edit = (targetKey, action) => {
+ // console.log('editTab:', targetKey, 'action:', action)
+ this[action](targetKey)
+ }
+
+ const handleMenuClick = (e) => {
+
+ }
+
+ const controlMenu = (
+
+
+ )
+
+ return (
+
+
+
+ {tabPanels}
+ {controlMenu}
+
+
+
+ )
+ },
+ created () {
+ const { pages, fullPathList } = this
+ AppEvent.$on(TAB_BINDING.TAB_CLOSE, val => {
+ this.closeThat(val)
+ }).$on('hook:tab:closeRight', val => {
+ // console.log('hook:tab:closeRight', val)
+ }).$on('hook:tab:closeLeft', val => {
+ // console.log('hook:tab:closeRight', val)
+ }).$on('hook:tab:closeAll', val => {
+ // console.log('hook:tab:closeRight', val)
+ }).$on('hook:tab:rename', val => {
+ // console.log('hook:tab:rename', val)
+ })
+
+ pages.push(this.$route)
+ fullPathList.push(this.$route.fullPath)
+ this.activeLastTab()
+
+ this.$watch('$route', newVal => {
+ const { fullPath, params } = newVal
+ if (this.activeKey !== fullPath) {
+ this.activeKey = fullPath
+ }
+ if (this.fullPathList.indexOf(fullPath) < 0) {
+ this.fullPathList.push(fullPath)
+ if (params && params._tabName) {
+ const newPage = Object.assign({}, newVal, {
+ meta: {
+ customTitle: params._tabName
+ }
+ })
+ this.pages.push(newPage)
+ } else {
+ this.pages.push(newVal)
+ }
+ }
+ })
+ this.$watch('activeKey', pathKey => {
+ this.$router.push({ path: pathKey })
+ })
+ },
+ methods: {
+ activeLastTab () {
+ this.activeKey = this.fullPathList[this.fullPathList.length - 1]
+ },
+ remove (targetKey) {
+ this.closeThat(targetKey)
+ },
+ closeThat (targetKey) {
+ if (this.fullPathList.length > 1) {
+ this.pages = this.pages.filter(page => page.fullPath !== targetKey)
+ this.fullPathList = this.fullPathList.filter(path => path !== targetKey)
+ this.activeLastTab()
+ }
+ }
+ }
+}
+
+export default MultiTab
diff --git a/src/components/MultiTab/MultiTab.vue b/src/components/MultiTab/MultiTab.vue
deleted file mode 100644
index bfb6e57a45..0000000000
--- a/src/components/MultiTab/MultiTab.vue
+++ /dev/null
@@ -1,162 +0,0 @@
-
diff --git a/src/components/MultiTab/RouteAPI.js b/src/components/MultiTab/RouteAPI.js
new file mode 100644
index 0000000000..e56b4604d2
--- /dev/null
+++ b/src/components/MultiTab/RouteAPI.js
@@ -0,0 +1,69 @@
+import { ROUTE_BINDING } from './APIEnums'
+import AppEvent from './events'
+
+const RouteAPI = {
+ /**
+ * Open a new tab(route)
+ * 打开一个新标签(路由)
+ *
+ * @param config: {
+ * routeName,
+ * title
+ * }
+ */
+ open (config) {
+ AppEvent.$emit(ROUTE_BINDING.R_OPEN, config)
+ },
+ /**
+ * Close a tab
+ * 关闭一个打开的标签
+ * 如果标签没打开或找不到标签则不做任何事情
+ *
+ * @param config
+ * @param cache: bool true 则关闭后会缓存页面,false 反之
+ */
+ close (config, cache) {
+ AppEvent.$emit(ROUTE_BINDING.R_CLOSE, { config, isCache: cache })
+ },
+ /**
+ * Active a opened tab
+ * 激活一个已经打开的 tab
+ * @param config
+ */
+ activeTab (config) {
+ AppEvent.$emit(ROUTE_BINDING.R_ACTIVE, config)
+ },
+ /**
+ * Replace current page to new Route
+ * 替换当前 tab 页面为一个新的页面
+ * 注意:这个替换会更新路由地址,但是 tab 的位置不会发生变化
+ * 被替换的路由还会被缓存住,下次打开还是缓存时的状态
+ *
+ * @param {*} config
+ */
+ replace (config) {
+ AppEvent.$emit('hook:replace', config)
+ },
+ /**
+ * Refresh current tab (clear page cache)
+ */
+ refresh (keyPath) {
+ AppEvent.$emit(ROUTE_BINDING.R_REFRESH, keyPath)
+ },
+ closeAll () {
+ AppEvent.$emit('hook:closeAll')
+ },
+ closeOthers () {
+ AppEvent.$emit('hook:closeOthers')
+ },
+ /**
+ * Get all cached page
+ *
+ * @param callback
+ */
+ caches (callback) {
+ AppEvent.$emit(ROUTE_BINDING.R_GET_CACHES, callback)
+ }
+}
+
+export default RouteAPI
diff --git a/src/components/MultiTab/RouteContent.jsx b/src/components/MultiTab/RouteContent.jsx
new file mode 100644
index 0000000000..29806e7eab
--- /dev/null
+++ b/src/components/MultiTab/RouteContent.jsx
@@ -0,0 +1,89 @@
+/* eslint-disable */
+import { ROUTE_BINDING, TAB_BINDING } from './APIEnums'
+import AppEvent from './events'
+import RouteAPI from './RouteAPI'
+import MultiTab from './MultiTab'
+import RouteKeepAlive from './RouteKeepAlive'
+
+const addAndGet = val => {
+ return val >= Number.MAX_SAFE_INTEGER ? 0 : ++val
+}
+
+const RouteContent = {
+ name: 'RouteContent',
+ data () {
+ return {
+ includes: [],
+ excludes: [],
+ /*
+ * Cache: { fullPath : String, snapshot : Number }
+ * cached: Map
+ */
+ cached: {}
+ }
+ },
+ render () {
+ const {
+ $route: { meta, fullPath },
+ includes,
+ excludes,
+ cached
+ } = this
+
+ const handleRef = (ref) => {
+ this.keepRef = ref
+ }
+ console.log('meta', this.$route);
+ if (meta.keepAlive) {
+ if (includes.findIndex(item => item === fullPath) === -1) {
+ includes.push(fullPath)
+ cached[fullPath] = {
+ fullPath,
+ snapshot: 0
+ }
+ }
+ }
+ const genKey = cached[fullPath].fullPath + cached[fullPath].snapshot
+ const props = {
+ on: {
+ ref: handleRef
+ }
+ }
+ return (
+
+
+
+ )
+ },
+ created () {
+ AppEvent.$on(ROUTE_BINDING.R_OPEN, ({ routeName, title, ...rest }) => {
+ this.$router.push({ name: routeName, params: { '_tabName': title, ...rest } })
+ }).$on(ROUTE_BINDING.R_REFRESH, keyPath => {
+ const { $route: { fullPath } } = this
+ let key = keyPath || fullPath
+ const cache = this.cached[key]
+ this.keepRef.clearCache(key)
+ cache.snapshot = addAndGet(cache.snapshot)
+ // how with
+ this.$forceUpdate()
+ }).$on(ROUTE_BINDING.R_GET_CACHES, (callback) => {
+ callback(this.keepRef.allCache())
+ }).$on(ROUTE_BINDING.R_CLOSE, val => {
+ const { config: keyPath, isCache } = val
+ AppEvent.$emit(TAB_BINDING.TAB_CLOSE, keyPath)
+ // this.keepRef.clearCache(keyPath)
+ })
+ }
+}
+
+RouteContent.install = function (Vue) {
+ if (Vue.prototype.$tab) {
+ return
+ }
+ Vue.prototype.$tab = RouteAPI
+ Vue.component(RouteContent.name, RouteContent)
+ Vue.component(RouteKeepAlive.name, RouteKeepAlive)
+ Vue.component(MultiTab.name, MultiTab)
+}
+
+export default RouteContent
diff --git a/src/components/MultiTab/RouteKeepAlive.js b/src/components/MultiTab/RouteKeepAlive.js
new file mode 100644
index 0000000000..ac8ceef075
--- /dev/null
+++ b/src/components/MultiTab/RouteKeepAlive.js
@@ -0,0 +1,183 @@
+const _toString = Object.prototype.toString
+
+const isRegExp = (v) => {
+ return _toString.call(v) === '[object RegExp]'
+}
+
+const isDef = (v) => {
+ return v !== undefined && v !== null
+}
+
+const isAsyncPlaceholder = (node) => {
+ return node.isComment && node.asyncFactory
+}
+
+const remove = (arr, item) => {
+ if (arr.length) {
+ const index = arr.indexOf(item)
+ if (index > -1) {
+ return arr.splice(index, 1)
+ }
+ }
+}
+
+export const getFirstComponentChild = (children) => {
+ if (Array.isArray(children)) {
+ for (let i = 0; i < children.length; i++) {
+ const c = children[i]
+ if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
+ return c
+ }
+ }
+ }
+}
+
+export const getComponentName = (opts) => {
+ return opts && (opts.Ctor.options.name || opts.tag)
+}
+
+export const getComponentKey = (opts) => {
+ return opts && (opts.Ctor.cid + (opts.tag ? `::${opts.tag}` : ''))
+}
+
+export const getCurrentRouteKey = ($route) => {
+ return $route.fullPath
+}
+
+const matches = (pattern, name) => {
+ if (Array.isArray(pattern)) {
+ return pattern.indexOf(name) > -1
+ } else if (typeof pattern === 'string') {
+ return pattern.split(',').indexOf(name) > -1
+ } else if (isRegExp(pattern)) {
+ return pattern.test(name)
+ }
+ /* istanbul ignore next */
+ return false
+}
+
+const pruneCache = (keepAliveInstance, cacheKey) => {
+ const { cache, keys, _vnode } = keepAliveInstance
+ for (const key in cache) {
+ const cachedNode = cache[key]
+ if (cachedNode) {
+ if (cacheKey === key) {
+ pruneCacheEntry(cache, key, keys, _vnode)
+ }
+ }
+ }
+}
+
+const pruneCacheEntry = (
+ cache,
+ key,
+ keys,
+ current
+) => {
+ const cached = cache[key]
+ if (cached && (!current || cached.tag !== current.tag)) {
+ cached.componentInstance.$destroy()
+ }
+ cache[key] = null
+ remove(keys, key)
+}
+
+/* const findCached = (cached, vnode) => {
+
+} */
+
+const RouteKeepAlive = {
+ name: 'RouteKeepAlive',
+ abstract: true,
+ props: {
+ include: {
+ type: [String, Array],
+ default: ''
+ },
+ exclude: {
+ type: [String, Array],
+ default: ''
+ },
+ max: {
+ type: [String, Number],
+ default: null
+ }
+ },
+
+ created () {
+ this.cache = Object.create(null)
+ this.keys = []
+ },
+
+ destroyed () {
+ for (const key in this.cache) {
+ pruneCacheEntry(this.cache, key, this.keys)
+ }
+ },
+
+ mounted () {
+ this.$watch('include', val => {
+ pruneCache(this, val)
+ })
+ this.$watch('exclude', val => {
+ pruneCache(this, val)
+ })
+ },
+
+ methods: {
+ allCache () {
+ return this.cache
+ },
+ clearCache (key) {
+ pruneCache(this, key)
+ this.$router.replace(this.$router.currentRoute)
+ }
+ },
+
+ render () {
+ this.$emit('ref', this)
+ const slot = this.$slots.default
+ const vnode = getFirstComponentChild(slot)
+ // const vnode = cloneVNode(defVNode, true)
+ const componentOptions = vnode && vnode.componentOptions
+
+ const key = getCurrentRouteKey(this.$route)
+ if (componentOptions) {
+ // check pattern
+ const { include, exclude } = this
+ if (
+ // not included
+ (include && (!key || !matches(include, key))) ||
+ // excluded
+ (exclude && key && matches(exclude, key))
+ ) {
+ return vnode
+ }
+
+ const { cache, keys } = this
+ if (cache[key]) {
+ vnode.componentInstance = cache[key].componentInstance
+ // make current key freshest
+ remove(keys, key)
+ keys.push(key)
+ } else {
+ // vnode = cloneVNode(vnode, true)
+
+ cache[key] = vnode
+ keys.push(key)
+ // prune oldest entry
+ if (this.max && keys.length > parseInt(this.max)) {
+ pruneCacheEntry(cache, keys[0], keys, this._vnode)
+ }
+ }
+ vnode.data.keepAlive = true
+ }
+ return vnode || (slot && slot[0])
+ }
+}
+
+RouteKeepAlive.install = function (Vue) {
+ Vue.component(RouteKeepAlive.name, RouteKeepAlive)
+}
+
+export default RouteKeepAlive
diff --git a/src/components/MultiTab/index.js b/src/components/MultiTab/index.js
index 02a1c77d64..2d9402703a 100644
--- a/src/components/MultiTab/index.js
+++ b/src/components/MultiTab/index.js
@@ -1,40 +1,15 @@
-import events from './events'
+import AppEvent from './events'
+import RouteAPI from './RouteAPI'
import MultiTab from './MultiTab'
-import './index.less'
+import * as APIEnums from './APIEnums'
+import RouteKeepAlive from './RouteKeepAlive'
+import RouteContent from './RouteContent'
-const api = {
- /**
- * open new tab on route fullPath
- * @param config
- */
- open: function (config) {
- events.$emit('open', config)
- },
- rename: function (key, name) {
- events.$emit('rename', { key: key, name: name })
- },
- /**
- * close current page
- */
- closeCurrentPage: function () {
- this.close()
- },
- /**
- * close route fullPath tab
- * @param config
- */
- close: function (config) {
- events.$emit('close', config)
- }
+export {
+ AppEvent,
+ RouteAPI,
+ MultiTab,
+ RouteKeepAlive,
+ APIEnums
}
-
-MultiTab.install = function (Vue) {
- if (Vue.prototype.$multiTab) {
- return
- }
- api.instance = events
- Vue.prototype.$multiTab = api
- Vue.component('multi-tab', MultiTab)
-}
-
-export default MultiTab
+export default RouteContent
diff --git a/src/components/MultiTab/index.less b/src/components/MultiTab/index.less
index 773e3af319..45cbeeb23f 100644
--- a/src/components/MultiTab/index.less
+++ b/src/components/MultiTab/index.less
@@ -1,25 +1,60 @@
-@import '../index';
+@import "~ant-design-vue/es/style/themes/default";
-@multi-tab-prefix-cls: ~"@{ant-pro-prefix}-multi-tab";
-@multi-tab-wrapper-prefix-cls: ~"@{ant-pro-prefix}-multi-tab-wrapper";
+@multi-tab-prefix-cls: ~"ant-pro-multi-tab";
+@multi-tab-wrapper-prefix-cls: ~"ant-pro-multi-tab-wrapper";
-/*
-.topmenu .@{multi-tab-prefix-cls} {
- max-width: 1200px;
- margin: -23px auto 24px auto;
-}
-*/
.@{multi-tab-prefix-cls} {
- margin: -23px -24px 24px -24px;
background: #fff;
-}
+ padding-top: 10px;
+ margin: -24px;
+ margin-bottom: 24px;
-.topmenu .@{multi-tab-wrapper-prefix-cls} {
- max-width: 1200px;
- margin: 0 auto;
+ .ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-active {
+ padding-bottom: 0;
+ }
+ .ant-tabs-nav .ant-tabs-tab .anticon {
+ margin-right: 4px;
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ .ant-pro-multi-tab-title {
+ user-select: none;
+ margin-right: 6px;
+ font-weight: 500;
+ }
+ .ant-pro-multi-tab-icon {
+ height: @font-size-base;
+ overflow: hidden;
+ color: @text-color-secondary;
+ font-size: @font-size-sm;
+ vertical-align: middle;
+ transition: all 0.3s;
+ &:hover {
+ color: @heading-color;
+ }
+ }
}
-.topmenu.content-width-Fluid .@{multi-tab-wrapper-prefix-cls} {
- max-width: 100%;
- margin: 0 auto;
+.topmenu {
+ .@{multi-tab-prefix-cls} {
+ border-bottom: 1px solid #e8e8e8;
+
+ .@{multi-tab-wrapper-prefix-cls} {
+ max-width: 100%;
+ margin: 0 auto;
+ }
+
+ &.wide .@{multi-tab-wrapper-prefix-cls} {
+ max-width: 1200px;
+ margin: 0 auto;
+ }
+
+ .ant-tabs {
+ height: 39px;
+ }
+ .ant-tabs-bar {
+ border-bottom: 0;
+ }
+ }
}
diff --git a/src/config/router.config.js b/src/config/router.config.js
index 0054b22418..52d9b7ed80 100644
--- a/src/config/router.config.js
+++ b/src/config/router.config.js
@@ -1,6 +1,7 @@
// eslint-disable-next-line
import { UserLayout, BasicLayout, BlankLayout } from '@/layouts'
import { bxAnaalyse } from '@/core/icons'
+// import RouteContent from '@/components/MultiTab'
const RouteView = {
name: 'RouteView',
diff --git a/src/layouts/BasicLayout.vue b/src/layouts/BasicLayout.vue
index 0784393760..64e84df4aa 100644
--- a/src/layouts/BasicLayout.vue
+++ b/src/layouts/BasicLayout.vue
@@ -48,7 +48,9 @@
-
+
+
+
@@ -57,7 +59,6 @@ import { SettingDrawer, updateTheme } from '@ant-design-vue/pro-layout'
import { i18nRender } from '@/locales'
import { mapState } from 'vuex'
import { CONTENT_WIDTH_TYPE, SIDEBAR_TYPE, TOGGLE_MOBILE_TYPE } from '@/store/mutation-types'
-
import defaultSettings from '@/config/defaultSettings'
import RightContent from '@/components/GlobalHeader/RightContent'
import GlobalFooter from '@/components/GlobalFooter'
@@ -72,7 +73,7 @@ export default {
GlobalFooter,
LogoSvg,
Ads
- },
+},
data () {
return {
// preview.pro.antdv.com only use.