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()
}
显示效果: