feat: 页面标签组件样式与功能优化
This commit is contained in:
@@ -1,14 +1,24 @@
|
|||||||
import {
|
import {
|
||||||
defineComponent,
|
defineComponent,
|
||||||
|
h,
|
||||||
nextTick,
|
nextTick,
|
||||||
onBeforeUnmount,
|
onBeforeUnmount,
|
||||||
onMounted,
|
onMounted,
|
||||||
ref,
|
ref,
|
||||||
type PropType,
|
type PropType,
|
||||||
|
type VNode,
|
||||||
watch,
|
watch,
|
||||||
} from 'vue';
|
} 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 { 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 type { TabView } from '@/types';
|
||||||
import { useState } from '@/hooks';
|
import { useState } from '@/hooks';
|
||||||
import { renderRouteIcon } from '@/utils/routeIcon';
|
import { renderRouteIcon } from '@/utils/routeIcon';
|
||||||
@@ -17,6 +27,10 @@ import styles from './style.module.less';
|
|||||||
const SCROLL_STEP = 240;
|
const SCROLL_STEP = 240;
|
||||||
|
|
||||||
type TabKeyHandler = (key: string) => void;
|
type TabKeyHandler = (key: string) => void;
|
||||||
|
type TabBatchHandler = (key: string) => void;
|
||||||
|
type TabAllHandler = () => void;
|
||||||
|
|
||||||
|
const renderAiIcon = (Icon: IconType, size = 16) => h(Icon, { size });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chrome 风格的页面标签栏组件
|
* Chrome 风格的页面标签栏组件
|
||||||
@@ -32,6 +46,14 @@ const PageTabs = defineComponent({
|
|||||||
activeKey: { type: String, required: true },
|
activeKey: { type: String, required: true },
|
||||||
/** 关闭标签 */
|
/** 关闭标签 */
|
||||||
onClose: { type: Function as PropType<TabKeyHandler>, default: undefined },
|
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 },
|
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(() => {
|
onMounted(() => {
|
||||||
const el = scrollRef.value;
|
const el = scrollRef.value;
|
||||||
if (!el) return;
|
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 () => (
|
return () => (
|
||||||
<div class={styles.pageTabsWrap}>
|
<div class={styles.pageTabsWrap}>
|
||||||
{showNavLeft.value && (
|
{showNavLeft.value && (
|
||||||
@@ -201,6 +260,24 @@ const PageTabs = defineComponent({
|
|||||||
<RightOutlined />
|
<RightOutlined />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
.pageTabsWrap {
|
.pageTabsWrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@@ -42,13 +44,46 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.navBtnRight {
|
.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 {
|
.pageTabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
width: 100%;
|
box-sizing: border-box;
|
||||||
|
min-width: 0;
|
||||||
|
width: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,7 +79,15 @@ export default defineComponent({
|
|||||||
|
|
||||||
const { menuItems } = useMenuStore();
|
const { menuItems } = useMenuStore();
|
||||||
const { phone } = useUserStore();
|
const { phone } = useUserStore();
|
||||||
const { tabs, cachedViews, removeTab } = useTabsStore();
|
const {
|
||||||
|
tabs,
|
||||||
|
cachedViews,
|
||||||
|
removeTab,
|
||||||
|
removeLeftTabs,
|
||||||
|
removeRightTabs,
|
||||||
|
removeOtherTabs,
|
||||||
|
removeAllTabs,
|
||||||
|
} = useTabsStore();
|
||||||
const { breadcrumbs } = useBreadcrumb();
|
const { breadcrumbs } = useBreadcrumb();
|
||||||
|
|
||||||
const selectedKeys = computed<string[]>(() => [route.path]);
|
const selectedKeys = computed<string[]>(() => [route.path]);
|
||||||
@@ -160,6 +168,22 @@ export default defineComponent({
|
|||||||
removeTab(key);
|
removeTab(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloseLeftTabs = (key: string) => {
|
||||||
|
removeLeftTabs(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseRightTabs = (key: string) => {
|
||||||
|
removeRightTabs(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseOtherTabs = (key: string) => {
|
||||||
|
removeOtherTabs(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseAllTabs = () => {
|
||||||
|
removeAllTabs();
|
||||||
|
};
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<Layout class={styles.basicLayoutMain}>
|
<Layout class={styles.basicLayoutMain}>
|
||||||
<Sider style={siderStyle} collapsed={collapsed.value} trigger={null} collapsible>
|
<Sider style={siderStyle} collapsed={collapsed.value} trigger={null} collapsible>
|
||||||
@@ -241,6 +265,10 @@ export default defineComponent({
|
|||||||
activeKey={activeTabKey.value}
|
activeKey={activeTabKey.value}
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
onClose={handleTabClose}
|
onClose={handleTabClose}
|
||||||
|
onCloseLeft={handleCloseLeftTabs}
|
||||||
|
onCloseRight={handleCloseRightTabs}
|
||||||
|
onCloseOther={handleCloseOtherTabs}
|
||||||
|
onCloseAll={handleCloseAllTabs}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -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 */
|
/** 关闭除 path 之外的所有 tab */
|
||||||
const removeOtherTabs = (path: string): void => {
|
const removeOtherTabs = (path: string): void => {
|
||||||
|
const currentPath = router.currentRoute.value.path;
|
||||||
state.tabs = state.tabs.filter((t) => t.path === path);
|
state.tabs = state.tabs.filter((t) => t.path === path);
|
||||||
syncCachedViews();
|
syncCachedViews();
|
||||||
|
if (currentPath !== path && state.tabs.length > 0) {
|
||||||
|
router.push(path);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 关闭所有 tab,保留第一个并跳转过去 */
|
/** 关闭所有 tab,保留第一个并跳转过去 */
|
||||||
@@ -106,6 +142,8 @@ export function useTabsStore() {
|
|||||||
cachedViews: computed(() => state.cachedViews),
|
cachedViews: computed(() => state.cachedViews),
|
||||||
addTab,
|
addTab,
|
||||||
removeTab,
|
removeTab,
|
||||||
|
removeLeftTabs,
|
||||||
|
removeRightTabs,
|
||||||
removeOtherTabs,
|
removeOtherTabs,
|
||||||
removeAllTabs,
|
removeAllTabs,
|
||||||
updateTab,
|
updateTab,
|
||||||
|
|||||||
Reference in New Issue
Block a user