给React路由加上Tabs页签效果
发布于 7 年前 作者 yunqiangwu 10021 次浏览 来自 分享

给React路由加上Tabs页签效果

很多后台管理系统中都有多Tab窗口切换多效果比如 jQadmin 中的的效果:

jQadmin github 链接

jQadmin中实现的tabs切换的效果以及很多中台后端系统中这种类似的功能大部分是通过 iframe 标签来实现的。

如果要走在 React 框架中实现这种效果,我们不能使用 iframe 来实现,react构建到大型常规应用中,一般都会有一个全局到 state 状态树,而网页到多个 frame 中 js 到执行环境是隔离的,所以没办法统一使用 Redux 这样就工具做到统一的状态管理了,这个方案pass、掉。

那么如何在React中实现Tabs效果,你可能想到到第一个方案是通过 Antd 提供到 Tabs 组件实现这个需求,但是还是会有一个问题,在 Tabs 下面切换页面是并不是通过 React-Router 切换页面到,所以这个方案 pass 掉。

解决思路

在 React-Router中 有一个 history 对象,history这个为React Router提供核心功能的包。它能轻松地在客户端为项目添加基于location的导航,这种对于单页应用至关重要的功能。

通过 history 我们可以:

  • 获取当前页面路由
  • 监听页面路由变化
  • 设置当前页面切换到另外一个页面

所以我们只要拿到history对象,我们完全可以实现 Tabs 页面切换这个功能

在 React-Router 4 中还提供一张根简单到方式获取 history 对象: withRouter高阶组件,提供了history让你使用~

import React from "react";
import {withRouter} from "react-router-dom";
class MyComponent extends React.Component {
 ...
 myFunction() {
 this.props.history.push("/some/Path");
 }
 ...
}
export default withRouter(MyComponent);

为了实现这个功能,我们可以定义一个组件 RouterTabs

RouterTabs 中可以使用 @withRouter 注入 history 。

在组件中 通过 history 监听 路由到变化,当切换到新到页面时,在RouterTabs的state中增加一个页签到数据,并把当前到路由参数也保存下来 ,并设置当前路由对于的页签为激活状态,切换到其他路由时,根据 history 的事件变更组件到状态,让组件和页面保持同步。

点击 RouterTabs 上面到标签,通过 history控制页面路由的切换。

具体实现代码如下:


import React, { Component } from 'react';
import ReactDom from 'react-dom';
import classNames from 'classnames';
import { Tag, Dropdown, Icon, Tooltip, Menu } from 'antd';
import styles from './index.less';
import {withRouter} from "react-router-dom";
const { SubMenu } = Menu;
// 模拟全局路由配置对象,
const routerCcnfig = [
 '/a': {name: 'A页面'},
 '/b': {name: 'B页面'}
];
// 通过 pathname 获取 pathname 对应到路由描述信息对象
const getTitleByPathname = (pathname) => {
 return routerCcnfig[pathname]:pathname;
}
@withRouter
export class RouterTabs extends Component {
 static unListen = null;
 static defaultProps = {
 initialValue: [],
 };
 constructor(props) {
 super(props);
 const { pathname } = this.props.location;
 
 this.state = {
 currentPageName: [], // 当前路由对应到 pathname
 refsTag: [], // tabs 所有到所有页签
 searchMap: {}, // 每个 页签对应的路由参数
 };
 this.handleMenuClick = this.handleMenuClick.bind(this);
 }
 componentDidMount() {
 if (this.unListen) {
 this.unListen();
 this.unListen = null;
 }
 // 监听路由切换事件
 this.unListen = this.props.history.listen((_location) => {
 if (this.didUnMounted) {
 return;
 }
 if (this.notListenOnce) {
 this.notListenOnce = false;
 return;
 }
 const { pathname } = _location;
 if (pathname === '/' || !getTitleByPathname(pathname)) {
 this.setState({
 currentPageName: '',
 });
 return;
 }
 const newRefsTag = [...this.state.refsTag];
 const currentPathname = pathname;
 if (newRefsTag.indexOf(currentPathname) === -1) {
 newRefsTag.push(currentPathname);
 }
 this.state.searchMap[pathname] = _location.search;
 this.setState({
 currentPageName: pathname,
 refsTag: newRefsTag,
 });
 // 假如是新的 导航item 添加进来,需要在 添加完成后调用 scrollIntoTags
 clearTimeout(this.tagChangeTimerId);
 this.tagChangeTimerId = setTimeout(() => {
 this.scrollIntoTags(pathname);
 }, 100);
 });
 const { pathname } = this.props.location;
 this.scrollIntoTags(pathname);
 }
 componentWillUnmount() {
 this.didUnMounted = true;
 if (this.unListen) {
 this.unListen();
 this.unListen = null;
 }
 }
 scrollIntoTags(pathname) {
 let dom;
 try {
 // eslint-disable-next-line react/no-find-dom-node
 dom = ReactDom.findDOMNode(this)
 .querySelector(`[data-key="${pathname}"]`);
 if (dom === null) {
 // 菜单 还没有假如 导航条(横)
 } else {
 // 菜单 已经加入 导航条(横)
 dom.scrollIntoView(false);
 }
 } catch (e) {
 // console.error(e);
 }
 }
 handleClose = (tag) => {
 const { pathname } = this.props.location;
 const { history } = this.props;
 let { currentPageName } = this.state;
 const { searchMap } = this.state;
 const newRefsTag = [...this.state.refsTag.filter(t => t !== tag)];
 if (currentPageName === tag) {
 currentPageName = this.state.refsTag[this.state.refsTag.indexOf(tag) - 1];
 }
 this.setState({
 currentPageName,
 refsTag: newRefsTag,
 });
 if (pathname !== currentPageName) {
 this.notListenOnce = true;
 history.push({
 pathname: currentPageName,
 search: searchMap[currentPageName],
 });
 }
 };
 handleClickTag = (tag, e) => {
 if (e && e.target.tagName.toLowerCase() === 'i') {
 return;
 }
 if (tag !== this.state.currentPageName) {
 this.props.history.push({
 pathname: tag,
 search: this.state.searchMap[tag] ? this.state.searchMap[tag].replace(/from=[^&]+&?/, '') : undefined,
 });
 }
 }
 handleMenuClick = (e) => {
 const eKey = e.key;
 let currentPathname = this.props.location.pathname;
 const { refsTag } = this.state;
 let newRefsTag;
 if (eKey === '1') {
 newRefsTag = '/';
 currentPathname = '首页';
 } else if (eKey === '2') {
 newRefsTag = [webConfig.indexPath];
 if (currentPathname !== webConfig.indexPath) {
 newRefsTag.push(currentPathname);
 }
 } else {
 this.handleClickTag(eKey);
 return;
 }
 if (currentPathname !== this.state.currentPageName) {
 this.props.history.push({
 pathname: currentPathname,
 search: this.state.searchMap[currentPathname],
 });
 }
 this.setState({
 refsTag: newRefsTag,
 });
 }
 render() {
 const { currentPageName, refsTag } = this.state;
 const { className, style } = this.props;
 const cls = classNames(styles['router-tabs'], className);
 const tags = refsTag.map((pathname, index) => {
 const routeInfo = getTitleByPathname(pathname); // 这里假设每个pathname都能获取到指定到页面名称
 let title = routeInfo ? routeInfo.name : '404';
 const isLongTag = title.length > 30;
 const tagElem = (
 <Tag
 key={pathname}
 data-key={pathname}
 className={classNames(styles.tag,
 { [styles.active]: pathname === currentPageName })}
 onClick={e => this.handleClickTag(pathname, e)}
 closable={index !== 0}
 afterClose={() => this.handleClose(pathname)}
 >
 <span className={styles.icon} />{isLongTag ? `${title.slice(
 0, 30)}...` : title}
 </Tag>
 );
 return isLongTag
 ? <Tooltip title={title} key={`tooltip_${pathname}`}>{tagElem}</Tooltip>
 : tagElem;
 });
 this.tags = tags;
 /* eslint-disable */
 return (
 <div className={cls} style={{
 ...style,
 height: '40px',
 maxHeight: '40px',
 lineHeight: '40px',
 marginRight: '-12px',
 }}>
 <div style={{
 flex: '1',
 height: '40px',
 position: 'relative',
 overflow: 'hidden',
 background: '#f0f0f0',
 padding: '0px 0px',
 }}>
 <div style={{
 position: 'absolute',
 whiteSpace: 'nowrap',
 width: '100%',
 top: '0px',
 padding: '0px 10px 0px 10px',
 overflowX: 'auto',
 }}>
 {tags}
 </div>
 </div>
 <div style={{
 width: '96px',
 height: '100%',
 background: '#fff',
 boxShadow: '-3px 0 15px 3px rgba(0,0,0,.1)',
 }}>
 <Dropdown overlay={<Menu onClick={this.handleMenuClick}>
 <Menu.Item key="1">关闭所有</Menu.Item>
 <Menu.Item key="2">关闭其他</Menu.Item>
 <SubMenu title="切换标签">
 {
 tags.map(item => (<Menu.Item key={item.key}>{item.props.children}</Menu.Item>))
 }
 </SubMenu>
 </Menu>}
 >
 <Tag size={'small'} color="#2d8cf0"
 style={{ marginLeft: 12 }}>
 标签选项 <Icon type="down" />
 </Tag>
 </Dropdown>
 </div>
 </div>
 );
 }
}

样式定义:


.router-tabs {
 user-select: none;
 display: flex;
 position: relative;
 overflow: hidden;
 transition: all .3s;
 box-shadow: rgba(0, 0, 0, 0.4) 0 1px 1px 0, inset rgba(0, 0, 0, 0.52) 0 5px 2px;
 clear: both;
 .tag {
 margin-top: 4px;
 height: 32px;
 line-height: 32px;
 border: 1px solid #e9eaec!important;
 color: #495060!important;
 background: #fff!important;
 padding: 0 12px;
 .icon {
 display: inline-block;
 width: 12px;
 height: 12px;
 margin-right: 8px;
 border-radius: 50%;
 background: #e9eaec;
 position: relative;
 top: 1px;
 }
 &.active {
 .icon {
 background: #2d8cf0;
 }
 }
 }
 
}

使用方法

只要把组件放到页面可以实现 Tabs 页面切换到效果了

render() {
 return (
 <Layout>
 <Header className={styles.header}>
 <RouterTabs />
 </Header>
 <Content>
 <Switch>
 { routers }
 <Redirect exact from="/" to={webConfig.indexPath} />
 <Route component={NotFound} />
 </Switch>
 </Content>
 </Layout>
 );
 }

预览效果

3 回复

不做前端好多年,跟不上节凑了

但是tab之间的切换 怎么保存状态呢

使用新版antd 有个bug onClose={(e) => this.handleClose(pathname,e)} 需要在handleClose 里阻止冒泡

回到顶部

AltStyle によって変換されたページ (->オリジナル) /