usePasswordStrength

用于检测用户的密码强度, 并且可以用于表单验证

import { useState } from 'react'

// 明确指定 hooks 返回的类型
type PasswordStrength = {
  percent: number;
  strokeColor: string;
  showInfo: boolean;
}

type CheckPasswordStrength = (value: string) => void

type validatePasswordStrength = (rule: any, value: string, callback: any) => Promise<any>

/*
* @description: 用于检验密码强度的 hooks, 包含检验规则函数
* @param: username 用户名, 用于检验密码是否包含用户名的一部分
* @return: [passwordStrength, checkPasswordStrength, validatePasswordStrength]
* @example: const [passwordStrength, checkPasswordStrength, validatePasswordStrength] = usePasswordStrength()
* @example:
*   <Form.Item
*    name="password"
*   label="Password"
*   rules={[
*    {
*     required: true,
*     validator: validatePasswordStrength
*   }>
*   <Input.Password />
*   </Form.Item>
*  */
const usePasswordStrength = (username?: string): [PasswordStrength, CheckPasswordStrength, validatePasswordStrength] => {
  const [passwordStrength, setPasswordStrength] = useState<PasswordStrength>({
    percent: 0,
    strokeColor: '#f5222d',
    showInfo: false
  })
  
  // 检测密码强度 value: 密码值
  const checkPasswordStrength: CheckPasswordStrength = (value: string) => {
    let percent = 0
    let strokeColor = '#f5222d'
    const showInfo = false
    
    const hasDigit = /[0-9]/.test(value)
    const hasLetter = /[a-zA-Z]/.test(value)
    const hasSpecialChar = /[!@#$%^&*()_+{}[\]:;<>,.?~\\/\-=|\\'\\"]/g.test(value)
    
    if (value.length >= 6) {
      let strength = 0
      if (hasDigit) strength += 1
      if (hasLetter) strength += 1
      if (hasSpecialChar) strength += 1
      if (value.length >= 10) strength += 1
      
      percent = (strength / 4) * 100
      
      // 根据密码强度设置颜色
      if (percent <= 25) {
        strokeColor = '#f5222d'
      }
      if (percent <= 50 && percent > 25) {
        strokeColor = '#e06c6e'
      }
      if (percent > 50 && percent < 80) {
        strokeColor = '#faad14'
      }
      if (percent >= 80) {
        strokeColor = '#52c41a'
      }
    }
    
    setPasswordStrength({
      percent,
      strokeColor,
      showInfo
    })
  }
  
  // 检验密码强度的函数
  const validatePasswordStrength = (rule: any, value: string, callback: any) => {
    // 不能有空格
    if (value.indexOf(' ') !== -1) return Promise.reject('Password cannot contain spaces!')
    // 至少6位
    if (value.length < 6) return Promise.reject('Password must be at least 6 characters!')
    // 至少包含一个字母 不分大小写
    if (!/[a-zA-Z]/.test(value)) return Promise.reject('Password must contain at least one letter!')
    // 至少包含一个特殊字符
    if (!/[^a-zA-Z0-9]/.test(value)) return Promise.reject('Password must contain at least one special character!')
    // 不包含用户名的一部分, 即密码不能含有用户的姓和名中的一部分
    const nameArr = username?.split(' ') || []
    const isExist = nameArr.some((name) => {
      return value.toLowerCase().indexOf(name.toLowerCase()) !== -1
    })
    if (isExist) return Promise.reject('Password cannot contain part of your name!')
    return Promise.resolve()
  }
  
  return [passwordStrength, checkPasswordStrength, validatePasswordStrength]
}

export default usePasswordStrength

用法

// 先获取 hooks 返回的三个值
const [passwordStrength, checkPasswordStrength, validatePasswordStrength] = usePasswordStrength()

// 密码强度检测 当用户输入密码时, 调用 checkPasswordStrength 函数
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  checkPasswordStrength(e.target.value.trim())
}

