滚动到当前元素位置

// 获取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}
/>

效果如下:

ellipsis

鼠标移入方向

<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>

Hover over this area
Description Text

自定义结果弹窗

主体结构

这个是弹窗的主体, 基于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>
)

效果演示

图片没对齐, 将就看😘😘😘

alertalert

全局类型声明

在项目中, 有时候需要自定义类型声明文件.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

Last Updated:
Contributors: huangdingxin