react hook 造轮子

    科技2022-07-14  158

    GitHub地址:https://github.com/rayhomie/rayhomieUI

    一、sass的使用

    1、@import方式引入scss文件,后面必须带后缀名scss

    @import "main.scss";

    2、Partials方式引入base.scss文件,文件必须以(下划线)开头,可以不用带后缀名

    @import "_base";

    sass @import和css @import命令的区别:

    CSS @import 指令在每次调用时,都会创建一个额外的 HTTP 请求。但,Sass @import 指令将文件包含在 CSS 中,不需要额外的 HTTP 请求。

    Sass Partials:如果你不希望将一个 Sass 的代码文件编译到一个 CSS 文件,你可以在文件名的开头添加一个下划线。这将告诉 Sass 不要将其编译到 CSS 文件。(partials只能当做模块导入,不能当做css文件来编译使用。)

    例如:以下实例创建一个 _colors.scss 的文件,但是不会编译成 _colors.css 文件:

    _colors.scss 文件代码:

    $myPink: #EE82EE; $myBlue: #4169E1; $myGreen: #8FBC8F;

    如果要导入该文件,则不需要使用下划线:

    实例:

    @import "colors"; body { font-family: Helvetica, sans-serif; font-size: 18px; color: $myBlue; }

    注意:请不要将带下划线与不带下划线的同名文件放置在同一个目录下,比如,_colors.scss 和 colors.scss 不能同时存在于同一个目录下,否则带下划线的文件将会被忽略。

    二、Button组件

    使用classnames和@types/classnames包对类名进行拼接使用字符串枚举类型定义声明props,使用时也需要导入enum类型常量进行使用组件js对象中属性键名是动态变化的,需要使用[]括起来设置键名,可以用这种方法进行字符串拼接键名使用sass的@mixin和@include混入使用样式使用交叉类型,使用react提供的原生标签属性类型 import React, { useState } from 'react' import classNames from 'classnames' export enum ButtonSize { Large = 'lg', Small = 'small' } export enum ButtonType { Primary = 'primary', Default = 'default', Danger = 'danger', Link = 'link' } interface BaseButtonProps { className?: string disabled?: boolean size?: ButtonSize btnType?: ButtonType children: React.ReactNode, href?: string//link有href才是有效的 } //为了让我们自定义的组件拥有button和a标签的原生React属性 type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLElement> //原生的button属性(react提供的)和 基本自定义属性 的交叉类型 type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes<HTMLElement> //原生的a标签属性(react提供的)和 基本自定义属性 的交叉类型 export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps> //可选的 button和a标签 的交叉类型 const Button: React.FC<ButtonProps> = (props) => { //使用rest运算符把多传入的props取出来 const { disabled, className, size, btnType, children, href, ...restProps } = props //需要安装classnames和@types/classnames包,对className进行拼接 const classes = classNames('btn', className, { [`btn-${btnType}`]: btnType,//后面的值返回true加上类名,false不加 [`btn-${size}`]: size, 'disabled': (btnType === ButtonType['Link']) && disabled //如果是传入的props.btnTpye是Link类型,则加上一个disabled类名 }) if (btnType === ButtonType['Link'] && href) {//如果是link类型 return ( <a className={classes} href={href} {...restProps}//把剩余的props全部传入 > {children} </a> ) } else {//button类型 return ( <button className={classes} disabled={disabled} {...restProps}//把剩余的props全部传入 > {children} </button> ) } } Button.defaultProps = { disabled: false, btnType: ButtonType.Default, } export default Button

    外部使用组件:

    import React from 'react'; import './styles/index.scss'; import Button, { ButtonSize, ButtonType } from './components/Button/index' //使用组件时也需要导入*字符串枚举*来设置相应props的值,正常使用组件 function App() { return ( <Button btnType={ButtonType.Danger} size={ButtonSize.Small} > 按钮 </Button> ); } export default App;

    sass混入@mixin的使用:

    使用@mixin和@include来重用重复的css代码

    //定义mixin @mixin button-size($padding-y, $padding-x, $font-size, $border-raduis) { padding: $padding-y $padding-x; font-size: $font-size; border-radius: $border-raduis; }

    @include使用mixin

    @include button-size( $btn-padding-y, $btn-padding-x, $btn-font-size, $border-radius);

    测试用例:

    <Button btnType={ButtonType.Default} size={ButtonSize.Small} > Default </Button> <Button btnType={ButtonType.Primary} size={ButtonSize.Small} > Primary </Button> <Button btnType={ButtonType.Primary} size={ButtonSize.Large} > Large Primary </Button> <Button btnType={ButtonType.Danger} size={ButtonSize.Small} > Danger </Button> <Button btnType={ButtonType.Default} size={ButtonSize.Small} disabled > disabled </Button> <Button btnType={ButtonType.Link} size={ButtonSize.Small} href='http://www.baidu.com/' > baidu Link </Button> <Button btnType={ButtonType.Link} size={ButtonSize.Small} href='http://www.baidu.com/' disabled > disabled Link </Button>

    三、Alert组件

    使用react-transition-group编写动画过渡效果使用ts类型断言,对传入的可选props函数进行执行 import React, { useState } from 'react' import classNames from 'classnames' import { CSSTransition } from 'react-transition-group'; export enum AlertType { Default = 'default', Success = 'success', Danger = 'danger', Warning = 'warning', } interface BaseAlertProps { className?: string alertType?: AlertType description?: string//描述 title: string//标题 closable?: boolean//是否显示关闭图标 onClose?: () => void//关闭alert时触发的事件 visible: boolean//显示状态 } const Alert: React.FC<BaseAlertProps> = (props) => { const { className, alertType, title, description, closable, onClose, visible } = props const classes = classNames('alt', className, { [`alt-${alertType}`]: alertType, }) const closeIconClasses = classNames({ 'alt-close': closable//true就显示类名,false类名为null,执行alt-close-none }) const onclose = onClose as () => void //类型断言 return (<> <CSSTransition in={visible}//为true进入显示组件(主要通过in属性来控制组件状态) classNames="card"//设置类名的前缀 timeout={400}//设置过渡动画事件 unmountOnExit={true}//消失动画结束后 + display:none > <div className={classes} > <span className='alt-title'>{title}</span> <p className='alt-description'>{description}</p> <span className={closeIconClasses || 'alt-close-none'} onClick={() => { onclose() }}>关闭</span> </div> </CSSTransition> </>) } Alert.defaultProps = { closable: true, alertType: AlertType.Default, onClose: () => { } } export default Alert

    css的编写:

    .card-enter, .card-appear { opacity: 0; transform: scale(.8); } .card-enter-active, .card-appear-active { opacity: 1; transform: scale(1); transition: opacity 300ms, transform 300ms; } .card-exit { opacity: 1; } .card-exit-active { opacity: 0; transform: scale(.8); transition: opacity 300ms, transform 300ms; } .alt { position: relative; padding: 0.75rem 1.25rem; margin-bottom: 1rem; border: 1px solid transparent; border-radius: 0.25rem; } .alt-default { color: #fff; background: #0d6efd; border-color: #0262ef; } .alt-success { color: #fff; background: #52c41a; border-color: #49ad17; } .alt-danger { color: #fff; background: #dc3545; border-color: #d32535; } .alt-warning { color: #fff; background: #fadb14; border-color: #efd005; } .alt-title {} .alt-description { font-size: 0.875rem; margin: 0.3rem 0 0; } .alt-close { position: absolute; top: 0; right: 0; padding: 0.75rem 1.25rem; color: inherit; cursor: pointer; } .alt-close-none { display: none; }

    测试用例:

    const [state, setState] = useState(false); <button onClick={() => { setState(!state) }}>显示</button> <Alert alertType={AlertType.Default} title='Default' description='hhh' onClose={() => { setState(!state) }} visible={state}></Alert> <Alert alertType={AlertType.Success} title='Success' visible></Alert> <Alert alertType={AlertType.Danger} title='Danger' visible></Alert> <Alert alertType={AlertType.Warning} title='Warning' closable={false} visible></Alert>

    四、组件测试

    Jest通用测试框架:断言库,Common Matchers

    React专用测试工具:

    ①React Testing Library☆

    对组件编写测试用例,就像终端用户在使用它一样方便。

    ②Airbnb推出的Enzyme

    对react组件的输出进行断言、操控、遍历等。(类似于jquery的链式操作)

    使用@testing-library/react进行组件测试:

    //button.test.tsx import React from 'react'; import { render } from '@testing-library/react';//使用测试框架render import Button from './index';//导入测试组件 test('our first react test case', () => { const wrapper = render(<Button>Nice</Button>) const element = wrapper.queryByText('Nice') expect(element).toBeTruthy() }) //在终端中输入npm run test进行测试

    使用@testing-library/jest-dom进行dom断言测试:

    1、在src下约定setupTests.ts文件中进行引入工具包

    // jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom/extend-expect';

    2、创建对应的单元测试xxx.test.tsx文件,就可以使用一些dom的断言进行测试

    //button.test.tsx import React from 'react'; import { render } from '@testing-library/react';//使用测试框架render import Button from './index';//导入测试组件 test('our first react test case', () => { const wrapper = render(<Button>Nice</Button>) const element = wrapper.queryByText('Nice') expect(element).toBeTruthy() }) //在终端中输入npm run test进行测试 import React from 'react'; import { render } from '@testing-library/react';//使用测试框架render import Button from './index';//导入测试组件 describe('test Button component', () => { it('should render the correct default button', () => { const wrapper = render(<Button>Nice</Button>) const element = wrapper.getByText('Nice') expect(element).toBeInTheDocument() expect(element.tagName).toEqual('BUTTON') expect(element).toHaveClass('btn btn-default') }) it('should render the correct component based on different props', () => { const wrapper = render(<Button>hhh</Button>) const element = wrapper.getByText('hhh') expect(element).toBeInTheDocument() expect(element.tagName).toEqual('BUTTON') expect(element).toHaveProperty('disabled') }) })

    五、Menu组件

    两种方案:

    //1.不完美的解决方案 const items = [ {disabled:false,element :(<a>title</a>)}, {disabled:true,element :'disabled link'} ]; <Menu defaultIndex={0} items={items} onSelect={} mode='vertical'> </Menu> //2.更加语义化的解决方案,贴近于html <Menu defaultIndex={0} onSelect={} mode='vertical'> <Menu.Item><a>title</a></Menu.Item> <Menu.Item disabled>disabled link</Menu.Item> </Menu> 使用context进行组件间传值React.Children API 遍历传入的子节点进行优化使用组件displayName进行调试优化

    组件Menu:

    import React, { useState, createContext } from 'react' import classNames from 'classnames'; type MenuMode = 'horizontal' | 'vertical' type selectCallback = (selectedIndex: number) => void interface MenuProps {//定义组件props类型 defaultIndex?: number//默认被选中的索引值(默认0) mode?: MenuMode//横向|纵向(默认横向) onSelect?: selectCallback//点击选择之后的触发的函数 className?: string//用户自定义的传入的class style?: React.CSSProperties//用户自定义组件的style传递给ul } interface MenuContext {//定义context传递类型,子父组件间传值 index: number onSelect?: selectCallback } //导出创建的context供子组件使用且提供默认值 export const MenuContext = createContext<MenuContext>({ index: 0 }) const Menu: React.FC<MenuProps> = (props) => { const { defaultIndex, mode, children, className, style, onSelect } = props const [Active, setActive] = useState(defaultIndex)//由父组件进行所有状态的维护 const classes = classNames('menu', className, { 'menu-vertical': mode === 'vertical' }) const handleClick = (index: number) => { setActive(index)//维护状态改变 if (onSelect) onSelect(index)//执行用户自定义传入的方法 } //初始化需要共享的状态和修改的方法 const passedContext: MenuContext = { index: Active || 0,//将状态共享 onSelect: handleClick//将函数共享 } //使用context所有的状态都由父组件进行控制 return ( <ul className={classes} style={style}> <MenuContext.Provider value={passedContext}>{/*提供者*/} {children} </MenuContext.Provider> </ul> ) } Menu.defaultProps = { defaultIndex: 0, mode: 'horizontal' } export default Menu

    子组件MenuItem:

    import React, { useContext } from 'react' import classNames from 'classnames'; import { MenuContext } from './index' interface MenuItemProps { index: number//每个item不用的索引值 disabled?: boolean//是否可用 className?: string style?: React.CSSProperties } const MenuItem: React.FC<MenuItemProps> = (props) => { const { index, disabled, className, style, children } = props; const context = useContext(MenuContext)//使用共享的context const classes = classNames('menu-item', className, { 'is-disabled': disabled, 'is-active': context.index === index }) const handleClick = () => { //点击li触发onSelect方法并传递相应index给父组件 if (context.onSelect && !disabled) context.onSelect(index) } return ( <li className={classes} style={style} onClick={handleClick}> {children} </li> ) } MenuItem.displayName = 'MenuItem' export default MenuItem
    测试用例:
    <Menu defaultIndex={0} onSelect={(i) => alert(i)} mode='vertical'> <MenuItem index={0}>cool link1</MenuItem> <MenuItem index={1}>cool link2</MenuItem> <MenuItem index={2}>cool link3</MenuItem> <MenuItem index={3} disabled>cool link4</MenuItem> </Menu> <Menu defaultIndex={0} onSelect={(i) => alert(i)}> <MenuItem index={0}>cool link1</MenuItem> <MenuItem index={1}>cool link2</MenuItem> <MenuItem index={2}>cool link3</MenuItem> <MenuItem index={3} disabled>cool link4</MenuItem> </Menu>

    组件优化:

    <Menu>子节点只能使用<MenuItem>子节点<MenuItem>的index属性可选且不选时默认值为顺序索引(0,1,2…n)

    注意:在传入的props的children直接使用数组map方法是非常危险的事情(因为props属性的数据结构不透明,未知)

    react提供了两个方法去循环children:①React.Children.map②React.Children.forEach

    const Menu: React.FC<MenuProps> = (props) => { const renderChildren = () => { return React.Children.map(children, (child, index) => { const childElement = child as React.FunctionComponentElement<MenuItemProps>//类型断言 const { displayName } = childElement.type//取出child的displayName if (displayName === 'MenuItem') {//取出每个child节点的displayName和MenuItem作对比 return React.cloneElement(childElement, { index })//如果是MenuItem就拷贝该子节点再添加index属性并输出 } else {//如果标签不是MenuItem就报错,不输出该child值 console.error('Warning: Menu has a child which is not a MenuItem component') } }) } return ( <ul className={classes} style={style}> <MenuContext.Provider value={passedContext}> {renderChildren()} </MenuContext.Provider> </ul> ) }

    我们还需要在遍历children时给子组件child自动顺序添加index属性,所以使用React.cloneElement API来克隆元素的同时添加属性

    测试用例:
    <Menu defaultIndex={0} onSelect={(i) => alert(i)} mode='vertical'> <MenuItem>cool link1</MenuItem> <MenuItem>cool link2</MenuItem> <MenuItem>cool link3</MenuItem> <MenuItem disabled>cool link4</MenuItem> <li>1111</li> </Menu>

    子组件SubMenu:

    (用于下拉菜单)

    防抖来控制动画流畅类名不同来控制display:none|blockjsx标签可以{…xxx}来设置属性值 import React, { useContext, useState, FunctionComponentElement } from 'react' import classNames from 'classnames' import { MenuContext } from './index' import { MenuItemProps } from './MenuItem' export interface SubMenuProps { index?: number title: string className?: string } const SubMenu: React.FC<SubMenuProps> = (props) => { const [open, setOpen] = useState(false)//控制开关 const { index, title, className, children } = props const context = useContext(MenuContext) const classes = classNames('menu-item submenu-item', className, { 'is-active': context.index === index }) const handleClick = (e: React.MouseEvent) => {//纵向时点击控制 e.preventDefault() setOpen(!open) } let timer: any//开闭更圆滑,防抖 const handleMouse = (e: React.MouseEvent, toggle: boolean) => {//横向时hover控制 clearTimeout(timer) e.preventDefault() timer = setTimeout(() => { setOpen(toggle) }, 300) } const clickEvents = context.mode === 'vertical' ? {//纵向时点击控制 onClick: handleClick } : {} const hoverEvents = context.mode !== 'vertical' ? {//横向时hover控制 onMouseEnter: (e: React.MouseEvent) => { handleMouse(e, true) }, onMouseLeave: (e: React.MouseEvent) => { handleMouse(e, false) } } : {} const renderChildren = () => { const subMenuClasses = classNames('viking-submenu', { 'menu-opened': open//通过display:none|block来控制 }) const childrenComponent = React.Children.map(children, (child, index) => { const childElement = child as React.FunctionComponentElement<MenuItemProps>//类型断言 if (childElement.type.displayName === 'MenuItem') {//SubMenu的子节点只能是MenuItem return childElement } else { console.error('Warning: Menu has a child which is not a MenuItem component') } }) return ( <ul className={subMenuClasses}> {childrenComponent} </ul> ) } return ( <li key={index} className={classes} {...hoverEvents}> <div className="submenu-title" {...clickEvents}>{title}</div> {renderChildren()} </li> ) } SubMenu.displayName = 'SubMenu' export default SubMenu
    测试用例:
    //纵向的menu <Menu defaultIndex={0} onSelect={(i) => alert(i)} mode='vertical'> <MenuItem>cool link1</MenuItem> <MenuItem>cool link2</MenuItem> <SubMenu title='cool link3'> <MenuItem>cool link3.1</MenuItem> </SubMenu> <MenuItem disabled>cool link4</MenuItem> <li>1111</li> </Menu> //横向的menu <Menu defaultIndex={0} onSelect={(i) => alert(i)}> <MenuItem>cool link1</MenuItem> <MenuItem>cool link2</MenuItem> <SubMenu title='cool link3'> <MenuItem>cool link3.1</MenuItem> </SubMenu> <MenuItem disabled>cool link4</MenuItem> </Menu>

    SubMenu的index问题:

    因为Menu组件分成了上层组件构成外层Menu、中层SubMenu、内存MenuItem。

    我们还需要把index传递给所以的内层组件,把每个内层组件给index排序。

    index不使用number,而使用字符串:以"n-n"的形式表示

    六、Tabs组件

    和Menu组件差不多的实现,注意修改一下css,效果如下:

    七、Icon组件

    fontawesome

    react-fontawesome

    npm i --save @fortawesome/fontawesome-svg-core \ @fortawesome/free-solid-svg-icons \ @fortawesome/react-fontawesome

    用例:

    //方式一:导入对象变量的形式引入 import { faCoffee } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; <FontAwesomeIcon icon={faCoffee} size='6x' rotation={180} spin border /> <FontAwesomeIcon icon={faCoffee} size='6x' rotation={180} pulse border /> //spin旋转动画、pulse像脉搏一样跳动、rotation旋转度数、border加上边框...等等

    //方式二:字符串的形式引入 import { fas } from '@fortawesome/free-solid-svg-icons';//fas是导入全部图标 import { library } from '@fortawesome/fontawesome-svg-core'; library.add(fas);//library进行管理,fas是全部图标 <FontAwesomeIcon icon='coffee' size='6x' rotation={180} pulse border /> <FontAwesomeIcon icon='arrow-down' size='lg' rotation={180} border />

    二次封装react-rontawesome组件

    //对react-fontawesome库进行二层封装 import React from 'react' import classNames from 'classnames' import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome' //定义自定义主题颜色 export type ThemeProps = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger' interface IconProps extends FontAwesomeIconProps {//继承react-fontawesome库暴露出来组件props theme?: ThemeProps className?: string } const Icon: React.FC<IconProps> = (props) => { const { className, theme, ...restProps } = props const classes = classNames('icon', className, { [`icon-${theme}`]: theme, }) return ( <FontAwesomeIcon className={classes} {...restProps} /> ) } export default Icon

    使用sass的@each方法进行循环变量,写样式:

    //示例: $sizes: 40px, 50px, 80px; //循环遍历$sizes变量 @each $size in $sizes { .icon-#{$size} { font-size: $size; height: $size; width: $size; } }

    设置自定义类样式:

    //Icon_style.scss $theme-colors: ("primary": $primary, "secondary": $secondary, "success": $success, "info": $info, "warning": $warning, "danger": $danger, "light": $light, "dark": $dark); @each $key, $val in $theme-colors { .icon-#{$key} { color: $val; } }

    测试用例:

    <Icon icon='arrow-down' size='6x' rotation={180} border theme='success' /> <Icon icon='arrow-down' size='6x' rotation={180} border theme='danger' />

    使用react-transition-group写动效

    CSSTransition组件设置*号对应的类名,然后按照以下方式进行书写动效样式:

    自定义Transition组件编码

    用于复用动画效果

    //由之前的CSSTransition组件 <CSSTransition in={state} timeout={300} classNames='card' appear //appear生效 unmoutOnExit //动画结束时display:none > {node} </CSSTransition> // 自定义复用之后 <Transition in={state} timeout={300} animation='zoom-in-top'//字符串字面量,自定义预设的动画 > {node} </Transition>
    写一个Transition自定义组件包裹CSSTransition组件
    import React from 'react' import { CSSTransition } from 'react-transition-group'; import { CSSTransitionProps } from 'react-transition-group/CSSTransition'; type AnimationName = 'zoom-in-top' | 'zoom-in-left' | 'zoom-in-bottom' type TransitionProps = CSSTransitionProps & {//继承CSSTransition的属性 animation?: AnimationName//新增加一个字面量属性值 } const Transition: React.FC<TransitionProps> = (props) => { const { children, classNames, animation, ...restProps } = props return ( <CSSTransition //如果传入了classNames属性,就使用classNames属性,不使用自定义的animation classNames={classNames ? classNames : animation} {...restProps}//把剩余的props全部传入 > { children} </CSSTransition > ) } Transition.defaultProps = {//默认props unmountOnExit: true, appear: true } export default Transition

    测试用例:我们用之前写的Alert组件测试

    //这是之前的用法 <CSSTransition in={visible}//为true进入显示组件(主要通过in属性来控制组件状态) classNames="card"//设置类名的前缀 timeout={400}//设置过渡动画事件 unmountOnExit={true}//消失动画结束后 + display:none > <div className={classes} > <span className='alt-title'>{title}</span> <p className='alt-description'>{description}</p> <span className={closeIconClasses || 'alt-close-none'} onClick={() => { onclose() }}>关闭</span> </div> </CSSTransition>

    这是使用自定义Transition组件进行优化

    <Transition in={visible}//为true进入显示组件(主要通过in属性来控制组件状态) animation='zoom-in-left'//使用我们自定义的animation timeout={400}//设置过渡动画事件 > <div className={classes} > <span className='alt-title'>{title}</span> <p className='alt-description'>{description}</p> <span className={closeIconClasses || 'alt-close-none'} onClick={() => { onclose() }}>关闭</span> </div> </Transition>

    八、Input组件

    设计Input组件需要设置的属性

    <Input disabled size='lg|sm' icon='fontawesome 支持的图标' prepand='前缀 string|ReactElement' append='后缀 string|ReactElement' {...restProps}//支持其他所有的 HTMLInput 属性 />

    九、Pagination组件

    import React, { useState, useEffect, useRef, useMemo } from 'react' import classnames from 'classnames' import Transition from '../Transition/Transition' interface PaginationProps { className?: string style?: React.CSSProperties pageSize: number//每页大小 current?: number//指针 total: number//总条数 disabled?: boolean//是否禁用 showQuickJumper?: boolean//是否用快速跳转输入框 onChange?: (next: number) => void//页码变化时回调 } const Pagination: React.FC<PaginationProps> = (props) => { const { className, style, pageSize, current, total, disabled, onChange, showQuickJumper } = props const [cur, setCur] = useState(current) //当前的页码状态 const [jumperTopic, setJumperTopic] = useState(false) //focus控制显示(输入后回车框) useEffect(() => { //当cur变化后实时获取到cur的值 if (onChange && cur) { onChange(cur) //执行传入的回调 } }, [cur]) //获取页数 const getPageNum = (total: number, pageSize: number): number => { return Math.ceil(total / pageSize) } //useMemo缓存优化获取页数 const pageNum = useMemo(() => getPageNum(total, pageSize), [total, pageSize]) const generateList = (pageNum: number) => { if (cur) //cur可能为undefined,默认值为1 return new Array(pageNum).fill('').map((item, index) => { //长度和填充页数相等的数组 return (<> <div className={classnames('item', { 'active': cur === index + 1,//点击态 'hidden': pageNum > 5 && cur <= 3 && index + 1 > 5 //点击1、2、3时大于5的页码都隐藏显示 || pageNum > 5 && cur >= pageNum - 2 && index + 1 < pageNum - 4 //点击倒数1,2,3时,小于倒数第四的页码隐藏 || pageNum > 5 && cur < pageNum - 2 && cur > 3 && (index + 1 > cur + 2 || index + 1 < cur - 2), //点击4~n-3时,显示cur附近的(一共五个) 'show': pageNum > 5 && (index + 1 === pageNum || index + 1 === 1), //一头一尾总是显示 disabled, 'active-disabled': cur === index + 1 && disabled })} key={index} onClick={() => { setCur(index + 1) }} > {index + 1} </div> <div className={classnames('item', { //控制...的显示和不显示 'hidden': pageNum > 0, 'show': pageNum > 5 && cur > 4 && index + 1 === 1 || pageNum > 5 && cur < pageNum - 3 && index + 1 === pageNum - 1, disabled })} onClick={() => { if (pageNum > 5 && cur > 4 && index + 1 === 1) { if (cur === 5) { //解决bug最前面的...(当cur为5时点击...变成1才对) setCur(cur - 4) } else { setCur(cur - 5) } } if (pageNum > 5 && cur < pageNum - 3 && index + 1 === pageNum - 1) { if (cur === pageNum - 4) { //当cur为n-4时点击...变成n才对 setCur(cur + 4) } else { setCur(cur + 5) } } }} > ... </div> </> ) }) } const handlePrev = () => { if (cur && cur > 1) { setCur(cur - 1) } } const handleNext = () => { if (cur && cur < pageNum) { setCur(cur + 1) } } const inputRef = useRef<HTMLInputElement>(document.createElement("input")) return ( <div className={classnames('generateList', className, { disabled })} style={style} > <div className={classnames('item', { disabled })} onClick={handlePrev} > {'<'} </div> {generateList(pageNum)} <div className={classnames('item', { disabled })} onClick={handleNext} > {'>'} </div> {showQuickJumper ? <div style={{ marginLeft: '20px' }} id='jump'> <div className='main-jumperTopic'>跳至 <input className={classnames('quickJumper', { disabled })} type="text" ref={inputRef}//ref保存当前Input节点 onChange={(e) => { inputRef.current.value = e.target.value }} onKeyDown={(e) => { if (e.keyCode === 13) {//确认的时候跳转 const value = Number(inputRef.current.value) if (value > 0 && value <= pageNum) { setCur(value) } inputRef.current.value = '' }; }} onFocus={() => { setJumperTopic(true) }} onBlur={() => { setJumperTopic(false) }} /> <Transition in={jumperTopic}//控制动画 animation='zoom-in-bottom' timeout={300} className='Topic' > <div>输入后回车</div> </Transition> 页 </div> </div> : <></>} </div> ) } Pagination.defaultProps = { current: 1 } export default Pagination
    测试用例:
    import React, { useState, useEffect } from 'react' import Pagination from './components/Pagination/Pagination'; interface Props { } const MOCK_DATA = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92] const PAGE_SIZE = 10 const searchPage = (current: number, pageSize: number, sourceData: any[]) => { return sourceData.slice(pageSize * (current - 1), pageSize * current) } const PaginationTest: React.FC<Props> = (props) => { useEffect(() => { const init = searchPage(1, PAGE_SIZE, MOCK_DATA) setData(init) }, []) const [data, setData] = useState<any[]>([]) return (<> <Pagination total={MOCK_DATA.length} pageSize={PAGE_SIZE} className='hhh' showQuickJumper onChange={(p) => { setData(searchPage(p, PAGE_SIZE, MOCK_DATA)); }} /> <div>{data.map((i) => <div>{i}</div>)}</div> </>) } export default PaginationTest

    Processed: 0.010, SQL: 8