JS 常用工具方法

类型判断

// 以下为更精确的判断方式,某些场景下比使用 typeof & instanceof 更高效、准确
// 判断变量是注意非undefined,Object.prototype.toString.call(person) // person is not defined
Object.prototype.toString.call(123)        // '[object Number]'
Object.prototype.toString.call('str')      // '[object String]'
Object.prototype.toString.call(true)       // '[object Boolean]'
Object.prototype.toString.call(null)       // '[object Null]'
Object.prototype.toString.call(undefined)  // '[object Undefined]'
Object.prototype.toString.call({})         // '[object Object]'
Object.prototype.toString.call([])         // '[object Array]'
Object.prototype.toString.call(() => {})   // '[object Function]'
Object.prototype.toString.call(/reg/g)     // '[object RegExp]'
Object.prototype.toString.call(new Date()) // '[object Date]'
Object.prototype.toString.call(Math)       // '[object Math]'
Object.prototype.toString.call(window)     // '[object Window]'
Object.prototype.toString.call(document)   // '[object HTMLDocument]'
Object.prototype.toString.call(10n)        // '[object BigInt]'
Object.prototype.toString.call(Symbol())   // '[object Symbol]'

日期格式化

/**
 * 日期格式化
 * @description dateFormat(new Date(), 'yyyy年MM月dd日 EEE HH:mm:ss.S a A 第q季度') // 2024年09月02日 星期一 09:12:12.553 上午 AM 第3季度
 * @description dateFormat(new Date(), 'yy-M-d EE H:m:ss a A q季度') // 24-9-2 周一 9:12:12 上午 AM 3季度
 * @param {Date|String|Number} date 日期
 * @param {String} fmt 格式
 * @returns {String} 如:2024-09-02 12:12:12
 */
export const dateFormat = (date, format = 'yyyy-MM-dd HH:mm:ss') => {
  if (!date) return ''
  // 不为Date类型进行处理
  if (!(date instanceof Date)) {
    // 判断数字时间戳
    if (!isNaN(date)) {
      // 10位时间戳转13位
      date = `${date}`
      if (date.length === 10) date = +date * 1000
      // 字符串时间戳转数字时间戳
      else if (date.length === 13) date = +date
      // 时间戳格式错误返回''
      else return ''
    } else if (typeof date === 'string') {
      // ios无法使用yyyy-MM-dd HH:mm:ss转换为Date,需将 - 替换为 /
      date = date.replace(/-/g, '/')
    }
    date = new Date(date)
    if (isNaN(date)) return ''
  }
  const m = (reg, fmt) => {
    const match = reg.exec(fmt)
    return match ? match[1] : ''
  }
  const t = (r, v) => {
    const reg = new RegExp(`(${r})`)
    if (reg.test(format)) {
      const mStr = m(reg, format)
      if (v instanceof Function) v(reg, mStr)
      else format = format.replace(reg, mStr.length === 1 ? v : `00${v}`.substring(`${v}`.length))
    }
  }
  const o = {
    'y+': r => t(r, (reg, mStr) => (format = format.replace(reg, `${date.getFullYear()}`.substring(4 - mStr.length)))), // 年
    'M+': r => t(r, date.getMonth() + 1), // 月份
    'd+': r => t(r, date.getDate()), // 日
    'E+': r => t(r, (reg, mStr) => (format = format.replace(reg, `${mStr.length > 1 ? (mStr.length > 2 ? '星期' : '周') : ''}${'日一二三四五六'.split('')[+date.getDay()]}`))), // 星期EEE、周EE
    'h+': r => t(r, date.getHours() % 12 === 0 ? 12 : date.getHours() % 12), // 小时
    'H+': r => t(r, date.getHours()), // 小时
    'm+': r => t(r, date.getMinutes()), // 分
    's+': r => t(r, date.getSeconds()), // 秒
    'q+': r => t(r, Math.floor((date.getMonth() + 3) / 3)), // 季度
    'S': r => t(r, date.getMilliseconds()), // 毫秒
    'a': r => t(r, date.getHours() < 12 ? '上午' : '下午'), // 上午/下午
    'A': r => t(r, date.getHours() < 12 ? 'AM' : 'PM'), // AM/PM
  }
  // 根据转换方式循环处理
  for (const k in o) o[k](k)
  // 返回最终格式化的时间
  return format
}

秒格式化

小时 分钟 -
/**
 * 秒格式化
 * @description secondFormat(100000) // 1天3小时46分钟40秒
 * @param {Number} second 秒
 * @param {String} format 格式化类型,'dhms'中一个或多个,不分先后顺序,如:ms: 分钟 秒,dms: 天 分钟 秒
 * @returns {String} 如:1天3小时46分钟40秒
 */
export const secondFormat = (second, format = 'dhms') => {
  if (!/d|h|m|s/.test(format)) {
    throw new Error('\'format\' argument must a string contains one or more of \'dhms\'.')
  }
  return [
    { f: 'd', u: '天', p: 86400 },
    { f: 'h', u: '小时', p: 3600 },
    { f: 'm', u: '分钟', p: 60 },
    { f: 's', u: '秒', p: 1 }
  ].reduce((strs, { f, u, p }) => {
    if (format.includes(f)) {
      const cur = Math.floor(second / p)
      if (cur > 0) {
        second = second % p
        strs.push(`${cur}${u}`)
      }
    }
    return strs
  }, []).join('')
}

时间转换为xx时间前

/**
 * 时间转换为xx时间前
 * @description getTimeInfo('2020-01-02 12:12:12') // 2月前
 * @param {Date|String|Number} date 时间
 * @returns {String} 如:5天前
 */
export const getTimeInfo = date => {
  if (!date) return ''
  // 不为Date类型进行处理
  if (!(date instanceof Date)) {
    // 判断数字时间戳
    if (!isNaN(date)) {
      // 10位时间戳转13位
      date = `${date}`
      if (date.length === 10) date = +date * 1000
      // 字符串时间戳转数字时间戳
      else if (date.length === 13) date = +date
      // 时间戳格式错误返回''
      else return ''
    } else if (typeof date === 'string') {
      // ios无法使用yyyy-MM-dd HH:mm:ss转换为Date,需将 - 替换为 /
      date = date.replace(/-/g, '/')
    }
    date = new Date(date)
    if (isNaN(date)) return ''
  }
  const diff = Date.now() - date.getTime() // 现在的时间-传入的时间 = 相差的时间(单位 = 毫秒)
  if (diff < 0) return ''
  if (diff / 1000 < 60) return '刚刚'
  if (diff / 60000 < 60) return Math.floor(diff / 60000) + '分钟前'
  if (diff / 3600000 < 24) return Math.floor(diff / 3600000) + '小时前'
  if (diff / 86400000 < 31) return Math.floor(diff / 86400000) + '天前'
  // if (diff / 2592000000 < 12) return Math.floor(diff / 2592000000) + '月前'
  // return Math.floor(diff / 31536000000) + '年前'
  return dateFormat(date)
}

