浏览代码

feat: multi-tab

Sendya 3 年之前
父节点
当前提交
484483f0e4

+ 1 - 1
public/index.html

@@ -18,7 +18,7 @@
     </noscript>
     <div id="app">
       <div class="first-loading-wrp">
-        <h1>Pro</h1>
+        <h1>Ant Design</h1>
         <div class="loading-wrp">
           <span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
         </div>

+ 17 - 0
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'
+}

+ 183 - 0
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 (
+    <Menu {...props}>
+      <Menu.Item key="closeThat">关闭当前标签</Menu.Item>
+      <Menu.Item key="closeRight">关闭右侧</Menu.Item>
+      <Menu.Item key="closeLeft">关闭左侧</Menu.Item>
+      <Menu.Item key="closeAll">关闭全部</Menu.Item>
+    </Menu>
+  )
+}
+
+const renderTabDropDown = (h, title, keyPath, handles) => {
+  const handleReload = () => {
+    handles['reload'](keyPath)
+  }
+
+  const menus = renderTabMenu(h, keyPath)
+  return (
+    <Dropdown overlay={menus} trigger={['contextmenu']}>
+      <span>
+        <span class='ant-pro-multi-tab-title'>{ title }</span>
+        <Icon title='reload page' class='ant-pro-multi-tab-icon' type={'reload'} onClick={handleReload} />
+      </span>
+    </Dropdown>
+  )
+}
+
+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 (
+        <Tabs.TabPane
+          style={{ height: 0 }}
+          key={page.fullPath}
+          tab={renderTabDropDown(h, i18nRender(title), page.fullPath, handles)}
+          closable={pages.length > 1}
+        ></Tabs.TabPane>
+      )
+    })
+
+    const edit = (targetKey, action) => {
+      // console.log('editTab:', targetKey, 'action:', action)
+      this[action](targetKey)
+    }
+
+    const handleMenuClick = (e) => {
+
+    }
+
+    const controlMenu = (<Dropdown slot="tabBarExtraContent" style={{ marginRight: '8px' }}>
+      <Menu slot="overlay" onClick={handleMenuClick}>
+        <Menu.Item key="1">全部关闭</Menu.Item>
+        <Menu.Item key="2">关闭当前</Menu.Item>
+        <Menu.Item key="3">关闭其他</Menu.Item>
+      </Menu>
+      <Button icon={'align-left'} type={'link'}><Icon type="down" /></Button>
+    </Dropdown>)
+
+    return (
+      <div class={['ant-pro-multi-tab', contentWidth ? 'wide' : null]}>
+        <div class={'ant-pro-multi-tab-wrapper'}>
+          <Tabs
+            hideAdd
+            type={'editable-card'}
+            vModel={this.activeKey}
+            tabBarStyle={customStyle}
+            onEdit={edit}
+          >
+            {tabPanels}
+            {controlMenu}
+          </Tabs>
+        </div>
+      </div>
+    )
+  },
+  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

+ 0 - 162
src/components/MultiTab/MultiTab.vue

