feature/0723/ZR #6
@@ -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>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string[]>(() => [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 () => (
|
||||
<Layout class={styles.basicLayoutMain}>
|
||||
<Sider style={siderStyle} collapsed={collapsed.value} trigger={null} collapsible>
|
||||
@@ -241,6 +265,10 @@ export default defineComponent({
|
||||
activeKey={activeTabKey.value}
|
||||
onChange={handleTabChange}
|
||||
onClose={handleTabClose}
|
||||
onCloseLeft={handleCloseLeftTabs}
|
||||
onCloseRight={handleCloseRightTabs}
|
||||
onCloseOther={handleCloseOtherTabs}
|
||||
onCloseAll={handleCloseAllTabs}
|
||||
/>
|
||||
</div>
|
||||
</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 */
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user
不要写这种长链路的if