feat: 页面标签组件样式与功能优化

This commit is contained in:
ZhuRui
2026-07-30 18:02:41 +08:00
parent 7606bb7d8d
commit ea906a0d92
4 changed files with 208 additions and 4 deletions
+78 -1
View File
@@ -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<TabKeyHandler>, default: undefined },
/** 关闭当前标签左侧标签 */
onCloseLeft: { type: Function as PropType<TabBatchHandler>, default: undefined },
/** 关闭当前标签右侧标签 */
onCloseRight: { type: Function as PropType<TabBatchHandler>, default: undefined },
/** 关闭当前标签以外的标签 */
onCloseOther: { type: Function as PropType<TabBatchHandler>, default: undefined },
/** 关闭全部标签 */
onCloseAll: { type: Function as PropType<TabAllHandler>, default: undefined },
/** 切换标签 */
onChange: { type: Function as PropType<TabKeyHandler>, 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) => (
<Menu.Item key={key}>
<div class={styles.batchMenuItem}>
<span class={styles.batchMenuIcon}>{icon}</span>
<span>{label}</span>
</div>
</Menu.Item>
);
return () => (
<div class={styles.pageTabsWrap}>
{showNavLeft.value && (
@@ -201,6 +260,24 @@ const PageTabs = defineComponent({
<RightOutlined />
</button>
)}
<Dropdown trigger={['click']} placement="bottomRight">
{{
default: () => (
<button type="button" class={styles.batchBtn} aria-label="标签页操作菜单">
{renderAiIcon(LuMoreHorizontal)}
</button>
),
overlay: () => (
<Menu onClick={({ key }) => handleBatchClose(String(key))}>
{renderBatchMenuItem('left', renderAiIcon(LuPanelLeftClose), '关闭左侧标签页')}
{renderBatchMenuItem('right', renderAiIcon(LuPanelRightClose), '关闭右侧标签页')}
{renderBatchMenuItem('other', renderAiIcon(LuCopyX), '关闭其它标签页')}
{renderBatchMenuItem('all', renderAiIcon(LuListX), '关闭全部标签页')}
</Menu>
),
}}
</Dropdown>
</div>
);
},
+63 -2
View File
@@ -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;
}
}