react hook 造轮子

本文介绍了如何在GitHub的rayhomieUI项目中使用Sass的@import和Partials,以及如何通过React组件实现按钮、Alert、Menu和Tabs组件的样式与交互。此外,还涵盖了组件测试、Icon组件、CSS动画和Input组件的设计。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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.mapReact.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|block
  • jsx标签可以{…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

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值