修改布局 ,增加tab切换页面功能

This commit is contained in:
linxd
2025-07-14 09:45:10 +08:00
parent 7f5527e654
commit 89d7a8a2e1
12 changed files with 444 additions and 220 deletions

144
src/layouts/BasicLayout.tsx Normal file
View File

@ -0,0 +1,144 @@
// src/layouts/BasicLayout.tsx
import React from 'react';
import ProLayout, { PageContainer } from '@ant-design/pro-layout';
import { Link, useLocation, useIntl, useHistory } from 'umi';
import { connect } from 'dva';
import defaultSettings from '../../config/defaultSettings';
import routes from '../../config/router.config'; // 引入你的自定义路由结构
import { ConfigProvider, Breadcrumb } from 'antd';
import HeaderComponent from './Header';
import IconFont from '@/components/IconFont/IconFont';
import type { BreadcrumbState } from '@/models/breadcrumb';
import type { TabModelState, TabItem } from '@/models/tab';
const MenuRender = (item: any, isSubMenu: boolean) => {
const intl = useIntl();
return (
<>
{isSubMenu ? (
<span className="ant-pro-menu-item">
<IconFont type={item.icon as string} />
<span className="ant-pro-menu-item-title">
{intl.formatMessage({ id: `menu.${item.name}` || '' })}
</span>
</span>
) : (
<Link className="ant-pro-menu-item" key={item.path} to={item.path || '/'} innerRef={null}>
<IconFont type={item.icon as string} />
<span className="ant-pro-menu-item-title">
{intl.formatMessage({ id: `menu.${item.name}` || '' })}
</span>
</Link>
)}
</>
);
};
const BreadcrumbRender = (routeBreadcrumb: any, intl: any, history: any, dynamicBreadcrumbName: string | null) => {
const breadcrumbRoutes = routeBreadcrumb?.routes;
return (
<Breadcrumb>
<Breadcrumb.Item
onClick={() => {
history.push('/');
}}
>
<span style={{ cursor: 'pointer' }}>
{intl.formatMessage({ id: 'menu.首页' })}
</span>
</Breadcrumb.Item>
{breadcrumbRoutes?.map((item: any, index: number) => {
// 判断是否是最后一个面包屑项且存在动态名称
const isLastItem = index === (breadcrumbRoutes.length - 1);
const displayName = (isLastItem && dynamicBreadcrumbName)
? dynamicBreadcrumbName
: intl.formatMessage({ id: `menu.${item.breadcrumbName}` || '' });
return (
<Breadcrumb.Item key={item.path}>
{displayName}
</Breadcrumb.Item>
);
})}
</Breadcrumb>
);
};
interface BasicLayoutProps {
children: React.ReactNode;
breadcrumb: BreadcrumbState;
tab: TabModelState;
dispatch: any;
}
const BasicLayout: React.FC<BasicLayoutProps> = (props) => {
const { children, breadcrumb, tab, dispatch } = props;
const location = useLocation();
const intl = useIntl();
const history = useHistory();
const handleTabChange = (key: string) => {
dispatch({
type: 'tab/switchTab',
payload: { key }
});
};
const handleTabEdit = (targetKey: any, action: string) => {
if (action === 'remove') {
dispatch({
type: 'tab/closeTab',
payload: { key: targetKey }
});
}
};
return (
<ConfigProvider>
<ProLayout
{...defaultSettings}
route={{ routes }}
subMenuItemRender={(menuItemProps, defaultDom) => {
return MenuRender(menuItemProps, true);
}}
menuItemRender={(item, dom) => {
return MenuRender(item, false);
}}
location={location}
fixSiderbar
layout="mix"
headerRender={() => {
return <HeaderComponent />;
}}
>
<PageContainer
ghost={true}
header={{
title: false,
// breadcrumbRender: ({ breadcrumb: routeBreadcrumb }) =>
// BreadcrumbRender(routeBreadcrumb, intl, history, breadcrumb.breadcrumbName),
}}
tabList={tab.tabList}
tabProps={{
type: 'editable-card',
hideAdd: true,
activeKey: tab.activeKey,
onChange: handleTabChange,
onEdit: handleTabEdit,
size: 'small',
tabBarGutter: 6,
renderTabBar: (props, DefaultTabBar) => <DefaultTabBar {...props} className="custom-tab-bar" />,
}}
>
{children}
</PageContainer>
</ProLayout>
</ConfigProvider>
);
};
export default connect(({ breadcrumb, tab }: { breadcrumb: BreadcrumbState; tab: TabModelState }) => ({
breadcrumb,
tab
}))(BasicLayout);

