滚动到当前元素位置
// 获取DOM元素
const element = document.getElementById('#element')
// 由于header的高度是固定的,所以需要减去header的高度
const headerHeight = 110
// getBoundingClientRect 返回元素的大小及其相对于视口的位置
const elementTop = element?.getBoundingClientRect().top || 0
const scrollTop = elementTop + window.pageYOffset - headerHeight
window.scrollTo({ top: scrollTop, behavior: 'smooth' })
浏览器全屏
浏览器进入全屏的方法:
const docElm = document.documentElement
if (docElm.requestFullscreen) {
//W3C
docElm.requestFullscreen()
} else if (docElm.mozRequestFullScreen) {
//FireFox
docElm.mozRequestFullScreen()
} else if (docElm.webkitRequestFullScreen) {
//Chrome等
docElm.webkitRequestFullScreen()
} else if (elem.msRequestFullscreen) {
//IE11
elem.msRequestFullscreen()
}
浏览器退出全屏的方法:
if (document.exitFullscreen) {
//W3C
document.exitFullscreen()
} else if (document.mozCancelFullScreen) {
//FireFox
document.mozCancelFullScreen()
} else if (document.webkitCancelFullScreen) {
//Chrome等
document.webkitCancelFullScreen()
} else if (document.msExitFullscreen) {
//IE11
document.msExitFullscreen()
}
浏览器分享
当我们需要将当前页面分享给其他人时,可以使用以下代码:
const handleShare = async () => {
if (navigator.share) {
// 获取host
const host = window.location.host
// 获取当前协议
const protocol = window.location.protocol
const url = `${ protocol }//${ host }${ YOURURL }`
try {
await navigator.share({
title: '分享的标题',
text: '分享的内容',
url: url // 也可以直接获取当前页面的url window.location.href
})
} catch (error) { /* empty */
}
} else {
message.error('你的浏览器不支持分享功能, 请更新浏览器.').then()
}
}
blob实现任意文件下载
可以做成一个公共方法,传入文件的url和文件名,就可以实现任意文件的下载
如果通过axios请求文件,需要设置responseType为blob
export function exportFile(number: string, exportType: string, other: number) {
return request.get<any, Blob>(`url?number=${number}&exportType=${exportType}&other=${other}`, { responseType: 'blob' })
}
具体实现代码:
import { message } from 'antd'
// 通过url下载文件 / download file by url
export function downloadFile(url: string, fileName: string) {
fetch(url).then((res) => res.blob()).then((content) => {
message.destroy()
// 创建blob地址 / create blob url
const blobUrl = URL.createObjectURL(content)
// 创建a标签 / create a tag
const a = document.createElement('a')
// 初始化a标签 / init a tag
a.download = fileName
// 将blob地址赋值给a标签的href / assign blob url to a tag href
a.href = blobUrl
// 将a标签插入到body中 / insert a tag into body
document.body.appendChild(a)
// 模拟点击 / simulate click
a.click()
// 移除a标签 / remove a tag
document.body.removeChild(a)
})
}
// 通过二进制流下载文件 / download file by binary stream
export function downloadFileByBinaryStream(fileData: Blob, fileName: string) {
message.destroy()
// 创建一个 Blob 对象
const blob = new Blob([fileData], { type: 'application/pdf;charset=UTF-8' })
// 创建url对象 / create blob url
const blobUrl = URL.createObjectURL(blob)
// 创建a标签 / create a tag
const a = document.createElement('a')
// 初始化a标签 / init a tag
a.download = fileName
// 将blob地址赋值给a标签的href / assign blob url to a tag href
a.href = blobUrl
// 将a标签插入到body中 / insert a tag into body
document.body.appendChild(a)
// 模拟点击 / simulate click
a.click()
// 移除a标签 / remove a tag
document.body.removeChild(a)
}
如果在axios配置responseType为blob,但是由于接口共用问题,有时候会返回url
这时候我们就需要将blob转回正常的json响应格式:
// 将JSON数据转化为Blob的对象 再转回去 / convert JSON data to Blob object and convert back
export async function convertBlobToJSON(BlobData: Blob): Promise<{
code: number;
data: string;
msg: string;
status: string;
url: string
}> {
let jsonObject: { code: number; data: string; msg: string; status: string; url: string }
// 将Blob对象转换为json对象
const reader = new FileReader()
// 设置读取完成后的回调函数
reader.onload = async function (event) {
// 步骤 2: 从事件对象中获取读取的数据
const jsonText = event.target?.result
// 将 JSON 文本解析为 JSON 对象
try {
jsonObject = JSON.parse(jsonText as string)
} catch (error) {
console.error('解析 JSON 时发生错误:', error)
}
}
// 开始读取 Blob 数据
reader.readAsText(BlobData)
// 等待读取完成
await new Promise((resolve) => setTimeout(resolve, 800))
// @ts-ignore
return jsonObject
}
将类型限定到对象的键
假如有以下对象:
const obj = {
a: {
name: {
first: 'John',
last: 'Doe',
},
address: {
city: 'New York',
state: 'NY',
}
},
b: {
name1: {
first: 'Jane',
last: 'Doe',
},
address1: {
city: 'New York',
state: 'NY',
}
}
}
我们定义一个方法
const obj = {
a: {
name: {
first: 'John',
last: 'Doe',
},
address: {
city: 'New York',
state: 'NY',
}
},
b: {
name1: {
first: 'Jane',
last: 'Doe',
},
address1: {
city: 'New York',
state: 'NY',
}
}
}
// 获取第一层的键
type FirstLevelKey = keyof typeof obj
// 获取第二层的键
type SecondLevelKey<T extends FirstLevelKey> = keyof typeof obj[T]
function getSecondLevelValue<K extends FirstLevelKey>(key: K, secondLevelKey: SecondLevelKey<K>) {
return obj[key][secondLevelKey]
}
getSecondLevelValue('a', 'name1')
这样就会有类型提示了
手机号输入框
当我们做国际项目时, 遇到各个国家不同的用户输入手机号, 同时需要显示其所在的国旗时, 我们可以使用react-phone-number-input
这个库
import PhoneInput, { parsePhoneNumber } from 'react-phone-number-input'
import 'react-phone-number-input/style.css'
// 电话号码改变
const handleChange = (newValue: string) => {
// 使用 parsePhoneNumber 方法解析手机号码并获取区号
try {
const selectInfo = parsePhoneNumber(newValue)
if (selectInfo) {
区号
const dialCode = selectInfo.countryCallingCode
电话号码
const phone_number = selectInfo.nationalNumber
}
} catch (error) {
console.error(error)
}
}
<PhoneInput
inputStyle={{ width: '100%', fontSize: '24px' }}
international
defaultCountry="CN"
onChange={handleChange}
/>
效果如下:
鼠标移入方向
<template>
<div id="app">
<div class="container" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
<div class="content">Hover over this area</div>
<div class="description" :class="descriptionClass">Description Text</div>
</div>
</div>
</template>
<script>
export default {
name: 'MouseEnter',
data() {
return {
descriptionClass: '',
}
},
methods: {
handleMouseEnter(event) {
// 获取元素的边界矩形,rect包含元素的top、right、bottom、left、width和height属性。
const rect = event.currentTarget.getBoundingClientRect()
// 计算鼠标在元素内部的水平位置,x是鼠标相对于元素左边的距离。
const x = event.clientX - rect.left
// 计算鼠标在元素内部的垂直位置,y是鼠标相对于元素顶部的距离。
const y = event.clientY - rect.top
const top = y
// 鼠标到元素底部的距离。
const bottom = rect.height - y
const left = x
// 鼠标到元素左边的距离。
const right = rect.width - x
// 找到以上四个距离中的最小值,即鼠标进入元素时距离最小的边。
const min = Math.min(top, bottom, left, right)
if (min === top) {
this.descriptionClass = 'enter-top'
} else if (min === bottom) {
this.descriptionClass = 'enter-bottom'
} else if (min === left) {
this.descriptionClass = 'enter-left'
} else if (min === right) {
this.descriptionClass = 'enter-right'
}
},
handleMouseLeave(event) {
const rect = event.currentTarget.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
const top = y
const bottom = rect.height - y
const left = x
const right = rect.width - x
const min = Math.min(top, bottom, left, right)
if (min === top) {
this.descriptionClass = 'leave-top'
} else if (min === bottom) {
this.descriptionClass = 'leave-bottom'
} else if (min === left) {
this.descriptionClass = 'leave-left'
} else if (min === right) {
this.descriptionClass = 'leave-right'
}
},
},
}
</script>
<style scoped>
.container {
position: relative;
width: 400px;
height: 400px;
border: 1px solid #000;
overflow: hidden;
}
.content {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
}
.description {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
transition: all 0.3s;
}
.enter-left {
top: 0;
left: -100%;
transform: translateX(100%) !important;
}
.enter-right {
top: 0;
left: 100%;
transform: translateX(-100%) !important;
}
.enter-top {
top: -100%;
left: 0;
transform: translateY(100%) !important;
}
.enter-bottom {
bottom: -100%;
left: 0;
transform: translateY(-100%) !important;
}
.leave-left {
top: 0;
left: 0;
transform: translateX(-100%);
}
.leave-right {
top: 0;
left: 0;
transform: translateX(100%) !important;
}
.leave-top {
top: 0;
left: 0;
transform: translateY(-100%);
}
.leave-bottom {
bottom: 0;
left: 0;
transform: translateY(100%);
}
.container:hover .description {
transform: translate(0, 0); /* animate into place */
}
</style>
自定义结果弹窗
主体结构
这个是弹窗的主体, 基于antd的Modal组件
import { Modal } from 'antd'
import React, { isValidElement, useEffect, useState } from 'react'
import { MessageAlertProps } from '@/hooks/useMessageAlert'
import './style/index.less'
import { ErrorOutlined, SuccessOutlined } from '@/components/CustomIcon'
interface IProps extends MessageAlertProps {
open?: boolean
}
const typeIcon = {
success: {
icon: <SuccessOutlined />,
color: '#52C41A',
},
error: {
icon: <ErrorOutlined />,
color: '#FF4D4F',
},
}
const MessageAlert = (props: IProps) => {
const [modalProps, setModalProps] = useState<IProps>(props)
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null)
// 初始化
useEffect(() => {
setModalProps(props)
// 设置定时器, 自动关闭弹出框
handleSetTimer()
}, [props])
// SVG动画
const svgAnimation = () => {
// 获取所有带有.svg类的SVG路径元素
const paths = document.querySelectorAll('.animation-svg') as unknown as SVGGeometryElement[]
if (!paths.length) return
// 遍历所有带有.svg类的路径元素
paths.forEach((path) => {
// 获取路径的长度
const length = String(path.getTotalLength())
if (props.animation) {
// 设置stroke-dasharray和stroke-dashoffset属性以应用描边动画
path.style.strokeDasharray = length
path.style.strokeDashoffset = length
}
// 设置动画颜色
if (props.type === 'success') {
path.style.setProperty('--from-color', 'yellow')
path.style.setProperty('--to-color', '#52C41A')
}
if (props.type === 'error') {
path.style.setProperty('--from-color', 'yellow')
path.style.setProperty('--to-color', '#FF4D4F')
}
})
}
// 点击关闭弹出框
const handleModalProps = () => {
setModalProps({ ...props, open: false })
clearTimeout(timer as NodeJS.Timeout)
setTimer(null)
}
// 鼠标一上去时, 清除定时器
const handleMouseEnter = () => {
clearTimeout(timer as NodeJS.Timeout)
setTimer(null)
}
// 鼠标移开时, 重新设置定时器
const handleSetTimer = () => {
if (props.duration && !timer) {
setTimer(
setTimeout(() => {
setModalProps({ ...props, open: false })
setTimer(null)
}, props.duration),
)
}
}
return (
<Modal
width={350}
onCancel={handleModalProps}
wrapClassName="customer-message-alert"
{...modalProps}
getContainer={false}
centered={true}
mask={false}
closeIcon={null}
footer={null}
>
<div
onClick={handleModalProps}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleSetTimer}
onAnimationStart={svgAnimation}
className="customer-message-alert-content"
>
{!!modalProps.icon && isValidElement(modalProps.icon) && (
modalProps.icon
)}
{!!modalProps.icon && (typeof modalProps.icon === 'string') && (
<img src={modalProps.icon} alt="emptyCart" />
)}
{!modalProps.icon && modalProps.type && typeIcon[modalProps.type].icon}
<p
className="alert-text"
style={{
color: modalProps.textColor || modalProps.type && typeIcon[modalProps.type].color
}}
>
{modalProps.description}
</p>
</div>
</Modal>
)
}
export default MessageAlert
弹窗的样式
.customer-message-alert {
--from-color: yellow;
--to-color: #52C41A;
.animation-svg {
stroke-width: 8;
stroke: var(--from-color);
fill: var(--to-color);
animation: draw 1s linear forwards, fill 1s forwards;
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
@keyframes fill {
from {
fill: var(--from-color)
}
to {
fill: var(--to-color);
}
}
.ant-modal-content, .ant-modal-header {
padding: 0;
box-shadow: none;
background-color: transparent;
}
.ant-modal-body {
text-align: center;
}
.customer-message-alert-content {
img {
width: 100%;
min-width: 200px;
max-width: 300px;
}
.alert-text {
margin: 0;
font-size: 22px;
font-family: "Times New Roman", Times, serif;
font-weight: bold;
color: #52C41A;
}
}
}
hook实现
这个组件是基于antd的Modal组件, 通过hook的方式实现调用, 可在非组件中调用(hook中也可以调用)
import MessageAlert from '@/components/MessageAlert'
import { ReactNode, useState, createElement, useEffect, useRef } from 'react'
import { ModalProps } from 'antd/es/modal/interface'
import ReactDOM from 'react-dom/client'
export interface MessageAlertProps extends ModalProps {
// 自定义图标
icon?: string | ReactNode
// 描述文本
description?: string
// 类型
type?: 'success' | 'error'
// 持续时间 - ms
duration?: number
// 文本颜色
textColor?: string
// 是否开启动画 默认关闭
animation?: boolean
}
/**
* @description 居中显示, 用于处理消息提示的hook
* */
export default function useMessageAlert() {
const [props, setProps] = useState<ModalProps>({})
const containerRef = useRef<HTMLDivElement | null>(null)
const rootRef = useRef<ReactDOM.Root | null>(null)
// 卸载组件
useEffect(() => {
return () => {
if (containerRef.current) {
// 卸载根组件
rootRef.current?.unmount()
// 移除容器
document.body.removeChild(containerRef.current)
}
}
}, [])
// 更新组件
useEffect(() => {
if (containerRef.current && rootRef.current) {
rootRef.current.render(<MessageAlert {...props} />)
}
}, [props])
const open = (props: MessageAlertProps) => {
return new Promise((resolve) => {
setProps({ ...props, open: true })
setTimeout(() => {
resolve('done')
}, props.duration || 1500)
})
}
const close = () => {
setProps({ ...props, open: false })
}
// 不手动渲染DOM直接调用(无法获取上下文以及主题定制), 返回一个Promise, 用于在外部调用
const showMessage = async (staticProps: MessageAlertProps) => {
return new Promise((resolve) => {
if (!containerRef.current) {
const container = document.createElement('div')
document.body.appendChild(container)
containerRef.current = container
rootRef.current = ReactDOM.createRoot(container)
}
const el = createElement(MessageAlert, { ...staticProps, open: true })
rootRef.current?.render(el)
// 延时关闭
if (staticProps.duration) {
setTimeout(() => {
resolve('done')
}, staticProps.duration)
} else {
resolve('done')
}
})
}
return {
alertApi: { open, close },
showMessage,
contextHolder: <MessageAlert {...props} />,
}
}
使用方法1
通过静态调用, 即DOM不需要手动渲染, 直接调用, 但是无法获取上下文以及主题定制
then方法会在弹窗关闭后(duration)执行
const { showMessage } = useMessageAlert()
showMessage({
type: 'success',
description: '测试用例',
duration: 99999
}).then()
使用方法2
通过手动渲染DOM, 通过contextHolder渲染, 可以获取上下文以及主题定制
const { contextHolder, alertApi } = useMessageAlert()
return (
<div>
{contextHolder}
<button onClick={() => alertApi.open({
type: 'success',
description: '测试用例',
duration: 99999
})}>点击</button>
</div>
)
效果演示
图片没对齐, 将就看😘😘😘
全局类型声明
在项目中, 有时候需要自定义类型声明文件.d.ts
, 用于全局使用
在项目根目录(非src)中创建typings.d.ts
文件, 并在tsconfig.json
中配置:
(如果已经配置了就不用配置)
{
"compilerOptions": {
"typeRoots": ["./typings"]
}
}
import { ActionType } from '@ant-design/pro-components'
import { Route } from '@ant-design/pro-layout/es/typing'
import '@umijs/max/typings'
// 显式声明全局变量
declare global {
namespace GLOBAL {
interface IRouteObject extends Route {
admin?: boolean
title?: string
locale?: string
name?: string
component?: string
layout?: boolean
path?: string
children?: IRouteObject[]
}
interface ITableRef<T> {
actionRef: ActionType
pagination: API.PaginationData
selectedRows: T[]
searchParams: any
handleAction: (status: keyof typeof actionFn, record?: T) => void
}
}
const process: {
env: {
UMI_ENV: string
BUILD_TIME: string
}
}
}
export {} // 使文件成为一个模块,避免命名冲突
由于typings.d.ts
是全局的, 所以在项目中可以直接使用
错误的写法
以下的下发不能在全局不引入的情况下使用, 因为在该文件的开头使用了import
导入
这种方法与上面那种正确的写法的区别:
declare global 用于显式地扩展全局命名空间。这样,文件中定义的类型就会被视为全局类型,不再局限于模块的作用域。因此,无论在项目的任何地方都可以直接使用这些类型,而不需要显式导入这个 .d.ts 文件。
为什么第一种方法可以全局使用
使用 declare global 是明确告诉 TypeScript 你在扩展全局作用域,这会让 TypeScript 将这些类型直接放入全局命名空间中,这样其他地方就不需要通过导入来引用这些类型。而这种的写法会导致 TypeScript 将整个文件视为模块,类型只能在导入该模块后使用。
import { ActionType } from '@ant-design/pro-components'
import { Route } from '@ant-design/pro-layout/es/typing'
import '@umijs/max/typings'
declare const process: {
env: {
UMI_APP_COM_BASE_API: string
UMI_APP_PH_BASE_API: string
UMI_APP_TH_BASE_API: string
UMI_APP_MY_BASE_API: string
UMI_APP_BASE_API: string
UMI_ENV: string
BUILD_TIME: string
}
}
declare namespace GLOBAL {
interface IRouteObject extends Route {
admin?: boolean
title?: string
locale?: string
name?: string
component?: string
layout?: boolean
path?: string
children?: IRouteObject[]
}
interface ITableRef<T> {
actionRef: ActionType
pagination: API.PaginationData
selectedRows: T[]
searchParams: any
handleAction: (status: keyof typeof actionFn, record?: T) => void
}
}
React实现KeepAlive
心心念念的KeepAlive, 一直想实现, 但是一直没有实现, 但是今天终于实现了呢
实现思路
使用Map存储组件, 通过useRef存储
实现代码
import useMenuName from '@/hooks/useMenuName'
import RightClickMenu from '@/layouts/RightClickMenu'
import { Tabs, TabsProps } from 'antd'
import { useEffect, useRef, useState } from 'react'
import { useOutlet } from 'react-router-dom'
import { useLocation } from 'umi'
import './style/index.less'
/**
* 全局 Layout 组件
* 用于缓存组件、处理路由变化、设置当前激活的 tab
* */
const Layout = () => {
const location = useLocation()
const componentList = useRef(new Map())
const outlet = useOutlet()
const { getMenuName } = useMenuName()
const [tabs, setTabs] = useState<TabsProps['items']>([])
const [activeKey, setActiveKey] = useState<string>('')
useEffect(() => {
// 缓存组件
setAliveComponent(location.pathname)
// 处理路由变化
handleRouteChange(location.pathname)
// 设置当前激活的 tab
setActiveKey(location.pathname)
}, [location.pathname])
// 缓存组件
const setAliveComponent = (key: string) => {
// 如果缓存中没有当前组件,则添加
if (!componentList.current.has(key)) {
componentList.current.set(key, outlet)
}
}
// 当路径变化时,更新 tabs
const handleRouteChange = (path: string) => {
// 如果已存在,则不添加
if (tabs?.find((item) => item.key === location.pathname)) {
return
}
// 添加新 tab
const newTab = {
key: path,
label: getMenuName(path).sourceName,
children: componentList.current.get(path),
closable: tabs?.length !== 1,
}
setTabs((prev) => [...prev!, newTab])
}
return (
<Tabs
size="small"
type="editable-card"
hideAdd={true}
tabBarStyle={{
paddingLeft: 40,
}}
activeKey={activeKey}
onChange={(key) => setActiveKey(key)}
>
{tabs?.map((tab) => (
<Tabs.TabPane
key={tab.key}
tab={tab.label}
closable={tabs.length !== 1 && tab.key !== activeKey}
>
{tab.children}
</Tabs.TabPane>
))}
</Tabs>
)
}
export default Layout
通过这个思路可以实现KeepAlive, 但是这个只是一个简单的实现, 如果需要更多的功能, 可以自行扩展
重点是通过const componentList = useRef(new Map())
和const outlet = useOutlet()
一个umijs
的例子: KeepAlive