// From表单
<Form.Item
  name="newPassword"
  label="Change Password"
  rules={[
    {
      required: true,
      validator: validatePasswordStrength
    }
  ]}
>
  <Input.Password onChange={handlePasswordChange} visibilityToggle={true} placeholder="***********" />
</Form.Item>

// 可配合antd的Progress组件使用
<Progress
  size="small"
  percent={passwordStrength.percent}
  strokeColor={passwordStrength.strokeColor}
  showInfo={passwordStrength.showInfo}
/>

useFetchData

用于请求数据, 并且可以用于表格的分页, 排序, 过滤等功能

import { useState, useEffect } from 'react'
import type { BaseResponse, PaginationData } from '@/api/type'
import { message, PaginationProps, TablePaginationConfig } from 'antd'


interface IResponse<T> extends BaseResponse {
  data: T;
  pagination: PaginationData
}

/*
* @description: 用于请求数据的hooks, 功能包括请求的数据, 请求状态, 请求错误, 倒计时, 分页
* @param {Function} apiFun 请求数据的方法
* @param {Object} params 请求数据的参数
* @return {Object} data 请求的数据
* @return {Boolean} loading 请求状态
* @return {Object} error 请求错误
* @return {Function} fetchData 请求数据的方法
* @return {Number} status 请求状态码
* @return {Number} countDown 倒计时
* @return {Object} pagination 分页
* @return {Function} handleTableChange 分页, 排序, 过滤
* @example
* const { data, loading, status, countDown, fetchData, error } = useFetchData(apiFun, params)
* */
export default function useFetchData<TDATA, TParams>(apiFun?: (params?: TParams | any) => Promise<any>, ...rest: any[]) {
  const [data, setData] = useState<TDATA | any>()
  const [status, setStatus] = useState<number>()
  const [loading, setLoading] = useState<boolean>(false)
  const [error, setError] = useState<any>(null)
  // 倒计时
  const [countDown, setCountDown] = useState<number>(0)
  const [pagination, setPagination] = useState<PaginationProps>()
  const [request, setRequest] = useState<any>()
  
  // 请求数据
  useEffect(() => {
    if (apiFun) {
      fetchData(apiFun, ...rest).then()
    }
  }, [apiFun])
  
  /*
  * @description: 请求数据的方法
  * @param {Function} apiFun 请求数据的方法, 通常被封装在api文件中
  * @param {String} msg [''] 请求成功后的消息提示
  * @param {Number} count [0] 倒计时
  * @param {Boolean} loadingWithCount [true] 是否在倒计时时显示loading
  * */
  const fetchData = async <TDATA, TParams>(apiFun: any, params?: TParams, msg?: string, count?: number, loadingWithCount = true) => {
    if (!apiFun) {
      return
    }
    setLoading(true)
    setRequest(apiFun)
    apiFun(params).then((res: IResponse<TDATA>) => {
      if (res.code === 200) {
        setData(res.data)
        setStatus(res.code)
        // 消息提示
        if (msg) {
          message.success(msg).then()
        }
        // 倒计时
        if (count) {
          setCountDown(count)
          const timer = setInterval(() => {
            setCountDown((prev) => {
              if (prev <= 1) {
                clearInterval(timer)
                setLoading(false)
                return 0
              }
              return prev - 1
            })
          }, 1000)
        }
        // 分页
        if (res?.pagination) {
          setPagination({
            current: res.pagination.current,
            pageSize: res.pagination.pageSize,
            total: res.pagination.total
          })
        }
      } else {
        if (res.msg) {
          message.error(res.msg).then()
        }
      }
    }).catch((err: any) => {
      setError(err)
      message.error(err.message || 'request failed').then()
    }).finally(() => {
      // 如果不需要倒计时时显示loading
      if (!loadingWithCount) {
        setLoading(false)
      }
    })
  }
  
  // 分页
  const handleTableChange = (pagination: TablePaginationConfig, filters: any) => {
    const { current, pageSize } = pagination
    const reqParams = {
      ...rest,
      current,
      pageSize
    }
    fetchData(request, reqParams).then()
  }
  
  return { data, loading, status, countDown, fetchData, pagination, handleTableChange, error, setPagination }
}

