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