feat: 新增页面标签页组件

This commit is contained in:
ZhuRui
2026-07-27 16:29:12 +08:00
parent e238328919
commit 826a541a60
8 changed files with 529 additions and 47 deletions
+18
View File
@@ -113,3 +113,21 @@
display: flex;
flex-direction: column;
}
// ── 顶部行:原 Header 中的三个区域(折叠按钮 / 面包屑 / 右侧账号信息)整体包成一个 row ──
.headerRow {
padding: 0 16px;
width: 100%;
height: 48px;
display: flex;
align-items: center;
}
// ── 页面标签栏容器(Chrome 风格 PageTabs 的外壳) ──
.pageTabs {
width: 100%;
padding: 4px 16px 0 16px;
height: 42px;
background: #fff;
border-top: 1px solid #f0f0f0;
}
+98 -47
View File
@@ -1,5 +1,6 @@
import { computed, defineComponent, h, ref } from 'vue';
import type { CSSProperties } from 'vue';
import { KeepAlive } from 'vue';
import { useRouter, useRoute, RouterView } from 'vue-router';
import { Layout, Menu, Modal, Breadcrumb, Dropdown, message } from 'ant-design-vue';
import type { MenuProps } from 'ant-design-vue';
@@ -16,8 +17,9 @@ import {
} from '@ant-design/icons-vue';
import { useMenuStore } from '@/stores/menuStore';
import { useUserStore } from '@/stores/userStore';
import { useTabsStore } from '@/stores/tabsStore';
import { useState, useBreadcrumb, auth } from '@/hooks';
import { ChangePasswordModal } from '@/components';
import { ChangePasswordModal, PageTabs } from '@/components';
import logoutSvg from '@/assets/img/icon/logout.svg';
import styles from './BasicLayout.module.less';
@@ -25,11 +27,13 @@ const { Header, Sider, Content, Footer } = Layout;
const headerStyle: CSSProperties = {
background: '#fff',
padding: '0 16px',
padding: '0',
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'center',
color: '#1a1a1a',
boxShadow: '0 1px 4px rgba(0, 21, 41, 0.08)',
height: 'auto',
};
const siderStyle: CSSProperties = {
@@ -93,6 +97,7 @@ export default defineComponent({
const { menuItems } = useMenuStore();
const { phone } = useUserStore();
const { tabs, cachedViews, removeTab } = useTabsStore();
const { breadcrumbs } = useBreadcrumb();
const selectedKeys = computed<string[]>(() => [route.path]);
@@ -107,6 +112,8 @@ export default defineComponent({
collapsed.value = !collapsed.value;
};
const activeTabKey = computed(() => route.path);
/** ===== 退出登录 ===== */
const handleLogout = () => {
Modal.confirm({
@@ -154,6 +161,17 @@ export default defineComponent({
}
};
/** ===== PageTabs 事件 ===== */
const handleTabChange = (key: string) => {
if (key !== route.path) {
router.push(key);
}
};
const handleTabClose = (key: string) => {
removeTab(key);
};
return () => (
<Layout class={styles.container}>
<Sider style={siderStyle} collapsed={collapsed.value} trigger={null} collapsible>
@@ -171,56 +189,89 @@ export default defineComponent({
<Layout class={styles.main}>
<Header style={headerStyle}>
<span class={styles.trigger} onClick={toggleCollapsed}>
{collapsed.value ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</span>
{/*
── 顶部行:折叠按钮 + 面包屑 + 右侧账号信息 ──
按需求把原来平铺在 Header 里的三块统一包到一个 row 容器内,
形成「行容器 + tabs 容器」的两层结构
*/}
<div class={styles.headerRow}>
<span class={styles.trigger} onClick={toggleCollapsed}>
{collapsed.value ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
</span>
{/* 面包屑:紧跟在折叠按钮之后 */}
<Breadcrumb separator=">" class={styles.breadcrumb}>
{breadcrumbs.value.map((item, index) => {
const isLast = index === breadcrumbs.value.length - 1;
return (
<Breadcrumb.Item key={item.path || index}>
{isLast || !item.path ? (
<span>{item.title}</span>
) : (
<a onClick={() => router.push(item.path as string)}>{item.title}</a>
)}
</Breadcrumb.Item>
);
})}
</Breadcrumb>
{/* 面包屑:紧跟在折叠按钮之后 */}
<Breadcrumb separator=">" class={styles.breadcrumb}>
{breadcrumbs.value.map((item, index) => {
const isLast = index === breadcrumbs.value.length - 1;
return (
<Breadcrumb.Item key={item.path || index}>
{isLast || !item.path ? (
<span>{item.title}</span>
) : (
<a onClick={() => router.push(item.path as string)}>{item.title}</a>
)}
</Breadcrumb.Item>
);
})}
</Breadcrumb>
{/* 右侧: 账号信息 + 退出 */}
<div class={styles.headerRight}>
<span class={styles.userPhone}>{phone.value}</span>
<Dropdown trigger={['hover']} placement="bottomLeft">
{{
default: () => <img src={logoutSvg} class={styles.logoutIcon} />,
overlay: () => (
<Menu onClick={handleUserMenuClick}>
<Menu.Item key="changePassword">
<span class={styles.menuItem}>
<EditOutlined />
</span>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="logout">
<span class={`${styles.menuItem} ${styles.menuItemDanger}`}>
<LogoutOutlined />
退
</span>
</Menu.Item>
</Menu>
),
}}
</Dropdown>
{/* 右侧: 账号信息 + 退出 */}
<div class={styles.headerRight}>
<span class={styles.userPhone}>{phone.value}</span>
<Dropdown trigger={['hover']} placement="bottomLeft">
{{
default: () => <img src={logoutSvg} class={styles.logoutIcon} />,
overlay: () => (
<Menu onClick={handleUserMenuClick}>
<Menu.Item key="changePassword">
<span class={styles.menuItem}>
<EditOutlined />
</span>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="logout">
<span class={`${styles.menuItem} ${styles.menuItemDanger}`}>
<LogoutOutlined />
退
</span>
</Menu.Item>
</Menu>
),
}}
</Dropdown>
</div>
</div>
{/*
── Tabs 行:Chrome 风格页面标签栏 ──
数据来自 tabsStore,点击切换路由,关闭按钮走 store.removeTab
*/}
<div class={styles.pageTabs}>
<PageTabs
tabs={tabs.value}
activeKey={activeTabKey.value}
onChange={handleTabChange}
onClose={handleTabClose}
/>
</div>
</Header>
{/*
── 路由出口 ──
用 <KeepAlive :include="cachedViews"> 缓存已访问过的页面,
include 是组件 name 列表,所以页面组件需要在 defineComponent 中显式给出 name
*/}
<Content class={styles.content}>
<RouterView />
<RouterView
v-slots={{
default: ({ Component }: any) => (
<KeepAlive include={cachedViews.value}>
<Component />
</KeepAlive>
),
}}
/>
</Content>
<Footer style={footerStyle}> ©{new Date().getFullYear()}</Footer>