From ea906a0d9243dd206ed9ed67c454ac8d65914049 Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Thu, 30 Jul 2026 18:02:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A1=B5=E9=9D=A2=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=A0=B7=E5=BC=8F=E4=B8=8E=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/pageTabs/index.tsx | 79 ++++++++++++++++++++++- src/components/pageTabs/style.module.less | 65 ++++++++++++++++++- src/layouts/BasicLayout.tsx | 30 ++++++++- src/stores/tabsStore.ts | 38 +++++++++++ 4 files changed, 208 insertions(+), 4 deletions(-) diff --git a/src/components/pageTabs/index.tsx b/src/components/pageTabs/index.tsx index a301785..ff9c669 100644 --- a/src/components/pageTabs/index.tsx +++ b/src/components/pageTabs/index.tsx @@ -1,14 +1,24 @@ import { defineComponent, + h, nextTick, onBeforeUnmount, onMounted, ref, type PropType, + type VNode, watch, } from 'vue'; -import { message } from 'ant-design-vue'; +import { Dropdown, Menu, message } from 'ant-design-vue'; import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue'; +import type { IconType } from 'vue-icons-plus/lib'; +import { + LuCopyX, + LuListX, + LuMoreHorizontal, + LuPanelLeftClose, + LuPanelRightClose, +} from 'vue-icons-plus/lu'; import type { TabView } from '@/types'; import { useState } from '@/hooks'; import { renderRouteIcon } from '@/utils/routeIcon'; @@ -17,6 +27,10 @@ import styles from './style.module.less'; const SCROLL_STEP = 240; type TabKeyHandler = (key: string) => void; +type TabBatchHandler = (key: string) => void; +type TabAllHandler = () => void; + +const renderAiIcon = (Icon: IconType, size = 16) => h(Icon, { size }); /** * Chrome 风格的页面标签栏组件 @@ -32,6 +46,14 @@ const PageTabs = defineComponent({ activeKey: { type: String, required: true }, /** 关闭标签 */ onClose: { type: Function as PropType, default: undefined }, + /** 关闭当前标签左侧标签 */ + onCloseLeft: { type: Function as PropType, default: undefined }, + /** 关闭当前标签右侧标签 */ + onCloseRight: { type: Function as PropType, default: undefined }, + /** 关闭当前标签以外的标签 */ + onCloseOther: { type: Function as PropType, default: undefined }, + /** 关闭全部标签 */ + onCloseAll: { type: Function as PropType, default: undefined }, /** 切换标签 */ onChange: { type: Function as PropType, default: undefined }, }, @@ -116,6 +138,34 @@ const PageTabs = defineComponent({ } }; + const handleBatchClose = (key: string) => { + if (props.tabs.length <= 1) { + message.warning('至少保留一个标签'); + return; + } + + const activeIndex = props.tabs.findIndex((tab) => tab.path === props.activeKey); + if (activeIndex === -1) return; + + if (key === 'left') { + if (activeIndex === 0) { + message.warning('左侧没有可关闭的标签页'); + return; + } + props.onCloseLeft?.(props.activeKey); + } else if (key === 'right') { + if (activeIndex === props.tabs.length - 1) { + message.warning('右侧没有可关闭的标签页'); + return; + } + props.onCloseRight?.(props.activeKey); + } else if (key === 'other') { + props.onCloseOther?.(props.activeKey); + } else if (key === 'all') { + props.onCloseAll?.(); + } + }; + onMounted(() => { const el = scrollRef.value; if (!el) return; @@ -174,6 +224,15 @@ const PageTabs = defineComponent({ ); }; + const renderBatchMenuItem = (key: string, icon: VNode, label: string) => ( + +
+ {icon} + {label} +
+
+ ); + return () => (
{showNavLeft.value && ( @@ -201,6 +260,24 @@ const PageTabs = defineComponent({ )} + + + {{ + default: () => ( + + ), + overlay: () => ( + handleBatchClose(String(key))}> + {renderBatchMenuItem('left', renderAiIcon(LuPanelLeftClose), '关闭左侧标签页')} + {renderBatchMenuItem('right', renderAiIcon(LuPanelRightClose), '关闭右侧标签页')} + {renderBatchMenuItem('other', renderAiIcon(LuCopyX), '关闭其它标签页')} + {renderBatchMenuItem('all', renderAiIcon(LuListX), '关闭全部标签页')} + + ), + }} +
); }, diff --git a/src/components/pageTabs/style.module.less b/src/components/pageTabs/style.module.less index b94d883..4c9957f 100644 --- a/src/components/pageTabs/style.module.less +++ b/src/components/pageTabs/style.module.less @@ -2,6 +2,8 @@ .pageTabsWrap { position: relative; + display: flex; + align-items: center; width: 100%; height: 100%; @@ -42,13 +44,46 @@ } .navBtnRight { - right: 0; + right: 34px; + } + + .batchBtn { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + margin-left: 6px; + padding: 0; + border: none; + border-radius: 6px; + background: #fff; + color: rgba(0, 0, 0, 0.45); + box-shadow: 0 0 4px rgba(214, 214, 214, 0.9); + cursor: pointer; + transition: + color 0.15s, + background-color 0.15s; + + &:hover { + color: rgba(0, 0, 0, 0.88); + background: #f5f5f5; + } + + &:active { + color: rgba(0, 0, 0, 0.88); + background: #ebebeb; + } } .pageTabs { display: flex; + flex: 1; align-items: flex-start; - width: 100%; + box-sizing: border-box; + min-width: 0; + width: auto; height: 100%; overflow-x: auto; overflow-y: hidden; @@ -227,3 +262,29 @@ } } } + +.batchMenuItem { + display: flex; + align-items: center; + padding: 4px 0; + gap: 8px; + line-height: 1; +} + +.batchMenuItem + .batchMenuItem { + margin-top: 4px; +} + +.batchMenuIcon { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + line-height: 1; + + > svg { + display: block; + } +} diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index 2f0693b..ae6b97b 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -79,7 +79,15 @@ export default defineComponent({ const { menuItems } = useMenuStore(); const { phone } = useUserStore(); - const { tabs, cachedViews, removeTab } = useTabsStore(); + const { + tabs, + cachedViews, + removeTab, + removeLeftTabs, + removeRightTabs, + removeOtherTabs, + removeAllTabs, + } = useTabsStore(); const { breadcrumbs } = useBreadcrumb(); const selectedKeys = computed(() => [route.path]); @@ -160,6 +168,22 @@ export default defineComponent({ removeTab(key); }; + const handleCloseLeftTabs = (key: string) => { + removeLeftTabs(key); + }; + + const handleCloseRightTabs = (key: string) => { + removeRightTabs(key); + }; + + const handleCloseOtherTabs = (key: string) => { + removeOtherTabs(key); + }; + + const handleCloseAllTabs = () => { + removeAllTabs(); + }; + return () => ( @@ -241,6 +265,10 @@ export default defineComponent({ activeKey={activeTabKey.value} onChange={handleTabChange} onClose={handleTabClose} + onCloseLeft={handleCloseLeftTabs} + onCloseRight={handleCloseRightTabs} + onCloseOther={handleCloseOtherTabs} + onCloseAll={handleCloseAllTabs} /> diff --git a/src/stores/tabsStore.ts b/src/stores/tabsStore.ts index d9c6bcc..12a548d 100644 --- a/src/stores/tabsStore.ts +++ b/src/stores/tabsStore.ts @@ -69,10 +69,46 @@ export function useTabsStore() { } }; + /** 关闭 path 左侧的所有 tab */ + const removeLeftTabs = (path: string): void => { + const idx = state.tabs.findIndex((t) => t.path === path); + if (idx <= 0) return; + + const currentPath = router.currentRoute.value.path; + const shouldRedirect = state.tabs.slice(0, idx).some((t) => t.path === currentPath); + + state.tabs = state.tabs.slice(idx); + syncCachedViews(); + + if (shouldRedirect) { + router.push(path); + } + }; + + /** 关闭 path 右侧的所有 tab */ + const removeRightTabs = (path: string): void => { + const idx = state.tabs.findIndex((t) => t.path === path); + if (idx === -1 || idx >= state.tabs.length - 1) return; + + const currentPath = router.currentRoute.value.path; + const shouldRedirect = state.tabs.slice(idx + 1).some((t) => t.path === currentPath); + + state.tabs = state.tabs.slice(0, idx + 1); + syncCachedViews(); + + if (shouldRedirect) { + router.push(path); + } + }; + /** 关闭除 path 之外的所有 tab */ const removeOtherTabs = (path: string): void => { + const currentPath = router.currentRoute.value.path; state.tabs = state.tabs.filter((t) => t.path === path); syncCachedViews(); + if (currentPath !== path && state.tabs.length > 0) { + router.push(path); + } }; /** 关闭所有 tab,保留第一个并跳转过去 */ @@ -106,6 +142,8 @@ export function useTabsStore() { cachedViews: computed(() => state.cachedViews), addTab, removeTab, + removeLeftTabs, + removeRightTabs, removeOtherTabs, removeAllTabs, updateTab,