修改布局 ,增加tab切换页面功能
This commit is contained in:
144
src/layouts/BasicLayout.tsx
Normal file
144
src/layouts/BasicLayout.tsx
Normal 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);
|
||||
|
@ -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" />
|
||||
|
@ -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;
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ export default {
|
||||
"采购失败(流标)公告": "Failed Procurement Announcement",
|
||||
"加载更多": "Load More",
|
||||
"登录/注册": "Login/Register",
|
||||
|
||||
"登录": "Login",
|
||||
// Buttons
|
||||
"common.confirm": "Confirm",
|
||||
"common.cancel": "Cancel",
|
||||
|
@ -40,6 +40,7 @@ export default {
|
||||
"采购失败(流标)公告": "采购失败(流标)公告",
|
||||
"加载更多": "加载更多",
|
||||
"登录/注册": "登录/注册",
|
||||
"登录": "登录",
|
||||
|
||||
// 按钮
|
||||
"common.confirm": "确定",
|
||||
|
60
src/models/breadcrumb.ts
Normal file
60
src/models/breadcrumb.ts
Normal 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
6
src/models/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import breadcrumb from './breadcrumb';
|
||||
|
||||
export default {
|
||||
breadcrumb,
|
||||
// ... 其他 models
|
||||
};
|
180
src/models/tab.ts
Normal file
180
src/models/tab.ts
Normal 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;
|
Reference in New Issue
Block a user