MultiTab.vue 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. <script>
  2. import events from './events'
  3. export default {
  4. name: 'MultiTab',
  5. data () {
  6. return {
  7. fullPathList: [],
  8. pages: [],
  9. activeKey: '',
  10. newTabIndex: 0
  11. }
  12. },
  13. created () {
  14. // bind event
  15. events.$on('open', val => {
  16. if (!val) {
  17. throw new Error(`multi-tab: open tab ${val} err`)
  18. }
  19. this.activeKey = val
  20. }).$on('close', val => {
  21. if (!val) {
  22. this.closeThat(this.activeKey)
  23. return
  24. }
  25. this.closeThat(val)
  26. }).$on('rename', ({ key, name }) => {
  27. console.log('rename', key, name)
  28. try {
  29. const item = this.pages.find(item => item.path === key)
  30. item.meta.customTitle = name
  31. this.$forceUpdate()
  32. } catch (e) {
  33. }
  34. })
  35. this.pages.push(this.$route)
  36. this.fullPathList.push(this.$route.fullPath)
  37. this.selectedLastPath()
  38. // 当前页面的保活由全局混入在组件 mounted 时自动注册,这里无需手动处理
  39. },
  40. methods: {
  41. onEdit (targetKey, action) {
  42. this[action](targetKey)
  43. },
  44. remove (targetKey) {
  45. const closedPage = this.pages.find(page => page.fullPath === targetKey)
  46. this.pages = this.pages.filter(page => page.fullPath !== targetKey)
  47. this.fullPathList = this.fullPathList.filter(path => path !== targetKey)
  48. // 判断当前标签是否关闭,若关闭则跳转到最后一个还存在的标签页
  49. const needNavigate = !this.fullPathList.includes(this.activeKey)
  50. if (needNavigate) {
  51. this.selectedLastPath()
  52. }
  53. // 清理已关闭标签页的组件缓存
  54. this.$nextTick(() => {
  55. this.removePageFromCache(closedPage)
  56. })
  57. },
  58. selectedLastPath () {
  59. this.activeKey = this.fullPathList[this.fullPathList.length - 1]
  60. },
  61. // content menu
  62. closeThat (e) {
  63. // 判断是否为最后一个标签页,如果是最后一个,则无法被关闭
  64. if (this.fullPathList.length > 1) {
  65. this.remove(e)
  66. } else {
  67. this.$message.info('这是最后一个标签了, 无法被关闭')
  68. }
  69. },
  70. closeLeft (e) {
  71. const currentIndex = this.fullPathList.indexOf(e)
  72. if (currentIndex > 0) {
  73. this.fullPathList.forEach((item, index) => {
  74. if (index < currentIndex) {
  75. this.remove(item)
  76. }
  77. })
  78. } else {
  79. this.$message.info('左侧没有标签')
  80. }
  81. },
  82. closeRight (e) {
  83. const currentIndex = this.fullPathList.indexOf(e)
  84. if (currentIndex < (this.fullPathList.length - 1)) {
  85. this.fullPathList.forEach((item, index) => {
  86. if (index > currentIndex) {
  87. this.remove(item)
  88. }
  89. })
  90. } else {
  91. this.$message.info('右侧没有标签')
  92. }
  93. },
  94. closeAll (e) {
  95. const currentIndex = this.fullPathList.indexOf(e)
  96. this.fullPathList.forEach((item, index) => {
  97. if (index !== currentIndex) {
  98. this.remove(item)
  99. }
  100. })
  101. },
  102. closeMenuClick (key, route) {
  103. this[key](route)
  104. },
  105. renderTabPaneMenu (e) {
  106. return (
  107. <a-menu {...{ on: { click: ({ key, item, domEvent }) => { this.closeMenuClick(key, e) } } }}>
  108. <a-menu-item key="closeThat">关闭当前标签</a-menu-item>
  109. <a-menu-item key="closeRight">关闭右侧</a-menu-item>
  110. <a-menu-item key="closeLeft">关闭左侧</a-menu-item>
  111. <a-menu-item key="closeAll">关闭全部</a-menu-item>
  112. </a-menu>
  113. )
  114. },
  115. // render
  116. renderTabPane (title, keyPath) {
  117. const menu = this.renderTabPaneMenu(keyPath)
  118. return (
  119. <a-dropdown overlay={menu} trigger={['contextmenu']}>
  120. <span style={{ userSelect: 'none' }}>{ title }</span>
  121. </a-dropdown>
  122. )
  123. },
  124. /**
  125. * 解析路由对应的组件名称(关闭页签时的默认处理,正常情况优先用 store 中 mounted 记录的真实 name)
  126. */
  127. getRouteComponentName (route) {
  128. if (!route) return null
  129. const layoutComponents = ['BasicLayout', 'RouteView', 'PageView', 'UserLayout', 'BlankLayout']
  130. if (route.matched && route.matched.length > 0) {
  131. const lastRecord = route.matched[route.matched.length - 1]
  132. const instance = lastRecord.instances && lastRecord.instances.default
  133. const instanceName = instance && instance.$options && instance.$options.name
  134. if (instanceName && !layoutComponents.includes(instanceName)) {
  135. return instanceName
  136. }
  137. const component = lastRecord.components && lastRecord.components.default
  138. if (component && component.name && !layoutComponents.includes(component.name)) {
  139. return component.name
  140. }
  141. }
  142. return route.name || null
  143. },
  144. /**
  145. * 关闭标签页时,从缓存中移除对应组件
  146. * 仅当没有其他打开的标签使用同一组件时才移除
  147. */
  148. removePageFromCache (closedPage) {
  149. if (!closedPage) return
  150. const fullPath = closedPage.fullPath
  151. // 优先用页面 mounted 时记入 store 的真实组件名(异步组件名称与路由名常常不一致)
  152. const componentName = this.$store.state.app.routeCacheMap[fullPath] || this.getRouteComponentName(closedPage)
  153. // 移除该页签的映射
  154. this.$store.commit('DEL_ROUTE_CACHE_MAP', fullPath)
  155. if (!componentName) return
  156. // 检查是否还有其他打开的标签页使用同一组件
  157. const stillInUse = this.pages.some(page => {
  158. return (this.$store.state.app.routeCacheMap[page.fullPath] || this.getRouteComponentName(page)) === componentName
  159. })
  160. if (!stillInUse) {
  161. this.$store.dispatch('DelCachedView', componentName)
  162. }
  163. }
  164. },
  165. watch: {
  166. '$route': function (newVal) {
  167. this.activeKey = newVal.fullPath
  168. if (this.fullPathList.indexOf(newVal.fullPath) < 0) {
  169. this.fullPathList.push(newVal.fullPath)
  170. this.pages.push(newVal)
  171. }
  172. // 页面保活的注册由全局混入在组件 mounted 时完成,无需在此手动加入
  173. },
  174. activeKey: function (newPathKey) {
  175. this.$router.push({ path: newPathKey })
  176. }
  177. },
  178. render () {
  179. const { onEdit, $data: { pages } } = this
  180. const panes = pages.map(page => {
  181. return (
  182. <a-tab-pane
  183. style={{ height: 0 }}
  184. tab={this.renderTabPane(page.meta.customTitle || page.meta.title, page.fullPath)}
  185. key={page.fullPath} closable={pages.length > 1}
  186. >
  187. </a-tab-pane>)
  188. })
  189. return (
  190. <div class="ant-pro-multi-tab">
  191. <div class="ant-pro-multi-tab-wrapper">
  192. <a-tabs
  193. hideAdd
  194. type={'editable-card'}
  195. v-model={this.activeKey}
  196. tabBarStyle={{ background: '#FFF', margin: 0, paddingLeft: '16px', paddingTop: '1px' }}
  197. {...{ on: { edit: onEdit } }}>
  198. {panes}
  199. </a-tabs>
  200. </div>
  201. </div>
  202. )
  203. }
  204. }
  205. </script>