util.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  1. import { AbortablePromise } from './AbortablePromise'
  2. type NotUndefined<T> = T extends undefined ? never : T
  3. /**
  4. * 生成uuid
  5. * @returns string
  6. */
  7. export function uuid() {
  8. return s4() + s4() + s4() + s4() + s4() + s4() + s4() + s4()
  9. }
  10. function s4() {
  11. return Math.floor((1 + Math.random()) * 0x10000)
  12. .toString(16)
  13. .substring(1)
  14. }
  15. /**
  16. * @description 对num自动填充px
  17. * @param {Number} num
  18. * @return {string} num+px
  19. */
  20. export function addUnit(num: number | string) {
  21. return Number.isNaN(Number(num)) ? `${num}` : `${num}px`
  22. }
  23. /**
  24. * @description 判断target是否对象
  25. * @param value
  26. * @return {boolean}
  27. */
  28. export function isObj(value: any): value is object {
  29. return Object.prototype.toString.call(value) === '[object Object]' || typeof value === 'object'
  30. }
  31. /**
  32. * 获取目标原始类型
  33. * @param target 任意类型
  34. * @returns {string} type 数据类型
  35. */
  36. export function getType(target: unknown): string {
  37. // 得到原生类型
  38. const typeStr = Object.prototype.toString.call(target)
  39. // 拿到类型值
  40. const match = typeStr.match(/\[object (\w+)\]/)
  41. const type = match && match.length ? match[1].toLowerCase() : ''
  42. // 类型值转小写并返回
  43. return type
  44. }
  45. /**
  46. * @description 默认的外部格式化函数 - picker 组件
  47. * @param items - 要格式化的数据项数组或单个数据项
  48. * @param kv - 配置对象,包含 labelKey 作为键值
  49. * @returns 格式化后的字符串
  50. */
  51. export const defaultDisplayFormat = function (items: any[] | Record<string, any>, kv?: { labelKey?: string }): string {
  52. const labelKey: string = kv?.labelKey || 'value'
  53. if (Array.isArray(items)) {
  54. return items.map((item) => item[labelKey]).join(', ')
  55. } else {
  56. return items[labelKey]
  57. }
  58. }
  59. /**
  60. * @description 默认函数占位符 - pickerView组件
  61. * @param value 值
  62. * @return value
  63. */
  64. export const defaultFunction = <T>(value: T): T => value
  65. /**
  66. * @description 检查值是否不为空
  67. * @param value 值
  68. * @return {Boolean} 是否不为空
  69. */
  70. export const isDef = <T>(value: T): value is NonNullable<T> => value !== undefined && value !== null
  71. /**
  72. * @description 防止数字小于零
  73. * @param {number} num
  74. * @param {string} label 标签
  75. */
  76. export const checkNumRange = (num: number, label: string = 'value'): void => {
  77. if (num < 0) {
  78. throw new Error(`${label} shouldn't be less than zero`)
  79. }
  80. }
  81. /**
  82. * @description 防止 pixel 无意义
  83. * @param {number} num
  84. * @param {string} label 标签
  85. */
  86. export const checkPixelRange = (num: number, label: string = 'value'): void => {
  87. if (num <= 0) {
  88. throw new Error(`${label} should be greater than zero`)
  89. }
  90. }
  91. /**
  92. * 将 RGB 值转换为十六进制颜色代码。
  93. * @param {number} r - 红色分量 (0-255)。
  94. * @param {number} g - 绿色分量 (0-255)。
  95. * @param {number} b - 蓝色分量 (0-255)。
  96. * @returns {string} 十六进制颜色代码 (#RRGGBB)。
  97. */
  98. export function rgbToHex(r: number, g: number, b: number): string {
  99. // 将 RGB 分量组合成一个十六进制数。
  100. const hex = ((r << 16) | (g << 8) | b).toString(16)
  101. // 使用零填充十六进制数,确保它有 6 位数字(RGB 范围)。
  102. const paddedHex = '#' + '0'.repeat(Math.max(0, 6 - hex.length)) + hex
  103. return paddedHex
  104. }
  105. /**
  106. * 将十六进制颜色代码转换为 RGB 颜色数组。
  107. * @param hex 十六进制颜色代码(例如:'#RRGGBB')
  108. * @returns 包含红、绿、蓝三个颜色分量的数组
  109. */
  110. export function hexToRgb(hex: string): number[] {
  111. const rgb: number[] = []
  112. // 从第一个字符开始,每两个字符代表一个颜色分量
  113. for (let i = 1; i < 7; i += 2) {
  114. // 将两个字符的十六进制转换为十进制,并添加到 rgb 数组中
  115. rgb.push(parseInt('0x' + hex.slice(i, i + 2), 16))
  116. }
  117. return rgb
  118. }
  119. /**
  120. * 计算渐变色的中间变量数组。
  121. * @param {string} startColor 开始颜色
  122. * @param {string} endColor 结束颜色
  123. * @param {number} step 获取渲染位置,默认为中间位置
  124. * @returns {string[]} 渐变色中间颜色变量数组
  125. */
  126. export const gradient = (startColor: string, endColor: string, step: number = 2): string[] => {
  127. // 将hex转换为rgb
  128. const sColor: number[] = hexToRgb(startColor)
  129. const eColor: number[] = hexToRgb(endColor)
  130. // 计算R\G\B每一步的差值
  131. const rStep: number = (eColor[0] - sColor[0]) / step
  132. const gStep: number = (eColor[1] - sColor[1]) / step
  133. const bStep: number = (eColor[2] - sColor[2]) / step
  134. const gradientColorArr: string[] = []
  135. for (let i = 0; i < step; i++) {
  136. // 计算每一步的hex值
  137. gradientColorArr.push(
  138. rgbToHex(parseInt(String(rStep * i + sColor[0])), parseInt(String(gStep * i + sColor[1])), parseInt(String(bStep * i + sColor[2])))
  139. )
  140. }
  141. return gradientColorArr
  142. }
  143. /**
  144. * 确保数值不超出指定范围。
  145. * @param {number} num 要限制范围的数值
  146. * @param {number} min 最小范围
  147. * @param {number} max 最大范围
  148. * @returns {number} 在指定范围内的数值
  149. */
  150. export const range = (num: number, min: number, max: number): number => {
  151. // 使用 Math.min 和 Math.max 保证 num 不会超出指定范围
  152. return Math.min(Math.max(num, min), max)
  153. }
  154. /**
  155. * 比较两个值是否相等。
  156. * @param {any} value1 第一个值
  157. * @param {any} value2 第二个值
  158. * @returns {boolean} 如果值相等则为 true,否则为 false
  159. */
  160. export const isEqual = (value1: any, value2: any): boolean => {
  161. // 使用严格相等运算符比较值是否相等
  162. if (value1 === value2) {
  163. return true
  164. }
  165. // 如果其中一个值不是数组,则认为值不相等
  166. if (!Array.isArray(value1) || !Array.isArray(value2)) {
  167. return false
  168. }
  169. // 如果数组长度不相等,则认为值不相等
  170. if (value1.length !== value2.length) {
  171. return false
  172. }
  173. // 逐个比较数组元素是否相等
  174. for (let i = 0; i < value1.length; ++i) {
  175. if (value1[i] !== value2[i]) {
  176. return false
  177. }
  178. }
  179. // 所有比较均通过,则认为值相等
  180. return true
  181. }
  182. /**
  183. * 在数字前补零,使其达到指定长度。
  184. * @param {number | string} number 要补零的数字
  185. * @param {number} length 目标长度,默认为 2
  186. * @returns {string} 补零后的结果
  187. */
  188. export const padZero = (number: number | string, length: number = 2): string => {
  189. // 将输入转换为字符串
  190. let numStr: string = number.toString()
  191. // 在数字前补零,直到达到指定长度
  192. while (numStr.length < length) {
  193. numStr = '0' + numStr
  194. }
  195. return numStr
  196. }
  197. /** @description 全局变量id */
  198. export const context = {
  199. id: 1000
  200. }
  201. export type RectResultType<T extends boolean> = T extends true ? UniApp.NodeInfo[] : UniApp.NodeInfo
  202. /**
  203. * 获取节点信息
  204. * @param selector 节点选择器 #id,.class
  205. * @param all 是否返回所有 selector 对应的节点
  206. * @param scope 作用域(支付宝小程序无效)
  207. * @param useFields 是否使用 fields 方法获取节点信息
  208. * @returns 节点信息或节点信息数组
  209. */
  210. export function getRect<T extends boolean>(selector: string, all: T, scope?: any, useFields?: boolean): Promise<RectResultType<T>> {
  211. return new Promise<RectResultType<T>>((resolve, reject) => {
  212. let query: UniNamespace.SelectorQuery | null = null
  213. if (scope) {
  214. query = uni.createSelectorQuery().in(scope)
  215. } else {
  216. query = uni.createSelectorQuery()
  217. }
  218. const method = all ? 'selectAll' : 'select'
  219. const callback = (rect: UniApp.NodeInfo | UniApp.NodeInfo[]) => {
  220. if (all && isArray(rect) && rect.length > 0) {
  221. resolve(rect as RectResultType<T>)
  222. } else if (!all && rect) {
  223. resolve(rect as RectResultType<T>)
  224. } else {
  225. reject(new Error('No nodes found'))
  226. }
  227. }
  228. if (useFields) {
  229. query[method](selector).fields({ size: true, node: true }, callback).exec()
  230. } else {
  231. query[method](selector).boundingClientRect(callback).exec()
  232. }
  233. })
  234. }
  235. /**
  236. * 将驼峰命名转换为短横线命名。
  237. * @param {string} word 待转换的词条
  238. * @returns {string} 转换后的结果
  239. */
  240. export function kebabCase(word: string): string {
  241. // 使用正则表达式匹配所有大写字母,并在前面加上短横线,然后转换为小写
  242. const newWord: string = word
  243. .replace(/[A-Z]/g, function (match) {
  244. return '-' + match
  245. })
  246. .toLowerCase()
  247. return newWord
  248. }
  249. /**
  250. * 将短横线链接转换为驼峰命名
  251. * @param word 需要转换的短横线链接
  252. * @returns 转换后的驼峰命名字符串
  253. */
  254. export function camelCase(word: string): string {
  255. return word.replace(/-(\w)/g, (_, c) => c.toUpperCase())
  256. }
  257. /**
  258. * 检查给定值是否为数组。
  259. * @param {any} value 要检查的值
  260. * @returns {boolean} 如果是数组则返回 true,否则返回 false
  261. */
  262. export function isArray(value: any): value is Array<any> {
  263. // 如果 Array.isArray 函数可用,直接使用该函数检查
  264. if (typeof Array.isArray === 'function') {
  265. return Array.isArray(value)
  266. }
  267. // 否则,使用对象原型的 toString 方法进行检查
  268. return Object.prototype.toString.call(value) === '[object Array]'
  269. }
  270. /**
  271. * 检查给定值是否为函数。
  272. * @param {any} value 要检查的值
  273. * @returns {boolean} 如果是函数则返回 true,否则返回 false
  274. */
  275. // eslint-disable-next-line @typescript-eslint/ban-types
  276. export function isFunction<T extends Function>(value: any): value is T {
  277. return getType(value) === 'function' || getType(value) === 'asyncfunction'
  278. }
  279. /**
  280. * 检查给定值是否为字符串。
  281. * @param {unknown} value 要检查的值
  282. * @returns {value is string} 如果是字符串则返回 true,否则返回 false
  283. */
  284. export function isString(value: unknown): value is string {
  285. return getType(value) === 'string'
  286. }
  287. /**
  288. * 否是数值
  289. * @param {*} value
  290. */
  291. export function isNumber(value: any): value is number {
  292. return getType(value) === 'number'
  293. }
  294. /**
  295. * 检查给定值是否为 Promise 对象。
  296. * @param {unknown} value 要检查的值
  297. * @returns {value is Promise<any>} 如果是 Promise 对象则返回 true,否则返回 false
  298. */
  299. export function isPromise(value: unknown): value is Promise<any> {
  300. // 先将 value 断言为 object 类型
  301. if (isObj(value) && isDef(value)) {
  302. // 然后进一步检查 value 是否具有 then 和 catch 方法,并且它们是函数类型
  303. return isFunction((value as Promise<any>).then) && isFunction((value as Promise<any>).catch)
  304. }
  305. return false // 如果 value 不是对象类型,则肯定不是 Promise
  306. }
  307. /**
  308. * 检查给定的值是否为布尔类型
  309. * @param value 要检查的值
  310. * @returns 如果值为布尔类型,则返回true,否则返回false
  311. */
  312. export function isBoolean(value: any): value is boolean {
  313. return typeof value === 'boolean'
  314. }
  315. export function isUndefined(value: any): value is undefined {
  316. return typeof value === 'undefined'
  317. }
  318. export function isNotUndefined<T>(value: T): value is NotUndefined<T> {
  319. return !isUndefined(value)
  320. }
  321. /**
  322. * 检查给定的值是否为奇数
  323. * @param value 要检查的值
  324. * @returns
  325. */
  326. export function isOdd(value: number): boolean {
  327. if (typeof value !== 'number') {
  328. throw new Error('输入必须为数字')
  329. }
  330. // 使用取模运算符来判断是否为奇数
  331. // 如果 number 除以 2 的余数为 1,就是奇数
  332. // 否则是偶数
  333. return value % 2 === 1
  334. }
  335. /**
  336. * 是否为base64图片
  337. * @param {string} url
  338. * @return
  339. */
  340. export function isBase64Image(url: string) {
  341. // 使用正则表达式检查URL是否以"data:image"开头,这是Base64图片的常见前缀
  342. return /^data:image\/(png|jpg|jpeg|gif|bmp);base64,/.test(url)
  343. }
  344. /**
  345. * 将外部传入的样式格式化为可读的 CSS 样式。
  346. * @param {object | object[]} styles 外部传入的样式对象或数组
  347. * @returns {string} 格式化后的 CSS 样式字符串
  348. */
  349. export function objToStyle(styles: Record<string, any> | Record<string, any>[]): string {
  350. // 如果 styles 是数组类型
  351. if (isArray(styles)) {
  352. // 使用过滤函数去除空值和 null 值的元素
  353. // 对每个非空元素递归调用 objToStyle,然后通过分号连接
  354. const result = styles
  355. .filter(function (item) {
  356. return item != null && item !== ''
  357. })
  358. .map(function (item) {
  359. return objToStyle(item)
  360. })
  361. .join(';')
  362. // 如果结果不为空,确保末尾有分号
  363. return result ? (result.endsWith(';') ? result : result + ';') : ''
  364. }
  365. if (isString(styles)) {
  366. // 如果是字符串且不为空,确保末尾有分号
  367. return styles ? (styles.endsWith(';') ? styles : styles + ';') : ''
  368. }
  369. // 如果 styles 是对象类型
  370. if (isObj(styles)) {
  371. // 使用 Object.keys 获取所有属性名
  372. // 使用过滤函数去除值为 null 或空字符串的属性
  373. // 对每个属性名和属性值进行格式化,通过分号连接
  374. const result = Object.keys(styles)
  375. .filter(function (key) {
  376. return styles[key] != null && styles[key] !== ''
  377. })
  378. .map(function (key) {
  379. // 使用 kebabCase 函数将属性名转换为 kebab-case 格式
  380. // 将属性名和属性值格式化为 CSS 样式的键值对
  381. return [kebabCase(key), styles[key]].join(':')
  382. })
  383. .join(';')
  384. // 如果结果不为空,确保末尾有分号
  385. return result ? (result.endsWith(';') ? result : result + ';') : ''
  386. }
  387. // 如果 styles 不是对象也不是数组,则直接返回
  388. return ''
  389. }
  390. /**
  391. * 判断一个对象是否包含任何字段
  392. * @param obj 要检查的对象
  393. * @returns {boolean} 如果对象为空(不包含任何字段)则返回 true,否则返回 false
  394. */
  395. export function hasFields(obj: unknown): boolean {
  396. // 如果不是对象类型或为 null,则认为没有字段
  397. if (!isObj(obj) || obj === null) {
  398. return false
  399. }
  400. // 使用 Object.keys 检查对象是否有属性
  401. return Object.keys(obj).length > 0
  402. }
  403. /**
  404. * 判断一个对象是否为空对象(不包含任何字段)
  405. * @param obj 要检查的对象
  406. * @returns {boolean} 如果对象为空(不包含任何字段)则返回 true,否则返回 false
  407. */
  408. export function isEmptyObj(obj: unknown): boolean {
  409. return !hasFields(obj)
  410. }
  411. export const requestAnimationFrame = (cb = () => {}) => {
  412. return new AbortablePromise((resolve) => {
  413. const timer = setInterval(() => {
  414. clearInterval(timer)
  415. resolve(true)
  416. cb()
  417. }, 1000 / 30)
  418. })
  419. }
  420. /**
  421. * 暂停指定时间函数
  422. * @param ms 延迟时间
  423. * @returns
  424. */
  425. export const pause = (ms: number = 1000 / 30) => {
  426. return new AbortablePromise((resolve) => {
  427. const timer = setTimeout(() => {
  428. clearTimeout(timer)
  429. resolve(true)
  430. }, ms)
  431. })
  432. }
  433. /**
  434. * 深拷贝函数,用于将对象进行完整复制。
  435. * @param obj 要深拷贝的对象
  436. * @param cache 用于缓存已复制的对象,防止循环引用
  437. * @returns 深拷贝后的对象副本
  438. */
  439. export function deepClone<T>(obj: T, cache: Map<any, any> = new Map()): T {
  440. // 如果对象为 null 或或者不是对象类型,则直接返回该对象
  441. if (obj === null || typeof obj !== 'object') {
  442. return obj
  443. }
  444. // 处理特殊对象类型:日期、正则表达式、错误对象
  445. if (isDate(obj)) {
  446. return new Date(obj.getTime()) as any
  447. }
  448. if (obj instanceof RegExp) {
  449. return new RegExp(obj.source, obj.flags) as any
  450. }
  451. if (obj instanceof Error) {
  452. const errorCopy = new Error(obj.message) as any
  453. errorCopy.stack = obj.stack
  454. return errorCopy
  455. }
  456. // 检查缓存中是否已存在该对象的复制
  457. if (cache.has(obj)) {
  458. return cache.get(obj)
  459. }
  460. // 根据原始对象的类型创建对应的空对象或数组
  461. const copy: any = Array.isArray(obj) ? [] : {}
  462. // 将当前对象添加到缓存中
  463. cache.set(obj, copy)
  464. // 递归地深拷贝对象的每个属性
  465. for (const key in obj) {
  466. if (Object.prototype.hasOwnProperty.call(obj, key)) {
  467. copy[key] = deepClone(obj[key], cache)
  468. }
  469. }
  470. return copy as T
  471. }
  472. /**
  473. * 深度合并两个对象。
  474. * @param target 目标对象,将合并的结果存放在此对象中
  475. * @param source 源对象,要合并到目标对象的对象
  476. * @returns 合并后的目标对象
  477. */
  478. export function deepMerge<T extends Record<string, any>>(target: T, source: Record<string, any>): T {
  479. // 深拷贝目标对象,避免修改原始对象
  480. target = deepClone(target)
  481. // 检查目标和源是否都是对象类型
  482. if (typeof target !== 'object' || typeof source !== 'object') {
  483. throw new Error('Both target and source must be objects.')
  484. }
  485. // 遍历源对象的属性
  486. for (const prop in source) {
  487. // eslint-disable-next-line no-prototype-builtins
  488. if (!source.hasOwnProperty(prop))
  489. continue
  490. // 使用类型断言,告诉 TypeScript 这是有效的属性
  491. ;(target as Record<string, any>)[prop] = source[prop]
  492. }
  493. return target
  494. }
  495. /**
  496. * 深度合并两个对象。
  497. * @param target
  498. * @param source
  499. * @returns
  500. */
  501. export function deepAssign(target: Record<string, any>, source: Record<string, any>): Record<string, any> {
  502. Object.keys(source).forEach((key) => {
  503. const targetValue = target[key]
  504. const newObjValue = source[key]
  505. if (isObj(targetValue) && isObj(newObjValue)) {
  506. deepAssign(targetValue, newObjValue)
  507. } else {
  508. target[key] = newObjValue
  509. }
  510. })
  511. return target
  512. }
  513. /**
  514. * 构建带参数的URL
  515. * @param baseUrl 基础URL
  516. * @param params 参数对象,键值对表示要添加到URL的参数
  517. * @returns 返回带有参数的URL
  518. */
  519. export function buildUrlWithParams(baseUrl: string, params: Record<string, string>) {
  520. // 将参数对象转换为查询字符串
  521. const queryString = Object.entries(params)
  522. .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
  523. .join('&')
  524. // 检查基础URL是否已包含查询字符串,并选择适当的分隔符
  525. const separator = baseUrl.includes('?') ? '&' : '?'
  526. // 返回带有参数的URL
  527. return `${baseUrl}${separator}${queryString}`
  528. }
  529. type DebounceOptions = {
  530. leading?: boolean // 是否在延迟时间开始时调用函数
  531. trailing?: boolean // 是否在延迟时间结束时调用函数
  532. }
  533. export function debounce<T extends (...args: any[]) => any>(func: T, wait: number, options: DebounceOptions = {}): T {
  534. let timeoutId: ReturnType<typeof setTimeout> | null = null
  535. let lastArgs: any[] | undefined
  536. let lastThis: any
  537. let result: ReturnType<T> | undefined
  538. const leading = isDef(options.leading) ? options.leading : false
  539. const trailing = isDef(options.trailing) ? options.trailing : true
  540. function invokeFunc() {
  541. if (lastArgs !== undefined) {
  542. result = func.apply(lastThis, lastArgs)
  543. lastArgs = undefined
  544. }
  545. }
  546. function startTimer() {
  547. timeoutId = setTimeout(() => {
  548. timeoutId = null
  549. if (trailing) {
  550. invokeFunc()
  551. }
  552. }, wait)
  553. }
  554. function cancelTimer() {
  555. if (timeoutId !== null) {
  556. clearTimeout(timeoutId)
  557. timeoutId = null
  558. }
  559. }
  560. function debounced(this: any, ...args: Parameters<T>): ReturnType<T> | undefined {
  561. lastArgs = args
  562. lastThis = this
  563. if (timeoutId === null) {
  564. if (leading) {
  565. invokeFunc()
  566. }
  567. startTimer()
  568. } else if (trailing) {
  569. cancelTimer()
  570. startTimer()
  571. }
  572. return result
  573. }
  574. return debounced as T
  575. }
  576. // eslint-disable-next-line @typescript-eslint/ban-types
  577. export function throttle(func: Function, wait: number): Function {
  578. let timeout: ReturnType<typeof setTimeout> | null = null
  579. let previous: number = 0
  580. const throttled = function (this: any, ...args: any[]) {
  581. const now = Date.now()
  582. const remaining = wait - (now - previous)
  583. if (remaining <= 0) {
  584. if (timeout) {
  585. clearTimeout(timeout)
  586. timeout = null
  587. }
  588. previous = now
  589. func.apply(this, args)
  590. } else if (!timeout) {
  591. timeout = setTimeout(() => {
  592. previous = Date.now()
  593. timeout = null
  594. func.apply(this, args)
  595. }, remaining)
  596. }
  597. }
  598. return throttled
  599. }
  600. /**
  601. * 根据属性路径获取对象中的属性值
  602. * @param obj 目标对象
  603. * @param path 属性路径,可以是字符串或字符串数组
  604. * @returns 属性值,如果属性不存在或中间的属性为 null 或 undefined,则返回 undefined
  605. */
  606. export const getPropByPath = (obj: any, path: string): any => {
  607. const keys: string[] = path.split('.')
  608. try {
  609. return keys.reduce((acc: any, key: string) => (acc !== undefined && acc !== null ? acc[key] : undefined), obj)
  610. } catch (error) {
  611. return undefined
  612. }
  613. }
  614. /**
  615. * 检查一个值是否为Date类型
  616. * @param val 要检查的值
  617. * @returns 如果值是Date类型,则返回true,否则返回false
  618. */
  619. export const isDate = (val: unknown): val is Date => Object.prototype.toString.call(val) === '[object Date]' && !Number.isNaN((val as Date).getTime())
  620. /**
  621. * 检查提供的URL是否为视频链接。
  622. * @param url 需要检查的URL字符串。
  623. * @returns 返回一个布尔值,如果URL是视频链接则为true,否则为false。
  624. */
  625. export function isVideoUrl(url: string): boolean {
  626. // 使用正则表达式匹配视频文件类型的URL
  627. const videoRegex = /\.(ogm|webm|ogv|asx|m4v|mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv|video)(?=$|[?#])/i
  628. return videoRegex.test(url)
  629. }
  630. /**
  631. * 检查提供的URL是否为图片URL。
  632. * @param url 待检查的URL字符串。
  633. * @returns 返回一个布尔值,如果URL是图片格式,则为true;否则为false。
  634. */
  635. export function isImageUrl(url: string): boolean {
  636. // 使用正则表达式匹配图片URL
  637. const imageRegex = /\.(xbm|tif|pjp|apng|svgz|jpeg|jpg|heif|ico|tiff|heic|pjpeg|avif|gif|png|svg|webp|jfif|bmp|dpg|image)(?=$|[?#])/i
  638. return imageRegex.test(url)
  639. }
  640. /**
  641. * 判断环境是否是H5
  642. */
  643. export const isH5 = (() => {
  644. let isH5 = false
  645. // #ifdef H5
  646. isH5 = true
  647. // #endif
  648. return isH5
  649. })()
  650. /**
  651. * 剔除对象中的某些属性
  652. * @param obj
  653. * @param predicate
  654. * @returns
  655. */
  656. export function omitBy<O extends Record<string, any>>(obj: O, predicate: (value: any, key: keyof O) => boolean): Partial<O> {
  657. const newObj = deepClone(obj)
  658. Object.keys(newObj).forEach((key) => predicate(newObj[key], key) && delete newObj[key]) // 遍历对象的键,删除值为不满足predicate的字段
  659. return newObj
  660. }
  661. /**
  662. * 缓动函数,用于在动画或过渡效果中根据时间参数计算当前值
  663. * @param t 当前时间,通常是从动画开始经过的时间
  664. * @param b 初始值,动画属性的初始值
  665. * @param c 变化量,动画属性的目标值与初始值的差值
  666. * @param d 持续时间,动画持续的总时间长度
  667. * @returns 计算出的当前值
  668. */
  669. export function easingFn(t: number = 0, b: number = 0, c: number = 0, d: number = 0): number {
  670. return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
  671. }
  672. /**
  673. * 从数组中寻找最接近目标值的元素
  674. *
  675. * @param arr 数组
  676. * @param target 目标值
  677. * @returns 最接近目标值的元素
  678. */
  679. export function closest(arr: number[], target: number) {
  680. return arr.reduce((prev, curr) => (Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev))
  681. }