diff --git a/config/router.config.ts b/config/router.config.ts index 4f2b48b..d5b3716 100644 --- a/config/router.config.ts +++ b/config/router.config.ts @@ -18,7 +18,8 @@ export default [ }, { path: '/', - component: '@/layouts/Index', + component: '@/layouts/BasicLayout', + flatMenu: true, routes: [ { path: '/', @@ -28,88 +29,69 @@ export default [ redirect: '/index', }, { - name: 'index', path: '/index', - meta: { - title: '首页', - }, + name: "首页", + icon: 'icon-shouye', component: '@/pages/index', }, { - name: 'userManage', + name: '管理员管理', path: '/userManage', - meta: { - title: '用户管理', - }, + icon: 'icon-guanliyuan', component: '@/pages/userManage/userManage', }, { - name: 'downloadManage', + name: '下载中心管理', path: '/downloadManage', - meta: { - title: '下载管理', - }, + icon: 'icon-Tab_xiazaizhongxin', component: '@/pages/downloadManage/downloadManage', }, { - name: 'noticeManage', + name: '通知中心管理', path: '/noticeManage', - meta: { - title: '公告管理', - }, + icon: 'icon-tongzhizhongxin', component: '@/pages/noticeManage/noticeManage', }, { - name: 'policyManage', + name: '政策法规管理', path: '/policyManage', + icon: 'icon-zhengcefagui', component: '@/pages/policyManage/policyManage', }, { - name: 'aboutManage', + name: '关于我们管理', path: '/aboutManage', - meta: { - title: '关于我们', - }, + icon: 'icon-guanyuwomen', component: '@/pages/aboutManage/aboutManage', }, { - name: 'helpManage', + name: '帮助中心管理', path: '/helpManage', - meta: { - title: '帮助中心', - }, + icon: 'icon-bangzhuzhongxin', component: '@/pages/helpManage/helpManage', }, { - name: 'readQuestionManage', + name: '已阅问题', path: '/readQuestionManage', - meta: { - title: '已读问题', - }, + icon: 'icon-yiyue', component: '@/pages/userQuestionManage/readQuestionManage', }, { - name: 'unreadQuestionManage', + name: '未阅问题', path: '/unreadQuestionManage', - meta: { - title: '未读问题', - }, + icon: 'icon-weiyuedu', component: '@/pages/userQuestionManage/unreadQuestionManage', }, { - name: 'friendLinkCategory', + name: '友情链接分类', path: '/friendLinkCategory', - meta: { - title: '友情链接分类', - }, + icon: 'icon-youqinglianjie', component: '@/pages/friendLinkManage/friendLinkCategory', }, { - name: 'friendLinkManage', + name: '友情链接管理', path: '/friendLinkManage', - meta: { - title: '友情链接管理', - }, + icon: 'icon-youqinglianjie', component: '@/pages/friendLinkManage/friendLinkManage', }, ] diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx new file mode 100644 index 0000000..3b58522 --- /dev/null +++ b/src/layouts/BasicLayout.tsx @@ -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 ? ( + + + + {intl.formatMessage({ id: `menu.${item.name}` || '' })} + + + ) : ( + + + + {intl.formatMessage({ id: `menu.${item.name}` || '' })} + + + )} + + ); +}; + +const BreadcrumbRender = (routeBreadcrumb: any, intl: any, history: any, dynamicBreadcrumbName: string | null) => { + const breadcrumbRoutes = routeBreadcrumb?.routes; + return ( + + { + history.push('/'); + }} + > + + {intl.formatMessage({ id: 'menu.首页' })} + + + {breadcrumbRoutes?.map((item: any, index: number) => { + // 判断是否是最后一个面包屑项且存在动态名称 + const isLastItem = index === (breadcrumbRoutes.length - 1); + const displayName = (isLastItem && dynamicBreadcrumbName) + ? dynamicBreadcrumbName + : intl.formatMessage({ id: `menu.${item.breadcrumbName}` || '' }); + + return ( + + {displayName} + + ); + })} + + ); +}; + +interface BasicLayoutProps { + children: React.ReactNode; + breadcrumb: BreadcrumbState; + tab: TabModelState; + dispatch: any; +} + +const BasicLayout: React.FC = (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 ( + + { + return MenuRender(menuItemProps, true); + }} + menuItemRender={(item, dom) => { + return MenuRender(item, false); + }} + location={location} + fixSiderbar + layout="mix" + headerRender={() => { + return ; + }} + > + + // 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) => , + }} + > + {children} + + + + ); +}; + +export default connect(({ breadcrumb, tab }: { breadcrumb: BreadcrumbState; tab: TabModelState }) => ({ + breadcrumb, + tab +}))(BasicLayout); + diff --git a/src/layouts/Header.tsx b/src/layouts/Header.tsx index d1c75eb..3a5b84e 100644 --- a/src/layouts/Header.tsx +++ b/src/layouts/Header.tsx @@ -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 (
logo diff --git a/src/layouts/Index.tsx b/src/layouts/Index.tsx deleted file mode 100644 index 79be5cc..0000000 --- a/src/layouts/Index.tsx +++ /dev/null @@ -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 ( - <> - -
- -
- - - - - - - Home - List - App - - {children} - - -
- - ); -}; -export default LayoutIndex; diff --git a/src/layouts/SiderMenu.tsx b/src/layouts/SiderMenu.tsx deleted file mode 100644 index d7f8f36..0000000 --- a/src/layouts/SiderMenu.tsx +++ /dev/null @@ -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 ( -
- - {items.map((item: IMenuItem) => - item.children ? ( - } - > - {item.children?.map((child: IMenuItem) => ( - }> - {intl.formatMessage({ id: child.label })} - - ))} - - ) : ( - }> - {intl.formatMessage({ id: item.label })} - - ), - )} - -
- ); -}; -export default SiderMenu; diff --git a/src/layouts/User.tsx b/src/layouts/User.tsx index 43f1c27..6ebc08c 100644 --- a/src/layouts/User.tsx +++ b/src/layouts/User.tsx @@ -4,7 +4,7 @@ const User: React.FC = (props) => { const intl = useIntl(); return (
- {intl.formatMessage({ id: '登录/注册' })} + {intl.formatMessage({ id: '登录' })}
); }; diff --git a/src/layouts/layout.less b/src/layouts/layout.less index 3b7abb9..ca4deab 100644 --- a/src/layouts/layout.less +++ b/src/layouts/layout.less @@ -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; + } + } + } +} diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index af39d55..2df2b39 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -40,7 +40,7 @@ export default { "采购失败(流标)公告": "Failed Procurement Announcement", "加载更多": "Load More", "登录/注册": "Login/Register", - + "登录": "Login", // Buttons "common.confirm": "Confirm", "common.cancel": "Cancel", diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index 0854aaf..1dec7c2 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -40,6 +40,7 @@ export default { "采购失败(流标)公告": "采购失败(流标)公告", "加载更多": "加载更多", "登录/注册": "登录/注册", + "登录": "登录", // 按钮 "common.confirm": "确定", diff --git a/src/models/breadcrumb.ts b/src/models/breadcrumb.ts new file mode 100644 index 0000000..de2d704 --- /dev/null +++ b/src/models/breadcrumb.ts @@ -0,0 +1,60 @@ + +type Effect = (action: { payload: any }, effects: { call: any; put: any; select: any }) => Generator; +type Reducer = (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; + resetState: Reducer; + }; +} + +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; \ No newline at end of file diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..e774b71 --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,6 @@ +import breadcrumb from './breadcrumb'; + +export default { + breadcrumb, + // ... 其他 models +}; diff --git a/src/models/tab.ts b/src/models/tab.ts new file mode 100644 index 0000000..8105cc8 --- /dev/null +++ b/src/models/tab.ts @@ -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; + }; + 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;