@@ -1,162 +0,0 @@
-<script>
-import events from './events'
-
-export default {
-  name: 'MultiTab',
-  data () {
-    return {
-      fullPathList: [],
-      pages: [],
-      activeKey: '',
-      newTabIndex: 0
-    }
-  },
-  created () {
-    // bind event
-    events.$on('open', val => {
-      if (!val) {
-        throw new Error(`multi-tab: open tab ${val} err`)
-      }
-      this.activeKey = val
-    }).$on('close', val => {
-      if (!val) {
-        this.closeThat(this.activeKey)
-        return
-      }
-      this.closeThat(val)
-    }).$on('rename', ({ key, name }) => {
-      console.log('rename', key, name)
-      try {
-        const item = this.pages.find(item => item.path === key)
-        item.meta.customTitle = name
-        this.$forceUpdate()
-      } catch (e) {
-      }
-    })
-
-    this.pages.push(this.$route)
-    this.fullPathList.push(this.$route.fullPath)
-    this.selectedLastPath()
-  },
-  methods: {
-    onEdit (targetKey, action) {
-      this[action](targetKey)
-    },
-    remove (targetKey) {
-      this.pages = this.pages.filter(page => page.fullPath !== targetKey)
-      this.fullPathList = this.fullPathList.filter(path => path !== targetKey)
-      // 判断当前标签是否关闭,若关闭则跳转到最后一个还存在的标签页
-      if (!this.fullPathList.includes(this.activeKey)) {
-        this.selectedLastPath()
-      }
-    },
-    selectedLastPath () {
-      this.activeKey = this.fullPathList[this.fullPathList.length - 1]
-    },
-
-    // content menu
-    closeThat (e) {
-      // 判断是否为最后一个标签页,如果是最后一个,则无法被关闭
-      if (this.fullPathList.length > 1) {
-        this.remove(e)
-      } else {
-        this.$message.info('这是最后一个标签了, 无法被关闭')
-      }
-    },
-    closeLeft (e) {
-      const currentIndex = this.fullPathList.indexOf(e)
-      if (currentIndex > 0) {
-        this.fullPathList.forEach((item, index) => {
-          if (index < currentIndex) {
-            this.remove(item)
-          }
-        })
-      } else {
-        this.$message.info('左侧没有标签')
-      }
-    },
-    closeRight (e) {
-      const currentIndex = this.fullPathList.indexOf(e)
-      if (currentIndex < (this.fullPathList.length - 1)) {
-        this.fullPathList.forEach((item, index) => {
-          if (index > currentIndex) {
-            this.remove(item)
-          }
-        })
-      } else {
-        this.$message.info('右侧没有标签')
-      }
-    },
-    closeAll (e) {
-      const currentIndex = this.fullPathList.indexOf(e)
-      this.fullPathList.forEach((item, index) => {
-        if (index !== currentIndex) {
-          this.remove(item)
-        }
-      })
-    },
-    closeMenuClick (key, route) {
-      this[key](route)
-    },
-    renderTabPaneMenu (e) {
-      return (
-        <a-menu {...{ on: { click: ({ key, item, domEvent }) => { this.closeMenuClick(key, e) } } }}>
-          <a-menu-item key="closeThat">关闭当前标签</a-menu-item>
-          <a-menu-item key="closeRight">关闭右侧</a-menu-item>
-          <a-menu-item key="closeLeft">关闭左侧</a-menu-item>
-          <a-menu-item key="closeAll">关闭全部</a-menu-item>
-        </a-menu>
-      )
-    },
-    // render
-    renderTabPane (title, keyPath) {
-      const menu = this.renderTabPaneMenu(keyPath)
-
-      return (
-        <a-dropdown overlay={menu} trigger={['contextmenu']}>
-          <span style={{ userSelect: 'none' }}>{ title }</span>
-        </a-dropdown>
-      )
-    }
-  },
-  watch: {
-    '$route': function (newVal) {
-      this.activeKey = newVal.fullPath
-      if (this.fullPathList.indexOf(newVal.fullPath) < 0) {
-        this.fullPathList.push(newVal.fullPath)
-        this.pages.push(newVal)
-      }
-    },
-    activeKey: function (newPathKey) {
-      this.$router.push({ path: newPathKey })
-    }
-  },
-  render () {
-    const { onEdit, $data: { pages } } = this
-    const panes = pages.map(page => {
-      return (
-        <a-tab-pane
-          style={{ height: 0 }}
-          tab={this.renderTabPane(page.meta.customTitle || page.meta.title, page.fullPath)}
-          key={page.fullPath} closable={pages.length > 1}
-        >
-        </a-tab-pane>)
-    })
-
-    return (
-      <div class="ant-pro-multi-tab">
-        <div class="ant-pro-multi-tab-wrapper">
-          <a-tabs
-            hideAdd
-            type={'editable-card'}
-            v-model={this.activeKey}
-            tabBarStyle={{ background: '#FFF', margin: 0, paddingLeft: '16px', paddingTop: '1px' }}
-            {...{ on: { edit: onEdit } }}>
-            {panes}
-          </a-tabs>
-        </div>
-      </div>
-    )
-  }
-}
-</script>

+ 69 - 0
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

+ 89 - 0
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 (
+      <route-keep-alive {...props} include={includes} exclude={excludes}>
+        <router-view key={genKey} />
+      </route-keep-alive>
+    )
+  },
+  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

+ 183 - 0
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

+ 12 - 37
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

+ 52 - 17
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;
+    }
+  }
 }

+ 1 - 0
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',

+ 4 - 3
src/layouts/BasicLayout.vue

@@ -48,7 +48,9 @@
     <template v-slot:footerRender>
       <global-footer />
     </template>
-    <router-view />
+    <multi-tab />
+    <!-- <router-view /> -->
+    <route-content />
   </pro-layout>
 </template>
 
@@ -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.