View File

@ -4,7 +4,8 @@ import LogoImg from '@/assets/img/logo.png';
//导入菜单组件
import Language from './Language';
import User from './User';
const HeaderComponent: React.FC = (props) => {
import './layout.less';
const HeaderComponent: React.FC = () => {
return (
<div className="headerComponent">
<img className="logo" src={LogoImg} alt="logo" />

View File

@ -1,34 +0,0 @@
import React from 'react';
// import Header from './Header';
import { Layout, Breadcrumb } from 'antd';
const { Header, Footer, Sider, Content } = Layout;
//导入logo图片
import HeaderComponent from './Header';
import SiderMenu from './SiderMenu';
import './layout.less';
const LayoutIndex: React.FC = (props) => {
const { children } = props;
return (
<>
<Layout>
<Header className="header">
<HeaderComponent />
</Header>
<Layout>
<Sider width={200} theme="light">
<SiderMenu />
</Sider>
<Layout className="layout-content">
<Breadcrumb style={{ margin: '10px 0' }}>
<Breadcrumb.Item>Home</Breadcrumb.Item>
<Breadcrumb.Item>List</Breadcrumb.Item>
<Breadcrumb.Item>App</Breadcrumb.Item>
</Breadcrumb>
<Content>{children}</Content>
</Layout>
</Layout>
</Layout>
</>
);
};
export default LayoutIndex;

View File

@ -1,140 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Menu } from 'antd';
import { useIntl, Link, useHistory } from 'umi';
import IconFont from '@/components/IconFont/IconFont';
interface IMenuItem {
label: string;
key: string;
path?: string;
icon: string;
children?: IMenuItem[];
}
// 引入样式文件 useIntl().formatMessage({ id: 'menu.首页' }),
const items: IMenuItem[] = [
{
label: 'menu.首页',
key: 'index',
path: '/index',
icon: 'icon-shouye',
},
{
label: 'menu.管理员管理',
key: 'userManage',
path: '/userManage',
icon: 'icon-guanliyuan',
},
{
label: 'menu.下载中心管理',
key: 'downloadManage',
path: '/downloadManage',
icon: 'icon-Tab_xiazaizhongxin',
},
{
label: 'menu.通知中心管理',
key: 'noticeManage',
path: '/noticeManage',
icon: 'icon-tongzhizhongxin',
},
{
label: 'menu.政策法规管理',
key: 'policyManage',
path: '/policyManage',
icon: 'icon-zhengcefagui',
},
{
label: 'menu.关于我们管理',
key: 'aboutManage',
path: '/aboutManage',
icon: 'icon-guanyuwomen',
},
{
label: 'menu.帮助中心管理',
key: 'helpManage',
path: '/helpManage',
icon: 'icon-bangzhuzhongxin'
},
{
label: 'menu.用户提问管理',
key: 'userQuestionManage',
icon: 'icon-yonghutiwen',
path: '/userQuestionManage',
children: [
{
label: 'menu.已阅问题',
key: 'readQuestionManage',
icon: 'icon-yiyue',
path: '/readQuestionManage',
},
{
label: 'menu.未阅问题',
key: 'unreadQuestionManage',
icon: 'icon-weiyuedu',
path: '/unreadQuestionManage',
},
],
},
{
label: 'menu.友情链接管理',
key: 'friendLinkManage',
icon: 'icon-youqinglianjie',
children: [
{
label: 'menu.友情链接分类',
key: 'friendLinkCategory',
icon: 'icon-fenlei',
path: '/friendLinkCategory',
},
{
label: 'menu.友情链接列表',
key: 'friendLinkList',
icon: 'icon-liebiaomoshi',
path: '/friendLinkManage',
},
],
},
];
const SiderMenu: React.FC = (props) => {
//当前激活菜单
const [current, setCurrent] = useState('index');
const intl = useIntl();
const history = useHistory();
useEffect(() => {
// 获取当前激活菜单
const path = history.location.pathname;
const menu = items.find((item) => item.path === path);
if (menu) {
setCurrent(menu.key);
} else {
// 如果跳转的详情页面获取根级激活菜单
const rootActiveMenu = path.split('/')[1];
setCurrent(rootActiveMenu);
}
}, [history.location.pathname]);
return (
<div className="header-menu">
<Menu selectedKeys={[current]} mode="inline">
{items.map((item: IMenuItem) =>
item.children ? (
<Menu.SubMenu
key={item.key}
title={intl.formatMessage({ id: item.label })}
icon={<IconFont type={item.icon} />}
>
{item.children?.map((child: IMenuItem) => (
<Menu.Item key={child.key} icon={<IconFont type={child.icon} />}>
<Link to={child.path}>{intl.formatMessage({ id: child.label })}</Link>
</Menu.Item>
))}
</Menu.SubMenu>
) : (
<Menu.Item key={item.key} icon={<IconFont type={item.icon} />}>
<Link to={item.path}>{intl.formatMessage({ id: item.label })}</Link>
</Menu.Item>
),
)}
</Menu>
</div>
);
};
export default SiderMenu;

View File

@ -4,7 +4,7 @@ const User: React.FC = (props) => {
const intl = useIntl();
return (
<div className="user">
<Link to={'/login'}>{intl.formatMessage({ id: '登录/注册' })}</Link>
<Link to={'/login'}>{intl.formatMessage({ id: '登录' })}</Link>
</div>
);
};

View File

@ -20,8 +20,32 @@
align-items: center;
}
.layout-content {
background: rgb(245,246,250);
background: rgb(245, 246, 250);
padding: 0 15px;
height: calc(100vh - 64px);
overflow: auto;
}
.ant-page-header.has-footer{
padding-top: 0 !important;
}
.custom-tab-bar {
margin-bottom: 10px !important;
.ant-tabs-tab {
border-radius: 5px !important;
font-size: 13px !important;
padding: 6px 10px !important;
.ant-tabs-tab-remove{
margin-left: 0 !important;
font-size: 10px !important;
}
&.ant-tabs-tab-active{
background-color: @main-color !important;
.ant-tabs-tab-btn{
color: #fff !important;
}
.ant-tabs-tab-remove{
color: #fff !important;
}
}
}
}

View File

@ -40,7 +40,7 @@ export default {
"采购失败(流标)公告": "Failed Procurement Announcement",
"加载更多": "Load More",
"登录/注册": "Login/Register",
"登录": "Login",
// Buttons
"common.confirm": "Confirm",
"common.cancel": "Cancel",

View File

@ -40,6 +40,7 @@ export default {
"采购失败(流标)公告": "采购失败(流标)公告",
"加载更多": "加载更多",
"登录/注册": "登录/注册",
"登录": "登录",
// 按钮
"common.confirm": "确定",

60
src/models/breadcrumb.ts Normal file
View File

@ -0,0 +1,60 @@
type Effect = (action: { payload: any }, effects: { call: any; put: any; select: any }) => Generator<any, void, unknown>;
type Reducer<S> = (state: S, action: { payload: any }) => S;
export interface BreadcrumbState {
breadcrumbName: string | null;
}
export interface BreadcrumbModelType {
namespace: 'breadcrumb';
state: BreadcrumbState;
effects: {
updateBreadcrumbName: Effect;
resetBreadcrumb: Effect;
};
reducers: {
setBreadcrumbName: Reducer<BreadcrumbState>;
resetState: Reducer<BreadcrumbState>;
};
}
const BreadcrumbModel: BreadcrumbModelType = {
namespace: 'breadcrumb',
state: {
breadcrumbName: null,
},
effects: {
*updateBreadcrumbName({ payload }, { put }) {
yield put({
type: 'setBreadcrumbName',
payload,
});
},
*resetBreadcrumb(_, { put }) {
yield put({
type: 'resetState',
});
},
},
reducers: {
setBreadcrumbName(state: BreadcrumbState, { payload }: { payload: string }) {
return {
...state,
breadcrumbName: payload,
};
},
resetState() {
return {
breadcrumbName: null,
};
},
},
};
export default BreadcrumbModel;

6
src/models/index.ts Normal file
View File

@ -0,0 +1,6 @@
import breadcrumb from './breadcrumb';
export default {
breadcrumb,
// ... 其他 models
};

180
src/models/tab.ts Normal file
View File

@ -0,0 +1,180 @@
import { Effect, Reducer, Subscription } from 'umi';
import { history } from 'umi';
import routes from '../../config/router.config';
export interface TabItem {
tab: string;
key: string;
path: string;
closable: boolean;
}
export interface TabModelState {
tabList: TabItem[];
activeKey: string;
}
export interface TabModelType {
namespace: 'tab';
state: TabModelState;
effects: {
addTab: Effect;
closeTab: Effect;
switchTab: Effect;
};
reducers: {
updateState: Reducer<TabModelState>;
};
subscriptions: {
setup: Subscription;
};
}
// 递归查找路由
const findRouteByPath = (path: string, routeData: any[]): any => {
let result = null;
for (const route of routeData) {
if (route.path === path) {
result = route;
break;
}
if (route.routes) {
const subResult = findRouteByPath(path, route.routes);
if (subResult) {
result = subResult;
break;
}
}
}
return result;
};
const TabModel: TabModelType = {
namespace: 'tab',
state: {
tabList: [],
activeKey: '/',
},
effects: {
*addTab({ payload }, { call, put, select }) {
const { tabList } = yield select((state: any) => state.tab);
const { path, tab, key } = payload;
// 检查tab是否已存在
const isExist = tabList.find((item: TabItem) => item.key === key);
if (!isExist) {
// 添加新tab
yield put({
type: 'updateState',
payload: {
tabList: [...tabList, { tab, key, path, closable: true }],
activeKey: key,
},
});
} else {
// 切换到已有tab
yield put({
type: 'updateState',
payload: {
activeKey: key,
},
});
}
},
*closeTab({ payload }, { call, put, select }) {
const { key } = payload;
const { tabList, activeKey } = yield select((state: any) => state.tab);
// 过滤掉要关闭的tab
const newTabList = tabList.filter((item: TabItem) => item.key !== key);
// 如果关闭的是当前激活的tab则切换到前一个tab
let newActiveKey = activeKey;
if (key === activeKey) {
const index = tabList.findIndex((item: TabItem) => item.key === key);
newActiveKey = index === 0 ? (newTabList[0] || {}).key : tabList[index - 1].key;
// 切换路由
const targetTab = newTabList.find((item: TabItem) => item.key === newActiveKey);
if (targetTab) {
history.push(targetTab.path);
}
}
yield put({
type: 'updateState',
payload: {
tabList: newTabList,
activeKey: newActiveKey,
},
});
},
*switchTab({ payload }, { call, put, select }) {
const { key } = payload;
const { tabList } = yield select((state: any) => state.tab);
// 找到目标tab并跳转
const targetTab = tabList.find((item: TabItem) => item.key === key);
if (targetTab) {
history.push(targetTab.path);
}
yield put({
type: 'updateState',
payload: {
activeKey: key,
},
});
}
},
reducers: {
updateState(state, { payload }) {
return {
...state,
...payload,
};
},
},
subscriptions: {
setup({ dispatch }) {
return history.listen((location) => {
const { pathname } = location;
// 首页不做特殊处理
if (pathname === '/') {
dispatch({
type: 'updateState',
payload: {
activeKey: '/',
},
});
return;
}
// 查找当前路由对应的菜单项
const currentRoute = findRouteByPath(pathname, routes);
if (currentRoute) {
// 如果找到对应路由添加或激活对应的tab
dispatch({
type: 'addTab',
payload: {
path: pathname,
key: pathname,
tab: currentRoute.name || '未命名页面',
},
});
}
});
},
},
};
export default TabModel;