useHistoryPagination

import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'

interface PageState {
  current: number
  pageSize: number
  scrollTop: number
}

/*
* @description: 用于保存当前页码 页数 以及 滚动条位置
* 当useHistoryPagination的setCurrent和setPageSize方法不为空时 会自动监听浏览器的前进后退事件, 并恢复页码和页数,否则不会监听浏览器的前进后退事件
* @param {setCurrent} 设置当前页码的方法
* @param {setPageSize} 设置当前页数的方法
* @return {saveHistory} 保存当前页码 页数 以及 滚动条位置 当前页面使用
* @return {getHistory} 获取当前页码 页数 以及 滚动条位置 当前页面使用
* @return {savePageState} 当页面跳转时 保存当前页码 页数 以及 滚动条位置 涉及到不同的页面跳转时使用
* @return {getPageState} 获取保存的页码 页数 以及 滚动条位置 涉及到不同的页面跳转时使用
* @return {restorePageState} 恢复页码和页数
* @example
   const { saveHistory, getHistory, savePageState, getPageState, restorePageState } = useHistoryPagination(setCurrent, setPageSize)
   
   // 当分页器页数 或者 页码改变时, 如果只是在当前页面进行分页器数据的切换 直接saveHistory(page, pageSize)即可 当浏览器前进后退时自动生效
   const handlePageChange = (page: number, pageSize: number) => {
    // 保存当前页码 页数 以及 滚动条位置
    saveHistory(page, pageSize)
    // set current page
    setCurrent(page)
    setPageSize(pageSize)
   }
  
   
   // 如果需要跳转到其他页面,返回当前页面需要保存当前页码 页数 以及 滚动条位置时,需要使用savePageState(page, pageSize)方法
   const goDetail = () => {
    // 保存当前页面的状态
    savePageState(current, pageSize)
    navigate(`/xxx`)
  }
  
  // 跳转到其它页面又返回时,读取缓存的页面状态
  useEffect(() => {
    const pageState = getPageState()
    if (Object.keys(pageState).length > 0) {
      setTimeout(() => {
        restorePageState(pageState)
      }, 1000)
    }
  }, [location.pathname])
* */

export default function useHistoryPagination(setCurrent?: (num: number) => void, setPageSize?: (num: number) => void) {
  const location = useLocation()
  
  useEffect(() => {
    if (!setCurrent || !setPageSize) return
    window.addEventListener('popstate', () => {
      const { current, pageSize, scrollTop } = getHistory()
      if (current && pageSize) {
        restorePageState(getHistory())
      }
    })
    
    return () => {
      window.removeEventListener('popstate', () => {
      })
    }
  }, [location.pathname])
  
  // 恢复页码和页数 [setPagination是为了当分页器数据定义不为setCurrent和setPageSize时使用, 但是此时需要手动设置window.addEventListener('popstate')事件]
  const restorePageState = (pageState: PageState, setPagination?: (preState: any) => void) => {
    if (!setCurrent || !setPageSize || !pageState.current || !pageState.pageSize) return
    if (!setPagination) {
      // 恢复页码和页数
      setCurrent(pageState.current)
      setPageSize(pageState.pageSize)
      // 恢复滚动条位置
      document.documentElement.scrollTop = pageState.scrollTop
    } else {
      setPagination((preState: PaginationProps) => {
        return {
          ...preState,
          current: pageState.current,
          pageSize: pageState.pageSize
        }
      })
    }
  }
  
  // 当页面跳转时 保存当前页码 页数 以及 滚动条位置 涉及到不同的页面跳转时使用
  const savePageState = (current: number, pageSize = 6) => {
    sessionStorage.setItem('pageState', JSON.stringify({
      current,
      pageSize,
      scrollTop: document.documentElement.scrollTop
    }))
  }
  
  // 获取保存的页码 页数 以及 滚动条位置 涉及到不同的页面跳转时使用
  const getPageState = () => {
    const pageState = JSON.parse(sessionStorage.getItem('pageState') || '{}')
    sessionStorage.removeItem('pageState')
    return pageState
  }
  
  // 保存当前页码 页数 以及 滚动条位置 当前页面使用
  const saveHistory = (current: number, pageSize = 6) => {
    history.pushState({ current, pageSize, scrollTop: document.documentElement.scrollTop }, '')
  }
  
  // 获取当前页码 页数 以及 滚动条位置 当前页面使用
  const getHistory = () => {
    const { current, pageSize, scrollTop } = history.state
    return { current, pageSize, scrollTop }
  }
  
  return { saveHistory, getHistory, savePageState, getPageState, restorePageState }
}