顺序执行Promise

/**
 * 顺序执行 Promise
 * 
 * function p1(){return new Promise((n,o)=>{setTimeout(()=>{console.warn("测试错误 1"),o({msg:"测试错误 1"})},500)})} // 测试错误 1
 * function p2(){return new Promise((n,o)=>{setTimeout(()=>{console.log("成功 2"),n({msg:"成功 2"})},600)})}          // 成功 2
 * function p3(){return new Promise((n,o)=>{setTimeout(()=>{console.warn("测试错误 3"),o({msg:"测试错误 3"})},300)})} // 测试错误 3
 * function p4(){return new Promise((n,o)=>{setTimeout(()=>{console.log("成功 4"),n({msg:"成功 4"})},400)})}          // 成功 4
 * function p5(){return new Promise((n,o)=>{setTimeout(()=>{console.warn("测试错误 5"),o({msg:"测试错误 5"})},200)})} // 测试错误 5
 *
 * const promises = [p1, p2, p3, p4, p5]
 *
 * promiseQueue(promises).then(console.log)
 * // 一次性返回结构数组
 * // 0: {msg: "测试错误 1"}
 * // 1: {msg: "成功 2"}
 * // 2: {msg: "测试错误 3"}
 * // 3: {msg: "成功 4"}
 * // 4: {msg: "测试错误 5"}
 *
 * @param {Array<Function: Promise>} 返回Promise对象的待执行方法数组
 * @returns {Array<Object>} 返回Promise对象,通过resolve获取顺序执行的结果(包含reject)
 */
export const promiseQueue = async promises => {
  const result = []
  for (const promise of promises) {
    try {
      result.push(await promise())
    } catch (error) {
      result.push(error)
    }
  }
  return Promise.resolve(result)
}

隐藏手机号中间四位

/**
 * 手机号中间四位 *
 * @param {String} phone 手机号
 * @returns {String} 如:188****8888
 */
export const hidePhone = phone => {
  if (!phone) return ''
  const arr = phone.split('')
  arr.splice(3, 4, '****')
  return arr.join('')
}

限制数据框内容仅为数字

/**
 * 限制数据框内容仅为数字
 * @description 校验、匹配规则可根据需求自定义,当前为保留正整数字符串
 * 
 * 某些情况下input[type=number]、InputNumber不能完全限制输入非数字字符使用
 * 
 * 以Element-ui Input为例,num可能为数组表单中的值,所以使用传值修改
 * 
 * <Input v-model="formData.num" @input="val => inputCheck(val, formData)" />
 * 
 * inputCheck (val, formData) {
 *   formData.num = inputFilter(val)
 * }
 * 
 * @param {String} val 输入字符
 * @returns {String|Number} 过滤后数字字符串(正整数)
 */
export const inputFilter = val => {
  if (val) {
    const valArr = val.match(/[1-9]\d*/g)
    if (valArr && valArr.length > 0) {
      val = valArr[0]
    } else {
      val = ''
    }
  }
  return val
}

/**
 * 整数,保留2位小数,最大100,最小0.01
 * <input type="text" :value="value" :readonly="i === 0" @input="arrangeValue" />
 * arrangeValue (e) {
 *   let { value } = e.target
 *   // 此处对0 和 未输入完的小数不作处理
 *   if (+value === 0 || value.endsWith('.')) {
 *     return
 *   }
 *   value = +inputFilter(value)
 *   e.target.value = value
 * }
 */
const inputFilter = val => {
  if (+val >= 100) return 100
  if (+val < 0.01) return 0.01
  if (val) {
    const valArr = val.match(/[+]?\d{0,2}(\.\d{1,2})?/g)
    if (valArr && valArr.length > 0) {
      val = +valArr[0]
    } else {
      val = 1
    }
  }
  return val
}

七牛云上传

import * as qiniu from 'qiniu-js'
import Cookies from 'js-cookie'
import uuid from 'uuid'
import $http from '@/libs/request'

/**
 * 七牛云上传
 * dependencies: {
 *   qiniu-js,
 *   uuid,
 *   js-cookie
 * }
 * 图片处理(裁剪大小、缩略图等)https://developer.qiniu.com/dora/manual/3683/img-directions-for-use
 *
 * @param file 上传的文件
 * @param next 上传中处理(进度条等)
 *
 * @param then 上传完成处理
 * @param catch 上传错误处理
 *
 * @return Promise
 */
// 相关参数
const UPTOKEN = '_UPTOKEN_'
const REQUEST_URL = '/upload/v1/uptoken'
const REGION = qiniu.region.z0
// 上传处理
const uploadHandler = (token, file, next, complete, error) => {
  // 文件类型
  const typeArr = file.type.split('/')
  // uuid + .文件类型
  const fileName = `${uuid()}.${typeArr[1]}`
  const observable = qiniu.upload(file, fileName, token, {}, { region: REGION })
  observable.subscribe({
    next ({ total }) { next && next(total) },
    error (err) { error && error(err) },
    complete (res) { complete && complete(res) }
  })
}
// 获取token,上传
export default (file, next) => {
  let token = Cookies.get(UPTOKEN)
  return new Promise((resolve, reject) => {
    if (!token) {
      $http.request({
        url: REQUEST_URL,
        method: 'post'
      }).then(({ code, data }) => {
        if (code === 1) {
          const date = new Date()
          date.setSeconds(date.getSeconds() + 3500) // uptoken失效前缓存
          Cookies.set(UPTOKEN, data, { expires: date })
          token = data
          uploadHandler(token, file, next, res => resolve(res), err => reject(err))
        } else {
          reject(new Error('获取uptoken失败'))
        }
      })
    } else {
      uploadHandler(token, file, next, res => resolve(res), err => reject(err))
    }
  })
}

滚动条滚动动画