用法1 当前页面使用

const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(12)

// 浏览器后退时,读取缓存的页面状态, 返回上次翻页的状态
const { saveHistory, getPageState, restorePageState } = useHistoryPagination(setCurrent, setPageSize)

const handlePageChange = (page: number, pageSize: number) => {
  // 保存页面分页器数据saveHistory调用此方法即可 当浏览器前进后退时自动生效(前提是setCurrent和setPageSize不为空)
  saveHistory(page, pageSize)
  setCurrent(page)
  setPageSize(pageSize)
}

// 分页器
<Pagination
  current={current}
  total={total}
  pageSize={pageSize}
  showTotal={total => `Total ${total} product`}
  pageSizeOptions={[12, 16, 24, 32, 64]}
  showSizeChanger={true}
  onChange={handlePageChange}
/>

用法2 涉及页面跳转

这里只展示在不同的组件中使用, 思路是跳转前保存当前页面的状态, 跳转后(监听路径的变化)恢复当前页面的状态

在跳转的页面:

const { savePageState } = useHistoryPagination()

const goDEtail = () => {
  savePageState(current, pageSize)
  navigate(`/xxx`)
}

在主页面:

const { getPageState, restorePageState } = useHistoryPagination(setCurrent, setPageSize)

// 监听路由变化
useEffect(() => {
  // 跳转到其它页面又返回时,读取缓存的页面状态
  const pageState = getPageState()
  if (Object.keys(pageState).length > 0) {
    setTimeout(() => {
      restorePageState(pageState)
    }, 1000)
  }
}, [location.pathname])

白屏检测

只检测root节点内部是否有内容, 如果没有内容, 则显示白屏检测页面

部分引入的内容说明:

引入值说明
searchRouteDetail用于获取路由信息, 跳转至路由守卫
routes路由配置文件, 跳转至路由配置表
sleep一个Promise方法, 用于延迟执行代码

useWhiteScreen.tsx

import React, { useEffect } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { searchRouteDetail } from '@/routes/RouterGuard'
import routes from '@/routes'
import sleep from '@/utils/sleep'