/**
 * 滚动条滚动动画
 * @description scrollTo(window, 0, 1000)
 * @param {HTMLDOM | window} el 滚动对象
 * @param {Number} from 滚动开始位置
 * @param {Number} to 滚动结束位置
 * @param {Number} duration 间隔时间
 * @param {Function} endCallback 动画结束回调
 */
export const scrollTo = (el, from, to = 0, duration = 500, endCallback) => {
  if (!window.requestAnimationFrame) {
    window.requestAnimationFrame = (
      window.webkitRequestAnimationFrame ||
      window.mozRequestAnimationFrame ||
      window.msRequestAnimationFrame ||
      function (callback) {
        return window.setTimeout(callback, 1000 / 60)
      }
    )
  }
  const difference = Math.abs(from - to)
  const step = Math.ceil(difference / duration * 50)

  const scroll = (start, end, step) => {
    if (start === end) {
      endCallback && endCallback()
      return
    }

    let d = (start + step > end) ? end : start + step
    if (start > end) {
      d = (start - step < end) ? end : start - step
    }

    if (el === window) {
      window.scrollTo(d, d)
    } else {
      el.scrollTop = d
    }
    window.requestAnimationFrame(() => scroll(d, end, step))
  }
  scroll(from, to, step)
}

页面截图

html2canvas

import html2canvas from 'html2canvas'
/**
 * 页面截图
 * @param dom 需要保存为图片的dom节点
 * @param imageType 图片类型,默认:image/png
 * @param toFile 是否输出为文件,默认:true
 * @returns
 */
export function generateImage(dom, imageType = 'image/png', toFile = true) {
  return new Promise(async (resolve, reject) => {
    try {
      const canvas = await html2canvas(dom, {})
      const imageUrl = canvas.toDataURL(imageType)
      if (toFile) {
        const imageFile = dataURLtoFile(imageUrl)
        resolve(imageFile)
      } else {
        resolve(imageUrl)
      }
    } catch (error) {
      reject(error)
    }
  })
}

base64转file

/**
 * base64转file
 * @param {String} dataURL base64
 * @param {String} filename 文件名(不带后缀),默认:当前时间戳
 * @returns {File} 返回File对象
 */
export const dataURLtoFile = (dataURL, filename = Date.now()) => {
  const arr = dataURL.split(',')
  const mime = arr[0].match(/:(.*?);/)[1]
  const typeArr = mime.split('/')
  if (typeArr && typeArr.length > 1) {
    filename = `${filename}.${typeArr[1]}`
  }
  const bstr = atob(arr[1])
  let n = bstr.length
  const u8arr = new Uint8Array(n)
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n)
  }
  return new File([u8arr], filename, { type: mime })
}

base64转blob

/**
 * base64转blob
 * @param {String} dataURL base64
 * @returns {Blob} 返回Blob对象
 */
export const dataURLToBlob = dataURL => {
  const arr = dataURL.split(',')
  const mime = arr[0].match(/:(.*?);/)[1]
  const bstr = atob(arr[1])
  let n = bstr.length
  const u8arr = new Uint8Array(n)
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n)
  }
  return new Blob([u8arr], { type: mime })
}

blob转file

/**
 * blob转file
 * @param {Blob} blob Blob对象
 * @param {String} filename 文件名(不带后缀),默认:当前时间戳
 * @returns {File} 返回File对象
 */
export const blobToFile = (blob, filename = Date.now()) => {
  const typeArr = blob.type.split('/')
  if (typeArr && typeArr.length > 1) {
    filename = `${filename}.${typeArr[1]}`
  }
  return new File([blob], filename, { type: blob.type })
}

blob转json

/**
 * blob转json
 * @param {Blob} blob Blob对象,type为 text/xml 或 application/json
 * @returns {Promise} 返回Promise对象,then(json => {})
 */
export const blobToJson = blob => {
  return new Promise((resolve, reject) => {
    if (blob.size <= 0) {
      reject(new Error('blob is empty'))
    }
    if (['text/xml', 'application/json'].includes(blob.type)) {
      const reader = new FileReader()
      reader.onload = () => {
        try {
          resolve(JSON.parse(reader.result))
        } catch (e) {
          reject(e)
        }
      }
      reader.readAsText(blob)
    } else {
      reject(new Error('blob type is not text/xml or application/json'))
    }
  })
}

IOS heic、heif格式文件转换

heic2any

import heic2any from 'heic2any'
/**
 * IOS heic、heif格式文件转换
 * @param imageFile 图片文件
 * @param toType 目标类型,默认:image/jpeg
 * @returns
 */
export function imageTypeTrans(imageFile, toType = 'image/jpeg') {
  return new Promise(async (resolve, reject) => {
    try {
      const { name } = imageFile
      const heicOrHeifReg = /\.(heic|heif)$/i
      const isHeicOrHeif = heicOrHeifReg.test(name.toLowerCase())
      let convertedImage = imageFile
      if (isHeicOrHeif) {
        const blob = await heic2any({
          blob: imageFile,
          multiple: undefined, // 只返回主图片
          toType,
        })
        const suffix = toType.split('/')[0]
        convertedImage = new File([blob], name.replace(heicOrHeifReg, `.${suffix}`), {
          type: toType,
        })
      }
      resolve(convertedImage)
    } catch (error) {
      reject(error)
    }
  })
}

图片压缩

Compressor.js

import Compressor from 'compressorjs'
/**
 * 图片压缩
 * @param imageFile 图片文件
 * @param mimeType 输出图片类型,默认:image/jpeg
 * @returns
 */
export function imageCompress(imageFile, mimeType = 'image/jpeg') {
  return new Promise((resolve, reject) => {
    new Compressor(imageFile, {
      quality: 0.7,
      maxWidth: 720,
      maxHeight: 2000,
      mimeType,
      success (result) {
        resolve(result)
      },
      error (err) {
        reject(err)
      },
    })
  })
}

绑定事件

/**
 * 绑定事件 eventOn(element, event, handler)
 * @description eventOn(window, 'scroll', this.scroll)
 * @param {HTMLDOM | window} element 绑定对象
 * @param {String} event 事件名称,如:scroll
 * @param {Function} handler 事件方法
 */
export const eventOn = (function () {
  // ssr中使用注意服务端无效
  if (typeof window === 'undefined') return
  if (document.addEventListener) {
    return function (element, event, handler) {
      if (element && event && handler) {
        element.addEventListener(event, handler, false)
      }
    }
  } else {
    return function (element, event, handler) {
      if (element && event && handler) {
        element.attachEvent('on' + event, handler)
      }
    }
  }
})()