// 白屏检测 / white screen detection
export default function useWhiteScreen() {
  const navigate = useNavigate()
  const { pathname } = useLocation()

  // 检测方法比较简单 当id:为root的div内部没有任何内容时,显示白屏检测
  // The detection method is relatively simple. When there is no content inside the div with id: root, the white screen detection is displayed.
  useEffect(() => {
    // 选择需要观察变动的节点 / Select the node to observe for changes
    const targetNode = document.getElementById('root')!
    
    /*
    * 观察器的配置(需要观察什么变动)/ Observer configuration (what changes need to be observed)
    * 三个属性的含义分别是:/ The meanings of the three attributes are:
    * childList:子节点的变动(指新增、删除或者更改)/ ChildList: changes in child nodes (refers to addition, deletion or modification)
    * attributes:属性的变动 / Attributes: changes in attributes
    * subtree:所有后代节点的变动 / Subtree: changes in all descendant nodes
    */
    const config = { attributes: true, childList: true, subtree: true }
    
    // 当观察到变动时执行的回调函数 / Callback function executed when changes are observed
    const callback = (mutationsList: MutationRecord[], observer: MutationObserver) => {
      for (const mutation of mutationsList) {
        if (mutation.type === 'childList') {
          // 检测root节点内部是否有内容 / Check whether there is content inside the root node
          if (targetNode.innerHTML === '' && pathname !== '/404') {
            // 获取路由信息 / Get routing information
            const routeDetail = searchRouteDetail(pathname, routes)
            const whiteScreenInfo = {
              pathname,
              routeDetail
            }
            localStorage.setItem('whiteScreenInfo', JSON.stringify(whiteScreenInfo))
            sleep(500).then(() => {
              navigate('/404')
              window.location.reload()
            })
          }
        } else if (mutation.type === 'attributes') {
          // console.log('The ' + mutation.attributeName + ' attribute was modified.')
        }
      }
    }
    
    // 创建一个观察器实例并传入回调函数 / Create an observer instance and pass in the callback function
    const observer = new MutationObserver(callback)
    
    // 以上述配置开始观察目标节点 / Start observing the target node with the above configuration
    observer.observe(targetNode, config)
    
    // 之后,可停止观察 / Later, you can stop observing
    // observer.disconnect()
  }, [])
}

404.tsx

这个页面可以自定义, 也可以使用antd的Result组件

import React, { ReactNode, useEffect, useState } from 'react'
import { Button, Result } from 'antd'
import { useNavigate } from 'react-router-dom'

const Error = () => {
  const navigate = useNavigate()
  const [errorText, setErrorText] = useState<ReactNode>('Sorry, The page does not exist or has no permissions.')
  const [isWhiteScreen, setIsWhiteScreen] = useState<boolean>(false)
 
  useEffect(() => {
    const whiteScreenInfo = localStorage.getItem('whiteScreenInfo')
    if (whiteScreenInfo) {
      setIsWhiteScreen(true)
      const errorInfo = JSON.parse(whiteScreenInfo)
      const errorText = (
        <>
          <div style={{ color: 'black', fontSize: '16px' }}>
            Oops, there is an exception in the data.
          </div>
          <div style={{ color: 'black', fontSize: '16px' }}>
            the path:
            <span style={{ fontWeight: 600, color: '#06569f' }}> {errorInfo?.routeDetail?.name}</span>
          </div>
          <div style={{ color: 'black', fontSize: '16px' }}>
            the parameter:
            <span style={{ fontWeight: 600, color: '#06569f' }}> {getParams(errorInfo.pathname, errorInfo.routeDetail.path)}</span>
          </div>
        </>
      )
      setErrorText(errorText)
      localStorage.removeItem('whiteScreenInfo')
    }
  }, [])
  
  // 根据pathname和路由的path属性,获取路由动态参数 / Get the dynamic parameters of the route according to the pathname and the name attribute of the route
  const getParams = (pathname: string, path: string) => {
    const pathArr = pathname.split('/')
    const nameArr = path.split('/')
    const params: any = {}
    nameArr.forEach((item, index) => {
      if (item.startsWith(':')) {
        params[item.slice(1)?.replace('?', '')] = pathArr[index]
      }
    })
    // 返回格式: id: xx / Return format: id: xx
    let errStr = ''
    Object.keys(params).forEach(item => {
      errStr += `${item}: ${params[item]} `
    })
    return errStr
  }
  
  return (
    <div>
      <Result
        status={isWhiteScreen ? 'warning' : '404'}
        title={isWhiteScreen ? 'Page Error' : '404'}
        subTitle={errorText}
        extra={<Button type="primary" onClick={()=>navigate('/')}>Back Home</Button>}
      />
    </div>
  )
}

export default Error

用法

在App.tsx中使用即可

const App = () => {
  useWhiteScreen()
}

显示效果:

whiteScreen

Last Updated:
Contributors: huangdingxin