解绑事件

/**
 * 解绑事件 eventOff(element, event, handler)
 * @description eventOff(window, 'scroll', this.scroll)
 * @param {HTMLDOM | window} element 解绑对象
 * @param {String} event 事件名称,如:scroll
 * @param {Function} handler 事件方法
 */
export const eventOff = (function () {
  // ssr中使用注意服务端无效
  if (typeof window === 'undefined') return
  if (document.removeEventListener) {
    return function (element, event, handler) {
      if (element && event) {
        element.removeEventListener(event, handler, false)
      }
    }
  } else {
    return function (element, event, handler) {
      if (element && event) {
        element.detachEvent('on' + event, handler)
      }
    }
  }
})()

连字符转驼峰

/**
 * 连字符转驼峰
 * @description toCamelCase('hello_world', '_') // helloWorld
 * @param {String} str 连字符字符串
 * @param {String} separator 分隔符,默认为'-',可不传
 * @returns {String} 如:helloWorld
 */
export const toCamelCase = (str = '', separator = '-') => {
  if (Object.prototype.toString.call(str) !== '[object String]') {
    throw new Error('Argument must be a string')
  }
  if (str === '') {
    return str
  }
  const regExp = new RegExp(`\\${separator}(\\w)`, 'g')
  return str.replace(regExp, (matched, $1) => $1.toUpperCase())
}

驼峰转连字符

/**
 * 驼峰转连字符
 * @description fromCamelCase('helloWorld', '_') // hello_world
 * @param {String} str 驼峰字符串
 * @param {String} separator 分隔符,默认为'-',可不传
 * @returns {String} 如:hello_world
 */
export const fromCamelCase = (str = '', separator = '-') => {
  if (Object.prototype.toString.call(str) !== '[object String]') {
    throw new Error('Argument must be a string')
  }
  if (str === '') {
    return str
  }
  return str.replace(/([A-Z])/g, `${separator}$1`).toLowerCase()
}

文件尺寸格式化

/**
 * 文件尺寸格式化
 * @description formatSize(10240000) // 9.77MB
 * @param {String | Number} size 字节数
 * @returns {String} 如:9.77MB
 */
export const formatSize = size => {
  if (Object.prototype.toString.call(size) !== '[object Number]') {
    throw new Error('Argument(s) is illegal !')
  }
  const unitsHash = 'B,KB,MB,GB,TB,PB,EB,ZB,YB'.split(',')
  let index = 0
  while (size > 1024 && index < unitsHash.length) {
    size /= 1024
    index++
  }
  return Math.round(size * 100) / 100 + unitsHash[index]
}

获取指定范围内的随机数

/**
 * 获取指定范围内的随机数
 * @description getRandom(0, 10) // 9
 * @param {Number} min 最小范围,包含
 * @param {Number} max 最大范围,包含
 * @returns {Number} 如:9
 */
export const getRandom = (min = 0, max = 100) => {
  if (Object.prototype.toString.call(min) !== '[object Number]' || Object.prototype.toString.call(max) !== '[object Number]') {
    throw new Error('Argument(s) is illegal !')
  }
  if (min > max) {
    [min, max] = [max, min]
  }
  return Math.floor(Math.random() * (max - min + 1) + min)
}

随机字符串

-
-
/**
 * 原理:Number.prototype.toString(radix)
 *      radix: 范围在 2 到 36 之间,用于指定表示数字值的基数
 */
Math.random().toString(36).substring(2)

/**
 * 生成指定长度随机字符串
 * @param {number} length 随机字符串长度
 * @param {string} template 随机字符串取值模板,默认:0-9a-zA-Z
 * @returns {string}
 */
const randomString = (length, template = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') => length > 0
  ? Array.from({ length }, () => template[Math.floor(Math.random() * template.length)]).join('')
  : ''

根据概率随机

奖项 一等奖 二等奖 三等奖 四等奖 五等奖 谢谢参与
概率 0.01% 0.09% 0.9% 9% 30% 60%
-
/**
 * 根据概率随机
 * 
 * @description
 * 获取随机数 r
 * 一等奖 0.01%     0 < r && r <= 1 其实只有=1时
 * 二等奖 0.09%     1 < r && r <= 10
 * 三等奖  0.9%    10 < r && r <= 100
 * 四等奖    9%   100 < r && r <= 1000
 * 五等奖   30%  1000 < r && r <= 4000
 * 谢谢参与 60%  4000 < r && r <= 10000
 * 
 * 0.01%  0  1  ->                                  0  <  x  <=  (0.01) * 100
 * 0.09%  1  2  ->                       (0.01) * 100  <  x  <=  (0.01 + 0.09) * 100
 *  0.9%  2  3  ->                (0.01 + 0.09) * 100  <  x  <=  (0.01 + 0.09 + 0.9) * 100
 *    9%  3  4  ->          (0.01 + 0.09 + 0.9) * 100  <  x  <=  (0.01 + 0.09 + 0.9 + 9) * 100
 *   30%  4  5  ->      (0.01 + 0.09 + 0.9 + 9) * 100  <  x  <=  (0.01 + 0.09 + 0.9 + 9 + 30) * 100
 *   60%  5 max -> (0.01 + 0.09 + 0.9 + 9 + 30) * 100  <  x  <=  10000
 * 
 * @param {Array} prizes: [{ odds: number, ...others }] // odds 概率,百分之多少,所有概率总和为1(100%)
 * @param {number} rate 分率,百分之多少 -> 100,千分之多少 -> 1000,默认:100
 * @param {number} precision 概率精度,0.01 -> 2,0.001 -> 3,默认:2
 * @returns {Object} prize: { odds: number, ...others }
 */
const getWinPrize = (prizes, rate = 100, precision = 2) => {
  const r = Math.ceil(Math.random() * (rate ** precision))
  const getOddsSum = i => prizes.slice(0, i).reduce((sum, { odds }) => sum + odds * rate, 0)
  return prizes.find((_, i) => r > getOddsSum(i) && r <= getOddsSum(i + 1))
}

打乱数组

/**
 * 打乱数组
 * @description arrayShuffle([1, 2, 3]) // [2, 1, 3]
 * @param {Array} array 待乱序数组
 * @returns {Array} 如:[2, 1, 3]
 */
export const arrayShuffle = array => {
  if (!Array.isArray(array)) {
    throw new Error('Argument must be an array')
  }
  let end = array.length
  if (!end) {
    return array
  }
  while (end) {
    const start = Math.floor(Math.random() * end--);
    [array[start], array[end]] = [array[end], array[start]]
  }
  return array
}

获取Url参数

/**
 * 获取Url参数,注意:获取的参数值均为String类型
 * @description getUrlParam('f', 'https://www.baidu.com/s?ie=utf-8&f=8') // '8'
 * @param {String} variable 需要获取的参数名,传空值或者null会获取参数Object
 * @param {String} url url,默认为当前url
 * @returns {Object | String | Null} 如:variable为空值或null:{ ie: 'utf-8', f: '8' },未查找到的参数或url无参数返:null,variable有值查询具体参数返回String类型值
 */
export const getUrlParam = (variable = '', url = window.location.href) => {
  if (url === '' || !url.includes('=')) return null
  const query = url.substr(url.lastIndexOf('?') + 1)
  const vars = query.split('&')
  if (!variable) {
    return vars.reduce((params, v) => {
      const pair = v.split('=')
      params[pair[0]] = pair[1]
      return params
    }, {})
  }
  for (const v of vars) {
    const pair = v.split('=')
    if (pair[0] === variable) return pair[1]
  }
  return null
}

切分数组

/**
 * 切分数组
 *
 * const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
 * splitArray(arr, 4) // [[1, 2, 3, 4], [5, 6, 7, 8], [9]]
 *
 * @param {Array} arr 带切分数组
 * @param {Number} size 切分大小
 * @returns {Array<Array>} 切分后数组
 */
export const splitArray = (arr, size = 10) => Array.from({
  length: Math.ceil(arr.length / size)
}, (v, i) =>
  arr.slice(i * size, i * size + size)
)

两数组差集

/**
 * 切分数组
 *
 * arrayDiffSet([1, 2, 3], [1, 2, 4]) // [3, 4]
 *
 * @param {Array} arr1
 * @param {Array} arr2
 * @returns {Array} 两数组差集数组
 */
export const arrayDiffSet = (a, b) => [...a, ...b].filter(x => !a.includes(x) || !b.includes(x))

四舍五入到指定小数位

/**
 * 四舍五入到指定小数位
 * @param {Number} number 待转换数字
 * @param {Number} decimals 小数位数
 */
export const round = (number, decimals = 0) => Number(`${Math.round(`${number}e${decimals}`)}e-${decimals}`)

防抖

更多防抖、节流介绍可参照

/**
 * 防抖
 * @param {Function} fn 具体执行方法
 * @param {Number} delay 间隔时间,默认500毫秒
 */
export const debounce = (fn, delay = 500) => {
  let timer
  return () => {
    timer && clearTimeout(timer)
    timer = setTimeout(() => fn && fn(), delay)
  }
}

生成uuid

包含"-" -
/**
 * 生成uuid
 * @returns {String} uuid
 */
export const uuid = () => {
  const tmpUrl = URL.createObjectURL(new Blob())
  const tmpUrlStr = tmpUrl.toString()
  URL.revokeObjectURL(tmpUrl)
  return tmpUrlStr.substring(tmpUrlStr.lastIndexOf('/') + 1)
}

Object合并

/**
 * Object合并
 * @description 后面的Object覆盖合并到前面的Object
 * @param {Object} target 合并目标对象
 * @param {...Object} args 任意个待合并对象
 */
export const merge = (target, ...args) => {
  return args.reduce((acc, cur) => Object.keys(cur).reduce((subAcc, key) => {
    const srcVal = cur[key]
    if (Object.prototype.toString.call(srcVal) === '[object Object]') {
      subAcc[key] = merge(subAcc[key] ? subAcc[key] : {}, srcVal)
    } else if (Array.isArray(srcVal)) {
      subAcc[key] = srcVal.map((item, idx) => {
        if (Object.prototype.toString.call(item) === '[object Object]') {
          const curAccVal = subAcc[key] ? subAcc[key] : []
          return merge(curAccVal[idx] ? curAccVal[idx] : {}, item)
        } else {
          return item
        }
      })
    } else {
      subAcc[key] = srcVal
    }
    return subAcc
  }, acc), target)
}

深拷贝

/**
 * 深拷贝
 * @param {any} target 需要拷贝目标,任何值
 * @param cache 使用WeakSet处理循环引用,默认即可,无需传值
 * @returns {any} 新拷贝结果
 */
export const deepClone = (target, cache = new WeakSet()) => {
  const type = typeof target
  // 拷贝基本类型值
  if (!(target !== null && (type === 'object' || type === 'function'))) return target
  // 如果之前已经拷贝过该对象,直接返回该对象
  if (cache.has(target)) return target
  // 将对象添加缓存
  cache.add(target)
  const arrayTag = '[object Array]'
  const objectTag = '[object Object]'
  const mapTag = '[object Map]'
  const setTag = '[object Set]'
  const functionTag = '[object Function]'
  const boolTag = '[object Boolean]'
  const dateTag = '[object Date]'
  const numberTag = '[object Number]'
  const stringTag = '[object String]'
  const errorTag = '[object Error]'
  const symbolTag = '[object Symbol]'
  const regexpTag = '[object RegExp]'
  const tag = Object.prototype.toString.call(target)
  const Ctor = target.constructor
  let cloneTarget
  switch (tag) {
    case boolTag:
    case dateTag:
      cloneTarget = new Ctor(+target)
      break
    case numberTag:
    case stringTag:
    case errorTag:
      cloneTarget = new Ctor(target)
      break
    case objectTag:
    case mapTag:
    case setTag:
      cloneTarget = new Ctor()
      break
    case arrayTag: {
      const { length } = target
      const result = new target.constructor(length)
      if (length && typeof target[0] === 'string' && Object.hasOwnProperty.call(target, 'index')) {
        result.index = target.index
        result.input = target.input
      }
      cloneTarget = result
    } break
    case symbolTag:
      cloneTarget = Object(Symbol.prototype.valueOf.call(target))
      break
    case regexpTag: {
      const result = new target.constructor(target.source, /\w*$/.exec(target))
      result.lastIndex = target.lastIndex
      cloneTarget = result
    } break
  }
  if (tag === mapTag) {
    target.forEach((value, key) => {
      cloneTarget.set(key, deepClone(value, cache))
    })
    return cloneTarget
  }
  if (tag === setTag) {
    target.forEach(value => {
      cloneTarget.add(deepClone(value, cache))
    })
    return cloneTarget
  }
  if (tag === functionTag) return target
  Reflect.ownKeys(target).forEach(key => {
    // 递归拷贝属性
    cloneTarget[key] = deepClone(target[key], cache)
  })
  return cloneTarget
}

大文件切片上传

过程:
1、实例化ChunkUpload对象
2、使用load()方法加载文件并切片
3、切片完成后自动开始上传切片文件
4、切片文件上传完成后自动合并切片文件,合并后后端最好对新文件进行md5校验,后端对完整的文件进行后续处理
5、完成上传

依赖spark-md5计算文件md5
使用axios封装$http,可参照
后端切片文件合并,可参照
以下提供ts版本和js版本

////////////////////
///TypeScript版本///
////////////////////

import SparkMD5 from 'spark-md5'
import $http from './http'

/**
 * 大文件切片上传
 *
 * 注意:复合后缀,如:.tar.gz,需对fileSuffix变量初始化时进行判断
 *
 * ------------------------------------------------------------------
 *
 * // 实例化ChunkUpload对象
 * const chunkUpload = new ChunkUpload(file, {
 *   // uploadIndex: 0,
 *   // chunkSize: 4 * 1024 * 1024,
 *   next: (state: { loading: number, uploading: number, merging: number, uploadIndex: number, status: string }) => {
 *     const { loading, uploading, merging, uploadIndex, status } = state
 *     console.log(loading, uploading, merging, uploadIndex, status)
 *   },
 *   success: (data: string, md5: string) => {
 *     console.log(md5)
 *   },
 *   error: (code: number, msg: string) => {
 *     console.log(msg)
 *   }
 * })
 * // 调用load方法开始切片并上传
 * chunkUpload.load()
 *
 * ------------------------------------------------------------------
 *
 * @param file 上传的文件
 * @param options.chunkSize 切片大小,默认:4 * 1024 * 1024
 * @param options.uploadIndex 上传索引,用于初始化时的断点续传,默认:0
 * @param options.next ({ loading, uploading, merging, uploadIndex, status }) => {} 上传进度,
 *                      loading:加载切片进度
 *                      uploading:上传进度
 *                      merging:合并进度
 *                      uploadIndex:已上传索引,只有uploading状态才会改变
 *                      status:状态'pending' | 'loading' | 'uploading' | 'merging' | 'finished'
 * @param options.success: (data, md5) => {} 上传成功回调,data:文件上传文件名,md5:文件md5
 * @param options.error: (code, msg) => {} 上传错误回调,code:状态码,msg:错误信息
 *
 * @function load 初始化之后,调用load开始上传
 * @function stop 暂停上传,返回当前上传的切片索引
 * @function proceed 继续上传
 *
 * ------------------------------------------------------------------
 *
 * @backend 后端上传切片参数
 *           @PathVariable("part") String part,
 *           @RequestParam("file") MultipartFile file,
 *           @RequestParam("md5") String md5
 * @backend 后端检查文件数量参数
 *           @PathVariable("md5") String md5
 * @backend 后端文件合并参数
 *           @PathVariable("md5") String md5,
 *           @RequestParam("chunks") int chunks,
 *           @RequestParam("suffix") String suffix
 */

type StatusEnum = 'pending' | 'loading' | 'uploading' | 'merging' | 'finished'
type NextFunction = (state: {
  loading: number,
  uploading: number,
  merging: number,
  uploadIndex: number,
  status: StatusEnum
}) => void

const CHECK_URL = (md5: string): string => `upload/v1/file/check/${md5}`
const UPLOAD_URL = (i: number): string => `upload/v1/upload/chunk/${i}`
const MERGE_URL = (md5: string): string => `upload/v1/file/merge/${md5}`

export default class ChunkUpload {
  // 待处理文件
  private file: File
  // 文件尺寸
  private fileSize: number
  // 文件后缀
  private fileSuffix: string
  // 切片大小
  chunkSize: number
  // 切片数量
  chunkCount: number
  // 取消上传标记
  private isStop: boolean
  // 切片文件数组
  private chunkList: Blob[]
  // MD5
  md5: string
  // 实例化spark用于计算文件md5
  private spark: SparkMD5.ArrayBuffer
  // 通过FileReader读取文件进行切分、计算文件md5
  private fileReader: FileReader
  // 加载切片索引
  private loadIndex: number
  // 上传切片索引
  private uploadIndex: number
  // 上传状态
  private status: StatusEnum
  // 上传进度
  private next: NextFunction

  // 成功
  private success: (data: string, md5: string) => void

  // 错误
  private error: (code: number, msg: string) => void

  constructor (file: File, options?: {
    chunkSize?: number,
    uploadIndex?: number,
    next?: NextFunction,
    success?: (data: string, md: string) => void,
    error?: (code: number, msg: string) => void
  }) {
    const { name, size } = file
    this.file = file
    this.fileSize = size
    this.fileSuffix = name.substring(name.lastIndexOf('.'))
    this.chunkSize = +(options?.chunkSize || 4 * 1024 * 1024)
    this.chunkCount = Math.ceil(size / this.chunkSize)
    this.isStop = false
    this.chunkList = []
    this.md5 = ''
    this.uploadIndex = +(options?.uploadIndex || 0)
    this.loadIndex = 0
    this.spark = new SparkMD5.ArrayBuffer()
    this.fileReader = new FileReader()
    this.status = 'pending'
    this.next = options?.next || (() => null)
    this.success = options?.success || (() => null)
    this.error = options?.error || (() => null)
    // 初始化
    this.init()
  }

  // 读取文件并进行切片
  private init (): void {
    const { chunkCount, spark, fileReader, uploadIndex, status, next } = this
    // 设置进度
    next({
      loading: 0,
      uploading: 0,
      merging: 0,
      uploadIndex,
      status
    })
    fileReader.onload = ({ target }) => {
      const { loadIndex, uploadIndex, status } = this
      let i: number = loadIndex
      // spark-md5读取当前片
      spark.append(target?.result as ArrayBuffer)
      // 读取完之后,切片索引+1
      i++
      // 设置进度
      next({
        loading: +(i / chunkCount * 100).toFixed(1),
        uploading: 0,
        merging: 0,
        uploadIndex,
        status
      })
      // 递归切片
      if (i < chunkCount) {
        this.loadIndex = i
        this.load()
      } else {
        // 切片完成,计算md5
        this.md5 = spark.end()
        // 设置上传状态
        this.status = 'uploading'
        // 如果初始化时断点续传
        if (uploadIndex > 0) {
          // 检查服务器临时目录已上传切片
          this.check()
        } else {
          // 开始上传
          this.upload()
        }
      }
    }
  }

  // 检查切片
  private check (): void {
    const { md5, uploadIndex, error } = this
    axios.post(CHECK_URL(md5)).then(({ data, code, msg }) => {
      if (code === 1) {
        // 当前索引不大于存在数量
        if (data >= uploadIndex) {
          this.upload()
        } else {
          this.uploadIndex = 0
          this.upload()
        }
      } else {
        error(code, msg)
      }
    }).catch(err => error(-1, err.message))
  }

  // 文件切片
  load (): void {
    const { chunkSize, fileSize, file, loadIndex, fileReader, isStop } = this
    // 设置上传状态
    this.status = 'loading'
    if (isStop) return
    const start: number = loadIndex * chunkSize
    const end: number = ((start + chunkSize) >= fileSize) ? fileSize : start + chunkSize
    const blob: Blob = file.slice(start, end)
    this.chunkList.push(blob)
    fileReader.readAsArrayBuffer(blob)
  }

  // 上传切片
  private upload (): void {
    const { md5, chunkList, chunkCount, uploadIndex, isStop, status, next, error } = this
    // 取消上传
    if (isStop) return
    // 当前上传切片索引
    let i: number = uploadIndex
    // 拼装FormData对象,上传文件
    const formData = new FormData()
    formData.append('file', chunkList[i])
    formData.append('md5', md5)
    // 上传切片
    axios.post(UPLOAD_URL(i), formData, {
      transformRequest: [(params: FormData, headers: { [key: string]: string }) => {
        headers = { // eslint-disable-line
          'Content-Type': 'multipart/form-data'
        }
        return params
      }],
      onUploadProgress (e: { loaded: number, total: number }) {
        const { loaded, total } = e
        // 设置进度
        next({
          loading: 100,
          uploading: +((loaded / total + i) * 100 / chunkCount).toFixed(1),
          merging: 0,
          uploadIndex: i,
          status
        })
      }
    }).then(({ code, msg }) => {
      if (code === 1) {
        i++
        if (i < chunkCount) {
          this.uploadIndex = i
          this.upload()
        } else {
          // 设置上传状态
          this.status = 'merging'
          this.merge()
        }
      } else {
        error(code, msg)
      }
    }).catch(err => error(-1, err.message))
  }

  // 合并文件
  private merge (): void {
    const { md5, chunkCount, uploadIndex, fileSuffix, isStop, status, next, success, error } = this
    // 取消上传
    if (isStop) return
    next({
      loading: 100,
      uploading: 100,
      merging: 0,
      uploadIndex,
      status
    })
    axios.post(MERGE_URL(md5), {
      chunks: chunkCount,
      suffix: fileSuffix
    }).then(({ data, code, msg }) => {
      if (code === 1) {
        // 设置状态
        this.status = 'finished'
        // 设置进度
        next({
          loading: 100,
          uploading: 100,
          merging: 100,
          uploadIndex,
          status: this.status
        })
        // 上传完成回调,返回md5值
        success(data, md5)
      } else {
        error(code, msg)
      }
    }).catch(err => error(-1, err.message))
  }

  // 取消操作,返回当前已上传索引
  stop (): number {
    this.isStop = true
    return this.uploadIndex
  }

  // 继续操作
  proceed (): void {
    this.isStop = false
    const { chunkCount, status, loadIndex, uploadIndex } = this
    // 继续切片
    if (status === 'loading' && loadIndex < chunkCount) {
      this.load()
    } else if (status === 'uploading' && uploadIndex < chunkCount) {
      // 检查并继续上传
      this.check()
    } else if (status === 'merging') {
      // 继续合并
      this.merge()
    }
  }
}
////////////////////
///JavaScript版本///
////////////////////

import SparkMD5 from 'spark-md5'
import $http from './http'

/**
 * 大文件切片上传
 *
 * 注意:复合后缀,如:.tar.gz,需对fileSuffix变量初始化时进行判断
 *
 * ------------------------------------------------------------------
 *
 * // 实例化ChunkUpload对象
 * const chunkUpload = new ChunkUpload(file, {
 *   // uploadIndex: 0,
 *   // chunkSize: 4 * 1024 * 1024,
 *   next: ({ loading, uploading, merging, uploadIndex, status }) => {
 *     console.log(loading, uploading, merging, uploadIndex, status)
 *   },
 *   success: (data, md5) => {
 *     console.log(md5)
 *   },
 *   error: (code, msg) => {
 *     console.log(msg)
 *   }
 * })
 * // 调用load方法开始切片并上传
 * chunkUpload.load()
 *
 * ------------------------------------------------------------------
 *
 * @param file 上传的文件
 * @param options.chunkSize 切片大小,默认:4 * 1024 * 1024
 * @param options.uploadIndex 上传索引,用于初始化时的断点续传,默认:0
 * @param options.next ({ loading, uploading, merging, uploadIndex, status }) => {} 上传进度,
 *                      loading:加载切片进度
 *                      uploading:上传进度
 *                      merging:合并进度
 *                      uploadIndex:已上传索引,只有uploading状态才会改变
 *                      status:状态'pending' | 'loading' | 'uploading' | 'merging' | 'finished'
 * @param options.success: (data, md5) => {} 上传成功回调,data:文件上传文件名,md5:文件md5
 * @param options.error: (code, msg) => {} 上传错误回调,code:状态码,msg:错误信息
 *
 * @function load 初始化之后,调用load开始上传
 * @function stop 暂停上传,返回当前上传的切片索引
 * @function proceed 继续上传
 *
 * ------------------------------------------------------------------
 *
 * @backend 后端上传切片参数
 *           @PathVariable("part") String part,
 *           @RequestParam("file") MultipartFile file,
 *           @RequestParam("md5") String md5
 * @backend 后端检查文件数量参数
 *           @PathVariable("md5") String md5
 * @backend 后端文件合并参数
 *           @PathVariable("md5") String md5,
 *           @RequestParam("chunks") int chunks,
 *           @RequestParam("suffix") String suffix
 */

const CHECK_URL = md5 => `upload/v1/file/check/${md5}`
const UPLOAD_URL = i => `upload/v1/upload/chunk/${i}`
const MERGE_URL = md5 => `upload/v1/file/merge/${md5}`

export default class ChunkUpload {
  constructor (file, options) {
    const { name, size } = file
    // 待处理文件
    this.file = file
    // 文件尺寸
    this.fileSize = size
    // 文件后缀
    this.fileSuffix = name.substring(name.lastIndexOf('.'))
    // 切片大小
    this.chunkSize = +(options && options.chunkSize ? options.chunkSize : 4 * 1024 * 1024)
    // 切片数量
    this.chunkCount = Math.ceil(size / this.chunkSize)
    // 取消上传标记
    this.isStop = false
    // 切片文件数组
    this.chunkList = []
    // MD5
    this.md5 = ''
    // 加载切片索引
    this.uploadIndex = +(options && options.uploadIndex ? options.uploadIndex : 0)
    // 上传切片索引
    this.loadIndex = 0
    // 实例化spark用于计算文件md5
    this.spark = new SparkMD5.ArrayBuffer()
    // 通过FileReader读取文件进行切分、计算文件md5
    this.fileReader = new FileReader()
    // 上传状态 'pending' | 'loading' | 'uploading' | 'merging' | 'finished'
    this.status = 'pending'
    // 上传进度
    this.next = options && options.next ? options.next : () => null
    // (data, md5) => {} 上传成功回调,data:文件上传文件名,md5:文件md5
    this.success = options && options.success ? options.success : () => null
    // (msg) => {} 上传错误回调,msg:错误信息
    this.error = options && options.error ? options.error : () => null
    // 初始化
    this.init()
  }

  // 读取文件并进行切片
  init () {
    const { chunkCount, spark, fileReader, uploadIndex, status, next } = this
    // 设置进度
    next({
      loading: 0,
      uploading: 0,
      merging: 0,
      uploadIndex,
      status
    })
    fileReader.onload = ({ target }) => {
      const { loadIndex, status } = this
      let i = loadIndex
      // spark-md5读取当前片
      spark.append(target.result)
      // 读取完之后,切片索引+1
      i++
      // 设置进度
      next({
        loading: +(i / chunkCount * 100).toFixed(1),
        uploading: 0,
        merging: 0,
        uploadIndex,
        status
      })
      // 递归切片
      if (i < chunkCount) {
        this.loadIndex = i
        this.load()
      } else {
        // 切片完成,计算md5
        this.md5 = spark.end()
        // 设置上传状态
        this.status = 'uploading'
        // 如果初始化时断点续传
        if (uploadIndex > 0) {
          // 检查服务器临时目录已上传切片
          this.check()
        } else {
          // 开始上传
          this.upload()
        }
      }
    }
  }

  // 检查切片
  check () {
    const { md5, uploadIndex, error } = this
    $http.post(CHECK_URL(md5)).then(({ data, code, msg }) => {
      if (code === 1) {
        // 当前索引不大于存在数量
        if (data >= uploadIndex) {
          this.upload()
        } else {
          this.uploadIndex = 0
          this.upload()
        }
      } else {
        error(code, msg)
      }
    }).catch(err => error(-1, err.message))
  }

  // 文件切片
  load () {
    const { chunkSize, fileSize, file, loadIndex, fileReader, isStop } = this
    // 设置上传状态
    this.status = 'loading'
    if (isStop) return
    const start = loadIndex * chunkSize
    const end = ((start + chunkSize) >= fileSize) ? fileSize : start + chunkSize
    const blob = file.slice(start, end)
    this.chunkList.push(blob)
    fileReader.readAsArrayBuffer(blob)
  }

  // 上传切片
  upload () {
    const { md5, chunkList, chunkCount, uploadIndex, isStop, status, next, error } = this
    // 取消上传
    if (isStop) return
    // 当前上传切片索引
    let i = uploadIndex
    // 拼装FormData对象,上传文件
    const formData = new FormData()
    formData.append('file', chunkList[i])
    formData.append('md5', md5)
    // 上传切片
    $http.post(UPLOAD_URL(i), formData, {
      transformRequest: [(params, headers) => {
        headers = { // eslint-disable-line
          'Content-Type': 'multipart/form-data'
        }
        return params
      }],
      onUploadProgress ({ loaded, total }) {
        // 设置进度
        next({
          loading: 100,
          uploading: +((loaded / total + i) * 100 / chunkCount).toFixed(1),
          merging: 0,
          uploadIndex: i,
          status
        })
      }
    }).then(({ code, msg }) => {
      if (code === 1) {
        i++
        if (i < chunkCount) {
          this.uploadIndex = i
          this.upload()
        } else {
          // 设置上传状态
          this.status = 'merging'
          this.merge()
        }
      } else {
        error(code, msg)
      }
    }).catch(err => error(-1, err.message))
  }

  // 合并文件
  merge () {
    const { md5, chunkCount, uploadIndex, fileSuffix, isStop, status, next, success, error } = this
    // 取消上传
    if (isStop) return
    next({
      loading: 100,
      uploading: 100,
      merging: 0,
      uploadIndex,
      status
    })
    $http.post(MERGE_URL(md5), {
      chunks: chunkCount,
      suffix: fileSuffix
    }).then(({ data, code, msg }) => {
      if (code === 1) {
        // 设置状态
        this.status = 'finished'
        // 设置进度
        next({
          loading: 100,
          uploading: 100,
          merging: 100,
          uploadIndex,
          status: this.status
        })
        // 上传完成回调,返回md5值
        success(data, md5)
      } else {
        error(msg)
      }
    }).catch(err => error(err.message))
  }

  // 取消操作,返回当前已上传索引
  stop () {
    this.isStop = true
    return this.uploadIndex
  }

  // 继续操作
  proceed () {
    this.isStop = false
    const { chunkCount, status, loadIndex, uploadIndex } = this
    // 继续切片
    if (status === 'loading' && loadIndex < chunkCount) {
      this.load()
    } else if (status === 'uploading' && uploadIndex < chunkCount) {
      // 检查并继续上传
      this.check()
    } else if (status === 'merging') {
      // 继续合并
      this.merge()
    }
  }
}
© 2024 www.wdg.pub all right reserved Last modified: 2025-01-14

results matching ""

    No results matching ""