From 862d176593a06275128e2b1c4be01b798cb245b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=8E=AB=E5=94=90=E5=B0=BC?= Date: Thu, 11 Jun 2026 23:33:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=A1=B9=E7=9B=AE=E6=95=B4?= =?UTF-8?q?=E4=BD=93=E9=87=8D=E6=9E=84=E4=B8=8E=E5=8A=9F=E8=83=BD=E5=AE=8C?= =?UTF-8?q?=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 配置开发环境接口地址 2. 重构工具函数目录结构,迁移防抖节流等工具到base目录 3. 统一页面布局样式为全屏居中布局 4. 新增大量通用工具函数:深克隆、深合并、uuid、时间格式化、随机数、数组打乱、剪贴板操作等 5. 优化自定义tabbar实现,修复交互逻辑 6. 新增列表数据管理hook和请求loading封装 7. 移除无用测试文件和冗余代码 8. 完善首页内容,增加轮播图、作者卡片、金刚区等模块 --- env/.env.development | 2 +- src/hooks/useListData.ts | 99 +++++ src/hooks/useRequest.ts | 20 +- .../page-template/page-template.vue | 2 +- src/pages/auth/login.vue | 8 +- src/pages/auth/register.vue | 6 +- src/pages/gallery/gallery.vue | 2 +- src/pages/index/index.vue | 274 +++++++++++- src/pages/me/me.vue | 2 +- src/pages/moments/moments.vue | 2 +- src/tabbar/TabbarItem.test.ts | 69 --- src/tabbar/TabbarItem.vue | 69 ++- src/tabbar/config.ts | 27 +- src/tabbar/index.vue | 99 ++--- src/utils/base/clipboard.ts | 21 + src/utils/base/colorGradient.ts | 146 ++++++ src/utils/{ => base}/debounce.ts | 0 src/utils/base/deepClone.ts | 47 ++ src/utils/base/deepMerge.ts | 41 ++ src/utils/base/getRect.ts | 29 ++ src/utils/base/guid.ts | 44 ++ src/utils/base/md5.ts | 414 ++++++++++++++++++ src/utils/base/random.ts | 17 + src/utils/base/randomArray.ts | 11 + src/utils/base/styleUtils.ts | 90 ++++ src/utils/base/test.ts | 249 +++++++++++ src/utils/base/throttle.ts | 32 ++ src/utils/base/timeFormat.ts | 59 +++ src/utils/base/timeFrom.ts | 52 +++ src/utils/base/trim.ts | 25 ++ src/utils/debounce.test.ts | 59 --- src/utils/randomResources.ts | 19 + src/utils/toLoginPage.ts | 2 +- 33 files changed, 1787 insertions(+), 251 deletions(-) create mode 100644 src/hooks/useListData.ts delete mode 100644 src/tabbar/TabbarItem.test.ts create mode 100644 src/utils/base/clipboard.ts create mode 100644 src/utils/base/colorGradient.ts rename src/utils/{ => base}/debounce.ts (100%) create mode 100644 src/utils/base/deepClone.ts create mode 100644 src/utils/base/deepMerge.ts create mode 100644 src/utils/base/getRect.ts create mode 100644 src/utils/base/guid.ts create mode 100644 src/utils/base/md5.ts create mode 100644 src/utils/base/random.ts create mode 100644 src/utils/base/randomArray.ts create mode 100644 src/utils/base/styleUtils.ts create mode 100644 src/utils/base/test.ts create mode 100644 src/utils/base/throttle.ts create mode 100644 src/utils/base/timeFormat.ts create mode 100644 src/utils/base/timeFrom.ts create mode 100644 src/utils/base/trim.ts delete mode 100644 src/utils/debounce.test.ts create mode 100644 src/utils/randomResources.ts diff --git a/env/.env.development b/env/.env.development index 6619c32..287405a 100644 --- a/env/.env.development +++ b/env/.env.development @@ -6,4 +6,4 @@ VITE_DELETE_CONSOLE=false VITE_SHOW_SOURCEMAP=false # development mode 后台请求地址 -VITE_SERVER_BASEURL='' +VITE_SERVER_BASEURL='https://www.yijunzhao.cn' diff --git a/src/hooks/useListData.ts b/src/hooks/useListData.ts new file mode 100644 index 0000000..8f65833 --- /dev/null +++ b/src/hooks/useListData.ts @@ -0,0 +1,99 @@ +import type { Ref } from 'vue' + +interface IPaginationParams { + page: number + size: number +} + +interface IUpdateDataOptions { + /** 错误信息 */ + error: boolean | Error + /** 是否还有更多数据 */ + hasNext: boolean + /** 列表数据 */ + items: T[] +} + +interface IUseListDataOptions { + params: IPaginationParams + /** 加载状态 */ + loading: Ref + onPullDownRefresh?: (params: IPaginationParams, updateData: (data: IUpdateDataOptions) => void) => void + onReachBottom?: (params: IPaginationParams, updateData: (data: IUpdateDataOptions) => void) => void +} + +/** + * useListData是一个定制化的列表数据管理钩子,用于处理列表数据的添加和刷新。 + * @param T 列表数据类型,默认为any。 + * @returns 返回一个对象{list, append},包含列表数据和数据操作方法。 + */ +export function useListData(options: IUseListDataOptions) { + const _hasNext = ref(false) + const _error = ref() + const _items = shallowRef([]) + + /** 是否追加数据 */ + const isAppend = ref(false) + /** 列表数据 */ + const list = shallowRef([]) + + /** + * 数据操作方法 + * @param data 新数据数组 + */ + function append(data: T[]) { + if (isAppend.value) { + list.value.push(...data) + return + } + list.value = data + } + + function _updateData(data: IUpdateDataOptions) { + _error.value = data.error + _hasNext.value = data.hasNext + _items.value = data.items + uni.stopPullDownRefresh() + } + + function refresh() { + console.log('刷新数据', options) + if (options.loading.value) { + return + } + isAppend.value = false + options.params.page = 1 + options.onPullDownRefresh?.(options.params, _updateData) + } + + /** 下拉刷新 */ + onPullDownRefresh(refresh) + + /** 上拉加载更多 */ + onReachBottom(() => { + if (options.loading.value) { + return + } + if (!_hasNext.value) { + return + } + options.params.page += 1 + isAppend.value = true + options.onReachBottom?.(options.params, _updateData) + }) + + /** 监听items变化 */ + watch(() => _items.value, (newVal) => { + console.log('监听数据变化', newVal) + console.log('监听数据变化', options) + if (_error.value) { + return + } + append(newVal) + }) + + return { + list, + refresh, + } +} diff --git a/src/hooks/useRequest.ts b/src/hooks/useRequest.ts index 8ac4bfe..76a016c 100644 --- a/src/hooks/useRequest.ts +++ b/src/hooks/useRequest.ts @@ -6,6 +6,12 @@ interface IUseRequestOptions { immediate?: boolean /** 初始化数据 */ initialData?: T + /** 是否显示加载提示 */ + loadingToast?: boolean + /** 加载提示内容 */ + loadingToastContent?: string + /** 加载提示是否显示mask */ + loadingToastMask?: boolean } interface IUseRequestReturn { @@ -21,17 +27,26 @@ interface IUseRequestReturn { * @param options 包含请求选项的对象 {immediate, initialData}。 * @param options.immediate 是否立即执行请求,默认为false。 * @param options.initialData 初始化数据,默认为undefined。 + * @param options.loadingToast 是否显示加载提示,默认为false。 + * @param options.loadingToastContent 加载提示内容,默认为'加载中'。 + * @param options.loadingToastMask 加载提示是否显示mask,默认为false。 * @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。 */ export default function useRequest( func: (args?: P) => Promise, - options: IUseRequestOptions = { immediate: false }, + options: IUseRequestOptions = { immediate: false, loadingToast: false, loadingToastContent: '加载中', loadingToastMask: false }, ): IUseRequestReturn { const loading = ref(false) const error = ref(false) const data = ref(options.initialData) as Ref const run = async (args?: P) => { loading.value = true + if (options.loadingToast) { + uni.showLoading({ + title: options.loadingToastContent, + mask: options.loadingToastMask, + }) + } return func(args) .then((res) => { data.value = res @@ -44,6 +59,9 @@ export default function useRequest( }) .finally(() => { loading.value = false + if (options.loadingToast) { + uni.hideLoading() + } }) } diff --git a/src/pages-blog/page-template/page-template.vue b/src/pages-blog/page-template/page-template.vue index e8c20b7..7835ce2 100644 --- a/src/pages-blog/page-template/page-template.vue +++ b/src/pages-blog/page-template/page-template.vue @@ -11,7 +11,7 @@ definePage({ diff --git a/src/pages/auth/login.vue b/src/pages/auth/login.vue index bef160c..ecdfe74 100644 --- a/src/pages/auth/login.vue +++ b/src/pages/auth/login.vue @@ -16,7 +16,7 @@ async function doLogin() { try { // 调用登录接口 await tokenStore.login({ - username: '菲鸽', + username: '小莫唐尼', password: '123456', }) uni.navigateBack() @@ -28,7 +28,7 @@ async function doLogin() { - - diff --git a/src/pages/auth/register.vue b/src/pages/auth/register.vue index e9809f0..767097c 100644 --- a/src/pages/auth/register.vue +++ b/src/pages/auth/register.vue @@ -19,7 +19,7 @@ function doRegister() { - - diff --git a/src/pages/gallery/gallery.vue b/src/pages/gallery/gallery.vue index 0da5668..e205497 100644 --- a/src/pages/gallery/gallery.vue +++ b/src/pages/gallery/gallery.vue @@ -11,7 +11,7 @@ definePage({ diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index a29de76..484ec23 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -1,4 +1,12 @@ + + diff --git a/src/pages/me/me.vue b/src/pages/me/me.vue index 9c5f142..3deff25 100644 --- a/src/pages/me/me.vue +++ b/src/pages/me/me.vue @@ -57,7 +57,7 @@ function handleLogout() { + + diff --git a/src/tabbar/config.ts b/src/tabbar/config.ts index df455e8..beddcd9 100644 --- a/src/tabbar/config.ts +++ b/src/tabbar/config.ts @@ -43,19 +43,10 @@ export const customTabbarList: CustomTabBarItem[] = [ { text: '%tabbar.home%', pagePath: 'pages/index/index', - // 注意 unocss 图标需要如下处理:(二选一) - // 2)配置到 unocss.config.ts 的 safelist 中 iconType: 'unocss', icon: 'i-carbon-home', // badge: 'dot', }, - { - pagePath: 'pages-demo/i18n/index', - text: '%tabbar.i18n%', - iconType: 'unocss', - icon: 'i-carbon-ibm-watson-language-translator', - // badge: 10, - }, { pagePath: 'pages/gallery/gallery', text: '%tabbar.gallery%', @@ -68,16 +59,14 @@ export const customTabbarList: CustomTabBarItem[] = [ iconType: 'uiLib', icon: 'gift', }, - { - pagePath: 'pages-blog/about/about', - text: '%tabbar.about%', - // 1)在fg-tabbar.vue页面上引入一下并注释掉(见tabbar/index.vue代码第2行) - // 2)配置到 unocss.config.ts 的 safelist 中 - iconType: 'unocss', - icon: 'i-carbon-menu', - // badge: 10, - roles: ['admin'], - }, + // { + // pagePath: 'pages-blog/about/about', + // text: '%tabbar.about%', + // iconType: 'unocss', + // icon: 'i-carbon-menu', + // badge: 10, + // roles: ['admin'], + // }, { pagePath: 'pages/me/me', text: '%tabbar.me%', diff --git a/src/tabbar/index.vue b/src/tabbar/index.vue index a709b72..afad865 100644 --- a/src/tabbar/index.vue +++ b/src/tabbar/index.vue @@ -1,7 +1,6 @@ + + diff --git a/src/utils/base/clipboard.ts b/src/utils/base/clipboard.ts new file mode 100644 index 0000000..27b8fb4 --- /dev/null +++ b/src/utils/base/clipboard.ts @@ -0,0 +1,21 @@ +/** + * 复制文本到剪贴板 + * @param {string} content 要复制的文本 + * @param {boolean} showToast 是否显示复制成功提示 + */ +export function copy(content: string, showToast: boolean = false) { + uni.setClipboardData({ + data: content, + showToast: false, + success: () => { + if (showToast) { + uni.showToast({ + title: '复制成功', + icon: 'success', + }) + } + }, + }) +} + +export default copy diff --git a/src/utils/base/colorGradient.ts b/src/utils/base/colorGradient.ts new file mode 100644 index 0000000..a8c5784 --- /dev/null +++ b/src/utils/base/colorGradient.ts @@ -0,0 +1,146 @@ +/** + * 求两个颜色之间的渐变值 + * @param startColor 开始的颜色 + * @param endColor 结束的颜色 + * @param step 颜色等分的份额 + * @returns 渐变色数组 + */ +export function colorGradient( + startColor: string = 'rgb(0, 0, 0)', + endColor: string = 'rgb(255, 255, 255)', + step: number = 10, +): string[] { + const startRGB = hexToRgb(startColor, false) as [number, number, number] // 转换为rgb数组模式 + const [startR, startG, startB] = startRGB + const endRGB = hexToRgb(endColor, false) as [number, number, number] + const [endR, endG, endB] = endRGB + + const sR = (endR - startR) / step // 总差值 + const sG = (endG - startG) / step + const sB = (endB - startB) / step + const colorArr: string[] = [] + for (let i = 0; i < step; i++) { + // 计算每一步的hex值 + const hex = rgbToHex( + `rgb(${Math.round(sR * i + startR)},${Math.round(sG * i + startG)},${Math.round(sB * i + startB)})`, + ) + colorArr.push(hex as string) + } + return colorArr +} + +/** + * 将hex表示方式转换为rgb表示方式(返回rgb数组或字符串) + * @param sColor hex或rgb字符串 + * @param str 是否返回字符串 + * @returns rgb数组或字符串 + */ +export function hexToRgb(sColor: string, str: boolean = true): [number, number, number] | string { + const reg = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i + sColor = sColor.toLowerCase() + if (sColor && reg.test(sColor)) { + if (sColor.length === 4) { + let sColorNew = '#' + for (let i = 1; i < 4; i += 1) { + sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1)) + } + sColor = sColorNew + } + // 处理六位的颜色值 + const sColorChange: [number, number, number] = [ + Number.parseInt(`0x${sColor.slice(1, 3)}`), + Number.parseInt(`0x${sColor.slice(3, 5)}`), + Number.parseInt(`0x${sColor.slice(5, 7)}`), + ] + if (!str) { + return sColorChange + } + else { + return `rgb(${sColorChange[0]},${sColorChange[1]},${sColorChange[2]})` + } + } + else if (/^(?:rgb|RGB)/.test(sColor)) { + const arr = sColor.replace(/(?:\(|\)|rgb|RGB)*/g, '').split(',') + return arr.map(val => Number(val)) as [number, number, number] + } + else { + return sColor + } +} + +/** + * rgb转hex + * @param rgb rgb字符串或hex字符串 + * @returns hex字符串 + */ +export function rgbToHex(rgb: string): string | undefined { + const reg = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i + if (/^(?:rgb|RGB)/.test(rgb)) { + const aColor = rgb.replace(/(?:\(|\)|rgb|RGB)*/g, '').split(',') + let strHex = '#' + for (let i = 0; i < aColor.length; i++) { + let hex = Number(aColor[i]).toString(16) + hex = hex.length === 1 ? `0${hex}` : hex // 保证每个rgb的值为2位 + strHex += hex + } + if (strHex.length !== 7) { + strHex = rgb + } + return strHex + } + else if (reg.test(rgb)) { + const aNum = rgb.replace(/#/, '').split('') + if (aNum.length === 6) { + return rgb + } + else if (aNum.length === 3) { + let numHex = '#' + for (let i = 0; i < aNum.length; i += 1) { + numHex += aNum[i] + aNum[i] + } + return numHex + } + } + else { + return rgb + } + // 默认返回原始值 + return rgb +} + +/** + * JS颜色十六进制转换为rgb或rgba,返回的格式为 rgba(255,255,255,0.5)字符串 + * @param color 十六进制色值或rgb字符串 + * @param alpha 透明度 + * @returns rgba字符串 + */ +function colorToRgba(color: string, alpha: number = 0.3): string { + color = rgbToHex(color) as string // 确保是hex格式 + const reg = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i + let sColor = color.toLowerCase() + if (sColor && reg.test(sColor)) { + if (sColor.length === 4) { + let sColorNew = '#' + for (let i = 1; i < 4; i += 1) { + sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1)) + } + sColor = sColorNew + } + const sColorChange: [number, number, number] = [ + Number.parseInt(`0x${sColor.slice(1, 3)}`), + Number.parseInt(`0x${sColor.slice(3, 5)}`), + Number.parseInt(`0x${sColor.slice(5, 7)}`), + ] + return `rgba(${sColorChange.join(',')},${alpha})` + } + else { + return sColor + } +} + +export default { + colorGradient, + hexToRgb, + rgbToHex, + colorToRgba, +} diff --git a/src/utils/debounce.ts b/src/utils/base/debounce.ts similarity index 100% rename from src/utils/debounce.ts rename to src/utils/base/debounce.ts diff --git a/src/utils/base/deepClone.ts b/src/utils/base/deepClone.ts new file mode 100644 index 0000000..4624489 --- /dev/null +++ b/src/utils/base/deepClone.ts @@ -0,0 +1,47 @@ +// 判断arr是否为一个数组,返回一个bool值 +export function isArray(arr: any): arr is any[] { + return Object.prototype.toString.call(arr) === '[object Array]' +} + +/** + * 深度克隆 + * @param obj 需要克隆的对象 + * @param cache 用于处理循环引用的 WeakMap + * @returns 克隆后的对象 + */ +export function deepClone(obj: T, cache: WeakMap = new WeakMap()): T { + if (obj === null || typeof obj !== 'object') + return obj + if (cache.has(obj)) + return cache.get(obj) + let clone: any + if (obj instanceof Date) { + clone = new Date(obj.getTime()) + } + else if (obj instanceof RegExp) { + clone = new RegExp(obj) + } + else if (obj instanceof Map) { + clone = new Map(Array.from(obj, ([key, value]) => [key, deepClone(value, cache)])) + } + else if (obj instanceof Set) { + clone = new Set(Array.from(obj, value => deepClone(value, cache))) + } + else if (Array.isArray(obj)) { + clone = obj.map(value => deepClone(value, cache)) + } + else if (Object.prototype.toString.call(obj) === '[object Object]') { + clone = Object.create(Object.getPrototypeOf(obj)) + cache.set(obj, clone) + for (const [key, value] of Object.entries(obj)) { + clone[key] = deepClone(value, cache) + } + } + else { + clone = Object.assign({}, obj) + } + cache.set(obj, clone) + return clone +} + +export default deepClone diff --git a/src/utils/base/deepMerge.ts b/src/utils/base/deepMerge.ts new file mode 100644 index 0000000..a2d9ca1 --- /dev/null +++ b/src/utils/base/deepMerge.ts @@ -0,0 +1,41 @@ +import deepClone from './deepClone' + +/** + * JS对象深度合并 + * @param target 目标对象 + * @param source 源对象 + * @returns 合并后的对象 + */ +export function deepMerge(target: T = {} as T, source: S = {} as S): T & S { + target = deepClone(target) + if (typeof target !== 'object' || target === null || typeof source !== 'object' || source === null) + return target as T & S + const merged: any = Array.isArray(target) ? target.slice() : Object.assign({}, target) + for (const prop in source) { + if (!Object.prototype.hasOwnProperty.call(source, prop)) + continue + const sourceValue = (source as any)[prop] + const targetValue = merged[prop] + if (sourceValue instanceof Date) { + merged[prop] = new Date(sourceValue) + } + else if (sourceValue instanceof RegExp) { + merged[prop] = new RegExp(sourceValue) + } + else if (sourceValue instanceof Map) { + merged[prop] = new Map(sourceValue) + } + else if (sourceValue instanceof Set) { + merged[prop] = new Set(sourceValue) + } + else if (typeof sourceValue === 'object' && sourceValue !== null) { + merged[prop] = deepMerge(targetValue, sourceValue) + } + else { + merged[prop] = sourceValue + } + } + return merged as T & S +} + +export default deepMerge diff --git a/src/utils/base/getRect.ts b/src/utils/base/getRect.ts new file mode 100644 index 0000000..393530e --- /dev/null +++ b/src/utils/base/getRect.ts @@ -0,0 +1,29 @@ +/** + * 获取元素的位置信息 + * @param {any} selector 选择器 + * @param {boolean} all 是否获取所有匹配元素 + * @returns {Promise} 返回一个 Promise,解析为元素的位置信息 + */ + +import { getCurrentInstance } from 'vue' + +export function getRect(selector: any, _instance: any = null, all: boolean = false): Promise { + const instance = _instance || getCurrentInstance() + return new Promise((resolve) => { + const query = uni.createSelectorQuery() + .in(instance?.proxy) + + query[all ? 'selectAll' : 'select'](selector) + .boundingClientRect((rect: any) => { + if (all && Array.isArray(rect) && rect.length) { + resolve(rect) + } + if (!all && rect) { + resolve(rect) + } + }) + .exec() + }) +} + +export default getRect diff --git a/src/utils/base/guid.ts b/src/utils/base/guid.ts new file mode 100644 index 0000000..13a25a4 --- /dev/null +++ b/src/utils/base/guid.ts @@ -0,0 +1,44 @@ +/** + * 本算法来源于简书开源代码,详见:https://www.jianshu.com/p/fdbf293d0a85 + * 全局唯一标识符(uuid,Globally Unique Identifier),也称作 uuid(Universally Unique IDentifier) + * 一般用于多个组件之间,给它一个唯一的标识符,或者v-for循环的时候,如果使用数组的index可能会导致更新列表出现问题 + * 最可能的情况是左滑删除item或者对某条信息流"不喜欢"并去掉它的时候,会导致组件内的数据可能出现错乱 + * v-for的时候,推荐使用后端返回的id而不是循环的index + * @param len uuid的长度,默认32 + * @param firstU 将返回的首字母置为"u",默认true + * @param radix 生成uuid的基数(意味着返回的字符串都是这个基数),2-二进制,8-八进制,10-十进制,16-十六进制 + * @returns 生成的uuid字符串 + */ +function guid(len: number = 32, firstU: boolean = true, radix?: number): string { + const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('') + const uuid: string[] = [] + const base = radix || chars.length + + if (len) { + // 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位 + for (let i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * base)] + } + else { + let r: number + // rfc4122标准要求返回的uuid中,某些位为固定的字符 + uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-' + uuid[14] = '4' + + for (let i = 0; i < 36; i++) { + if (!uuid[i]) { + r = 0 | (Math.random() * 16) + uuid[i] = chars[i === 19 ? (r & 0x3) | 0x8 : r] + } + } + } + // 移除第一个字符,并用u替代,因为第一个字符为数值时,该guid不能用作id或者class + if (firstU) { + uuid.shift() + return `u${uuid.join('')}` + } + else { + return uuid.join('') + } +} + +export default guid diff --git a/src/utils/base/md5.ts b/src/utils/base/md5.ts new file mode 100644 index 0000000..65b95f8 --- /dev/null +++ b/src/utils/base/md5.ts @@ -0,0 +1,414 @@ +/* + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ +let hexcase: number = 0 /* hex output format. 0 - lowercase; 1 - uppercase */ +let b64pad: string = '' /* base-64 pad character. "=" for strict RFC compliance */ + +/** + * 这些是通常需要调用的函数 + * @param s 输入字符串 + * @returns 十六进制/BASE64/任意编码的MD5字符串 + */ +function hex_md5(s: string): string { + return rstr2hex(rstr_md5(str2rstr_utf8(s))) +} +function b64_md5(s: string): string { + return rstr2b64(rstr_md5(str2rstr_utf8(s))) +} +function any_md5(s: string, e: string): string { + return rstr2any(rstr_md5(str2rstr_utf8(s)), e) +} +function hex_hmac_md5(k: string, d: string): string { + return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))) +} +function b64_hmac_md5(k: string, d: string): string { + return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))) +} +function any_hmac_md5(k: string, d: string, e: string): string { + return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e) +} + +/** + * 执行简单自测,判断 VM 是否正常 + * @returns 是否通过测试 + */ +function md5_vm_test(): boolean { + return hex_md5('abc').toLowerCase() === '900150983cd24fb0d6963f7d28e17f72' +} + +/** + * 计算原始字符串的 MD5 + * @param s 原始字符串 + * @returns MD5 结果字符串 + */ +function rstr_md5(s: string): string { + return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)) +} + +/** + * 计算 HMAC-MD5 + * @param key 密钥 + * @param data 数据 + * @returns HMAC-MD5 结果字符串 + */ +function rstr_hmac_md5(key: string, data: string): string { + let bkey: number[] = rstr2binl(key) + if (bkey.length > 16) + bkey = binl_md5(bkey, key.length * 8) + + const ipad: number[] = Array.from({ length: 16 }) + const opad: number[] = Array.from({ length: 16 }) + for (let i = 0; i < 16; i++) { + ipad[i] = bkey[i] ^ 0x36363636 + opad[i] = bkey[i] ^ 0x5C5C5C5C + } + + const hash: number[] = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8) + return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)) +} + +/** + * 原始字符串转十六进制字符串 + * @param input 输入字符串 + * @returns 十六进制字符串 + */ +function rstr2hex(input: string): string { + try { + void hexcase + } + catch (e) { + hexcase = 0 + } + const hex_tab: string = hexcase ? '0123456789ABCDEF' : '0123456789abcdef' + let output = '' + let x: number + for (let i = 0; i < input.length; i++) { + x = input.charCodeAt(i) + output += hex_tab.charAt((x >>> 4) & 0x0F) + hex_tab.charAt(x & 0x0F) + } + return output +} + +/** + * 原始字符串转 BASE64 字符串 + * @param input 输入字符串 + * @returns BASE64 字符串 + */ +function rstr2b64(input: string): string { + try { + void b64pad + } + catch (e) { + b64pad = '' + } + const tab: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + let output = '' + const len: number = input.length + for (let i = 0; i < len; i += 3) { + const triplet: number + = (input.charCodeAt(i) << 16) + | (i + 1 < len ? input.charCodeAt(i + 1) << 8 : 0) + | (i + 2 < len ? input.charCodeAt(i + 2) : 0) + for (let j = 0; j < 4; j++) { + if (i * 8 + j * 6 > input.length * 8) + output += b64pad + else output += tab.charAt((triplet >>> (6 * (3 - j))) & 0x3F) + } + } + return output +} + +/** + * 原始字符串转任意编码字符串 + * @param input 输入字符串 + * @param encoding 编码表 + * @returns 编码后的字符串 + */ +function rstr2any(input: string, encoding: string): string { + const divisor: number = encoding.length + let i: number, j: number, q: number, x: number, quotient: number[] + + /* Convert to an array of 16-bit big-endian values, forming the dividend */ + let dividend: number[] = Array.from({ length: Math.ceil(input.length / 2) }) + for (i = 0; i < dividend.length; i++) { + dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1) + } + + /* + * Repeatedly perform a long division. The binary array forms the dividend, + * the length of the encoding is the divisor. Once computed, the quotient + * forms the dividend for the next step. All remainders are stored for later + * use. + */ + const full_length: number = Math.ceil((input.length * 8) / (Math.log(encoding.length) / Math.log(2))) + const remainders: number[] = Array.from({ length: full_length }) + for (j = 0; j < full_length; j++) { + quotient = [] + x = 0 + for (i = 0; i < dividend.length; i++) { + x = (x << 16) + dividend[i] + q = Math.floor(x / divisor) + x -= q * divisor + if (quotient.length > 0 || q > 0) + quotient[quotient.length] = q + } + remainders[j] = x + dividend = quotient + } + + /* Convert the remainders to the output string */ + let output = '' + for (i = remainders.length - 1; i >= 0; i--) output += encoding.charAt(remainders[i]) + + return output +} + +/** + * 字符串转 UTF-8 编码 + * @param input 输入字符串 + * @returns UTF-8 编码字符串 + */ +function str2rstr_utf8(input: string): string { + let output = '' + let i = -1 + let x: number, y: number + + while (++i < input.length) { + /* Decode utf-16 surrogate pairs */ + x = input.charCodeAt(i) + y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0 + if (x >= 0xD800 && x <= 0xDBFF && y >= 0xDC00 && y <= 0xDFFF) { + x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF) + i++ + } + + /* Encode output as utf-8 */ + if (x <= 0x7F) { + output += String.fromCharCode(x) + } + else if (x <= 0x7FF) { + output += String.fromCharCode(0xC0 | ((x >>> 6) & 0x1F), 0x80 | (x & 0x3F)) + } + else if (x <= 0xFFFF) { + output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F), 0x80 | ((x >>> 6) & 0x3F), 0x80 | (x & 0x3F)) + } + else if (x <= 0x1FFFFF) { + output += String.fromCharCode( + 0xF0 | ((x >>> 18) & 0x07), + 0x80 | ((x >>> 12) & 0x3F), + 0x80 | ((x >>> 6) & 0x3F), + 0x80 | (x & 0x3F), + ) + } + } + return output +} + +/** + * 字符串转 UTF-16LE 编码 + * @param input 输入字符串 + * @returns UTF-16LE 编码字符串 + */ +function str2rstr_utf16le(input: string): string { + let output = '' + for (let i = 0; i < input.length; i++) + output += String.fromCharCode(input.charCodeAt(i) & 0xFF, (input.charCodeAt(i) >>> 8) & 0xFF) + return output +} + +/** + * 字符串转 UTF-16BE 编码 + * @param input 输入字符串 + * @returns UTF-16BE 编码字符串 + */ +function str2rstr_utf16be(input: string): string { + let output = '' + for (let i = 0; i < input.length; i++) + output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF, input.charCodeAt(i) & 0xFF) + return output +} + +/** + * 原始字符串转小端字数组 + * @param input 输入字符串 + * @returns 小端字数组 + */ +function rstr2binl(input: string): number[] { + const output: number[] = Array.from({ length: input.length >> 2 }) + for (let i = 0; i < output.length; i++) output[i] = 0 + for (let i = 0; i < input.length * 8; i += 8) output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << i % 32 + return output +} + +/** + * 小端字数组转字符串 + * @param input 小端字数组 + * @returns 字符串 + */ +function binl2rstr(input: number[]): string { + let output = '' + for (let i = 0; i < input.length * 32; i += 8) output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xFF) + return output +} + +/** + * 计算小端字数组的 MD5 + * @param x 小端字数组 + * @param len 位长度 + * @returns MD5 结果数组 + */ +function binl_md5(x: number[], len: number): number[] { + /* append padding */ + x[len >> 5] |= 0x80 << len % 32 + x[(((len + 64) >>> 9) << 4) + 14] = len + + let a = 1732584193 + let b = -271733879 + let c = -1732584194 + let d = 271733878 + + for (let i = 0; i < x.length; i += 16) { + const olda = a + const oldb = b + const oldc = c + const oldd = d + + a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936) + d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586) + c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819) + b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330) + a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897) + d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426) + c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341) + b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983) + a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416) + d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417) + c = md5_ff(c, d, a, b, x[i + 10], 17, -42063) + b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162) + a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682) + d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101) + c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290) + b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329) + + a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510) + d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632) + c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713) + b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302) + a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691) + d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083) + c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335) + b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848) + a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438) + d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690) + c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961) + b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501) + a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467) + d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784) + c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473) + b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734) + + a = md5_hh(a, b, c, d, x[i + 5], 4, -378558) + d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463) + c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562) + b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556) + a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060) + d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353) + c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632) + b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640) + a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174) + d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222) + c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979) + b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189) + a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487) + d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835) + c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520) + b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651) + + a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844) + d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415) + c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905) + b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055) + a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571) + d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606) + c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523) + b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799) + a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359) + d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744) + c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380) + b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649) + a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070) + d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379) + c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259) + b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551) + + a = safe_add(a, olda) + b = safe_add(b, oldb) + c = safe_add(c, oldc) + d = safe_add(d, oldd) + } + return [a, b, c, d] +} + +/** + * 四种基本操作 + */ +function md5_cmn(q: number, a: number, b: number, x: number, s: number, t: number): number { + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b) +} +function md5_ff(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number { + return md5_cmn((b & c) | (~b & d), a, b, x, s, t) +} +function md5_gg(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number { + return md5_cmn((b & d) | (c & ~d), a, b, x, s, t) +} +function md5_hh(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number { + return md5_cmn(b ^ c ^ d, a, b, x, s, t) +} +function md5_ii(a: number, b: number, c: number, d: number, x: number, s: number, t: number): number { + return md5_cmn(c ^ (b | ~d), a, b, x, s, t) +} + +/** + * 安全整数加法,防止溢出 + * @param x 整数 + * @param y 整数 + * @returns 加法结果 + */ +function safe_add(x: number, y: number): number { + const lsw: number = (x & 0xFFFF) + (y & 0xFFFF) + const msw: number = (x >> 16) + (y >> 16) + (lsw >> 16) + return (msw << 16) | (lsw & 0xFFFF) +} + +/** + * 左移位操作 + * @param num 数字 + * @param cnt 位数 + * @returns 结果 + */ +function bit_rol(num: number, cnt: number): number { + return (num << cnt) | (num >>> (32 - cnt)) +} + +/** + * 计算字符串的 MD5 值 + * @param str 输入字符串 + * @returns MD5 十六进制字符串 + */ +function md5(str: string): string { + return hex_md5(str) +} + +export default { + md5, +} diff --git a/src/utils/base/random.ts b/src/utils/base/random.ts new file mode 100644 index 0000000..b17002f --- /dev/null +++ b/src/utils/base/random.ts @@ -0,0 +1,17 @@ +/** + * 生成指定范围的随机整数 + * @param min 最小值(包含) + * @param max 最大值(包含) + * @returns 随机整数 + */ +export function random(min: number, max: number): number { + if (min >= 0 && max > 0 && max >= min) { + const gab = max - min + 1 + return Math.floor(Math.random() * gab + min) + } + else { + return 0 + } +} + +export default random diff --git a/src/utils/base/randomArray.ts b/src/utils/base/randomArray.ts new file mode 100644 index 0000000..464ea34 --- /dev/null +++ b/src/utils/base/randomArray.ts @@ -0,0 +1,11 @@ +/** + * 打乱数组顺序 + * @param array 需要打乱的数组 + * @returns 打乱后的新数组 + */ +export function randomArray(array: T[] = []): T[] { + // 原理是sort排序,Math.random()产生0<= x < 1之间的数,会导致x-0.5大于或者小于0 + return array.sort(() => Math.random() - 0.5) +} + +export default randomArray diff --git a/src/utils/base/styleUtils.ts b/src/utils/base/styleUtils.ts new file mode 100644 index 0000000..2c5017c --- /dev/null +++ b/src/utils/base/styleUtils.ts @@ -0,0 +1,90 @@ +import type { CSSProperties } from 'vue' +import trim from './trim' +import test from './test' + +export function isValidValue(value: any): boolean { + if (test.isEmpty(value)) + return false + if (typeof value === 'string') { + const trimmed = trim(value).toLowerCase() + return trimmed !== 'null' && trimmed !== 'undefined' && trimmed !== '' + } + return true +} + +/** + * 将 CSS 字符串解析为对象 + */ +export function cssStrToObj(str: string): object { + const result = {} + if (!isValidValue(str)) + return result + const styleStr = trim(String(str)) + if (!isValidValue(styleStr)) + return result + const declarations = styleStr.split(';') + declarations.forEach((decl) => { + const [prop, ...values] = decl.split(':').map(s => s.trim()) + const value = values.join(':') + if (prop && value) { + const camelProp = prop.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase()); + (result as Record)[camelProp] = value + } + }) + return result +} + +/** + * 将 CSS 对象转换为 CSS 字符串 + */ +export function cssObjToStr(obj: object): string { + if (!isValidValue(obj) || typeof obj !== 'object') + return '' + return ( + `${Object.entries(obj) + .map(([key, value]) => { + if (!isValidValue(value)) + return '' + // 驼峰转短横线 + const kebab = key.replace(/([A-Z])/g, '-$1').toLowerCase() + return `${kebab}: ${value}` + }) + .filter(Boolean) + .join('; ')};` + ) +} + +/** + * 合并多个 CSS 输入(对象或字符串),返回一个 CSS 对象 + */ +export function mergeStyles(...args: (object | string)[]): CSSProperties { + const result = {} as Record + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (!isValidValue(arg)) + return result as CSSProperties + if (typeof arg === 'string') { + Object.assign(result, cssStrToObj(arg)) + } + else if (test.object(arg)) { + const cleanedObj: Record = {} + Object.keys(arg).forEach((key) => { + const value = (arg as Record)[key] + if (isValidValue(value)) { + // 有效 + cleanedObj[key] = value + } + }) + Object.assign(result, cleanedObj) + } + } + return result +} + +/** + * 合并为 CSS 字符串 + */ +export function mergeCssStrings(...args: (object | string)[]): string { + const obj = mergeStyles(...args) + return cssObjToStr(obj) +} diff --git a/src/utils/base/test.ts b/src/utils/base/test.ts new file mode 100644 index 0000000..96a185f --- /dev/null +++ b/src/utils/base/test.ts @@ -0,0 +1,249 @@ +/** + * 验证电子邮箱格式 + */ +function email(value: string): boolean { + return /[\w!#$%&'*+/=?^`{|}~-]+(?:\.[\w!#$%&'*+/=?^`{|}~-]+)*@(?:\w(?:[\w-]*\w)?\.)+\w(?:[\w-]*\w)?/.test( + value, + ) +} + +/** + * 验证手机格式 + */ +function mobile(value: string): boolean { + return /^1[3-9]\d{9}$/.test(value) +} + +/** + * 验证日期格式 + */ +function date(value: string): boolean { + return !/Invalid|NaN/.test(new Date(value).toString()) +} + +/** + * 验证整数 + */ +function digits(value: string): boolean { + return /^\d+$/.test(value) +} + +/** + * 是否车牌号 + */ +function carNo(value: string): boolean { + // 新能源车牌 + const xreg + = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z][A-Z](?:\d{5}[DF]$|[DF][A-HJ-NP-Z0-9]\d{4}$)/ + // 旧车牌 + const creg + = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]$/ + if (value.length === 7) { + return creg.test(value) + } + else if (value.length === 8) { + return xreg.test(value) + } + else { + return false + } +} + +/** + * 中文 + */ +function chinese(value: string): boolean { + const reg = /^[\u4E00-\u9FA5]+$/ + return reg.test(value) +} + +/** + * 只能输入字母 + */ +function letter(value: string): boolean { + return /^[a-z]*$/i.test(value) +} + +/** + * 只能是字母或者数字 + */ +function enOrNum(value: string): boolean { + // 英文或者数字 + const reg = /^[0-9a-z]*$/i + return reg.test(value) +} + +/** + * 验证是否包含某个值 + */ +function contains(value: string, param: string): boolean { + return value.includes(param) +} + +/** + * 验证一个值范围[min, max] + */ +function range(value: number, param: [number, number]): boolean { + return value >= param[0] && value <= param[1] +} + +/** + * 验证一个长度范围[min, max] + */ +function rangeLength(value: string, param: [number, number]): boolean { + return value.length >= param[0] && value.length <= param[1] +} + +/** + * 判断是否为空 + */ +function empty(value: any): boolean { + switch (typeof value) { + case 'undefined': + return true + case 'string': + if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length === 0) + return true + break + case 'boolean': + if (!value) + return true + break + case 'number': + if (value === 0 || Number.isNaN(value)) + return true + break + case 'object': + if (value === null || value.length === 0) + return true + if (Object.keys(value).length > 0) { + return false + } + return true + } + return false +} + +/** + * 是否json字符串 + */ +function jsonString(value: string): boolean { + if (typeof value == 'string') { + try { + const obj = JSON.parse(value) + if (typeof obj == 'object' && obj) { + return true + } + else { + return false + } + } + catch (e) { + return false + } + } + return false +} + +/** + * 是否数组 + */ +function array(value: any): boolean { + if (typeof Array.isArray === 'function') { + return Array.isArray(value) + } + else { + return Object.prototype.toString.call(value) === '[object Array]' + } +} + +/** + * 是否对象 + */ +function object(value: any): boolean { + return Object.prototype.toString.call(value) === '[object Object]' +} + +/** + * 是否短信验证码 + */ +function code(value: string, len: number = 6): boolean { + return new RegExp(`^\\d{${len}}$`).test(value) +} + +/** + * 是否函数方法 + * @param {object} value + */ +function func(value: any) { + return typeof value === 'function' +} + +/** + * 是否promise对象 + * @param {object} value + */ +function promise(value: any) { + return object(value) && func(value.then) && func(value.catch) +} + +/** + * 是否图片格式 + * @param {object} value + */ +function image(value: any) { + const newValue = value.split('?')[0] + const IMAGE_REGEXP = /\.(?:jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i + return IMAGE_REGEXP.test(newValue) +} + +/** + * 是否视频格式 + * @param {object} value + */ +function video(value: any) { + const VIDEO_REGEXP = /\.(?:mp4|mpg|mpeg|dat|asf|avi|rm|mov|wmv|flv|mkv|m3u8)/i + return VIDEO_REGEXP.test(value) +} + +/** + * 是否为正则对象 + * @param {object} + * @return {boolean} + */ +function regExp(o: any) { + return o && Object.prototype.toString.call(o) === '[object RegExp]' +} + +/** + * 验证字符串 + */ +function string(value: any) { + return typeof value === 'string' +} + +export default { + email, + mobile, + date, + digits, + carNo, + chinese, + letter, + enOrNum, + contains, + range, + rangeLength, + empty, + isEmpty: empty, + jsonString, + object, + array, + code, + func, + promise, + video, + image, + regExp, + string, +} diff --git a/src/utils/base/throttle.ts b/src/utils/base/throttle.ts new file mode 100644 index 0000000..0073375 --- /dev/null +++ b/src/utils/base/throttle.ts @@ -0,0 +1,32 @@ +let timer: ReturnType | undefined +let flag: boolean | undefined + +/** + * 节流原理:在一定时间内,只能触发一次 + * @param func 要执行的回调函数 + * @param wait 延时的时间,单位ms,默认500 + * @param immediate 是否立即执行,默认true + */ +export function throttle(func: () => void, wait: number = 500, immediate: boolean = true): void { + if (immediate) { + if (!flag) { + flag = true + // 如果是立即执行,则在wait毫秒内开始时执行 + typeof func === 'function' && func() + timer = setTimeout(() => { + flag = false + }, wait) + } + } + else { + if (!flag) { + flag = true + // 如果是非立即执行,则在wait毫秒内的结束处执行 + timer = setTimeout(() => { + flag = false + typeof func === 'function' && func() + }, wait) + } + } +} +export default throttle diff --git a/src/utils/base/timeFormat.ts b/src/utils/base/timeFormat.ts new file mode 100644 index 0000000..401dbad --- /dev/null +++ b/src/utils/base/timeFormat.ts @@ -0,0 +1,59 @@ +// padStart 的 polyfill,因为某些机型或情况,还无法支持es7的padStart,比如电脑版的微信小程序 +// 所以这里做一个兼容polyfill的兼容处理 +if (!String.prototype.padStart) { + // 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解 + String.prototype.padStart = function (this: string, maxLength: number, fillString: string = ' '): string { + if (Object.prototype.toString.call(fillString) !== '[object String]') + throw new TypeError('fillString must be String') + if (this.length >= maxLength) { + return String(this) + } + const fillLength = maxLength - this.length + let times = Math.ceil(fillLength / fillString.length) + while (times) { + times >>= 1 + fillString += fillString + if (times === 1) { + fillString += fillString + } + } + return fillString.slice(0, fillLength) + this + } +} + +/** + * 时间格式化 + * @param dateTime 时间戳、Date对象或null,默认当前时间 + * @param fmt 格式化字符串,默认 'yyyy-mm-dd' + * @returns 格式化后的时间字符串 + */ +export function timeFormat(dateTime: number | string | Date | null = null, fmt: string = 'yyyy-mm-dd'): string { + // 如果为null,则格式化当前时间 + if (!dateTime) + dateTime = Number(new Date()) + // 如果dateTime长度为10或者13,则为秒和毫秒的时间戳,如果超过13位,则为其他的时间格式 + if (typeof dateTime === 'number' || typeof dateTime === 'string') { + if (dateTime.toString().length === 10) + dateTime = Number(dateTime) * 1000 + } + const date = new Date(dateTime) + let ret: RegExpExecArray | null + const opt: Record = { + 'y+': date.getFullYear().toString(), // 年 + 'm+': (date.getMonth() + 1).toString(), // 月 + 'd+': date.getDate().toString(), // 日 + 'h+': date.getHours().toString(), // 时 + 'M+': date.getMinutes().toString(), // 分 + 's+': date.getSeconds().toString(), // 秒 + // 有其他格式化字符需求可以继续添加,必须转化成字符串 + } + for (const k in opt) { + ret = new RegExp(`(${k})`).exec(fmt) + if (ret) { + fmt = fmt.replace(ret[1], ret[1].length === 1 ? opt[k] : opt[k].padStart(ret[1].length, '0')) + } + } + return fmt +} + +export default timeFormat diff --git a/src/utils/base/timeFrom.ts b/src/utils/base/timeFrom.ts new file mode 100644 index 0000000..3a4cb7f --- /dev/null +++ b/src/utils/base/timeFrom.ts @@ -0,0 +1,52 @@ +import timeFormat from './timeFormat' + +/** + * 时间戳转为多久之前 + * @param dateTime 时间戳、Date对象或null,默认当前时间 + * @param format 时间格式字符串或false,超出范围时返回指定格式,否则返回多久以前 + * @returns 格式化后的时间字符串 + */ +export function timeFrom(dateTime: number | string | Date | null = null, format: string | false = 'yyyy-mm-dd'): string { + // 如果为null,则格式化当前时间 + if (!dateTime) + dateTime = Number(new Date()) + // 如果dateTime长度为10或者13,则为秒和毫秒的时间戳,如果超过13位,则为其他的时间格式 + if (typeof dateTime === 'number' || typeof dateTime === 'string') { + if (dateTime.toString().length === 10) + dateTime = Number(dateTime) * 1000 + } + const timestamp = +new Date(Number(dateTime)) + const timer = (Number(new Date()) - timestamp) / 1000 + // 如果小于5分钟,则返回"刚刚",其他以此类推 + let tips = '' + switch (true) { + case timer < 300: + tips = '刚刚' + break + case timer >= 300 && timer < 3600: + tips = `${Number.parseInt(String(timer / 60))}分钟前` + break + case timer >= 3600 && timer < 86400: + tips = `${Number.parseInt(String(timer / 3600))}小时前` + break + case timer >= 86400 && timer < 2592000: + tips = `${Number.parseInt(String(timer / 86400))}天前` + break + default: + // 如果format为false,则无论什么时间戳,都显示xx之前 + if (format === false) { + if (timer >= 2592000 && timer < 365 * 86400) { + tips = `${Number.parseInt(String(timer / (86400 * 30)))}个月前` + } + else { + tips = `${Number.parseInt(String(timer / (86400 * 365)))}年前` + } + } + else { + tips = timeFormat(timestamp, format as string) + } + } + return tips +} + +export default timeFrom diff --git a/src/utils/base/trim.ts b/src/utils/base/trim.ts new file mode 100644 index 0000000..0760bfc --- /dev/null +++ b/src/utils/base/trim.ts @@ -0,0 +1,25 @@ +/** + * 去除字符串空格 + * @param str 输入字符串 + * @param pos 去除位置,'both' | 'left' | 'right' | 'all',默认'both' + * @returns 去除空格后的字符串 + */ +export function trim(str: string, pos: 'both' | 'left' | 'right' | 'all' = 'both'): string { + if (pos === 'both') { + return str.replace(/^\s+|\s+$/g, '') + } + else if (pos === 'left') { + return str.replace(/^\s*/, '') + } + else if (pos === 'right') { + return str.replace(/(\s*$)/g, '') + } + else if (pos === 'all') { + return str.replace(/\s+/g, '') + } + else { + return str + } +} + +export default trim diff --git a/src/utils/debounce.test.ts b/src/utils/debounce.test.ts deleted file mode 100644 index 8e71356..0000000 --- a/src/utils/debounce.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { debounce } from './debounce' - -describe('debounce', () => { - beforeEach(() => { - vi.useFakeTimers() - }) - - afterEach(() => { - vi.useRealTimers() - }) - - it('trailing edge(默认):多次调用后只触发一次,使用最后一次的参数', () => { - const fn = vi.fn() - const debouncedFn = debounce(fn, 100) - - debouncedFn('first') - debouncedFn('second') - debouncedFn('last') - - expect(fn).not.toHaveBeenCalled() - - vi.advanceTimersByTime(100) - - expect(fn).toHaveBeenCalledTimes(1) - expect(fn).toHaveBeenCalledWith('last') - }) - - it('cancel:取消后计时器到期不执行', () => { - const fn = vi.fn() - const debouncedFn = debounce(fn, 100) - - debouncedFn() - debouncedFn.cancel() - - vi.advanceTimersByTime(100) - - expect(fn).not.toHaveBeenCalled() - }) - - it('flush:立即执行待执行的调用并传递参数', () => { - const fn = vi.fn() - const debouncedFn = debounce(fn, 100) - - debouncedFn('arg1') - debouncedFn.flush() - - expect(fn).toHaveBeenCalledWith('arg1') - }) - - it('leading edge:第一次调用立即执行', () => { - const fn = vi.fn() - const debouncedFn = debounce(fn, 100, { edges: ['leading'] }) - - debouncedFn() - - expect(fn).toHaveBeenCalledTimes(1) - }) -}) diff --git a/src/utils/randomResources.ts b/src/utils/randomResources.ts new file mode 100644 index 0000000..6e0f6d8 --- /dev/null +++ b/src/utils/randomResources.ts @@ -0,0 +1,19 @@ +export const randomImageUrl = { + ycy: 'https://t.alcy.cc/ycy', + pc: 'https://t.alcy.cc/pc', + moemp: 'https://t.alcy.cc/moemp', + picsum: 'https://picsum.photos/800/600', + bing: 'https://bing.img.run/rand_uhd.php', + xsot: 'https://api.xsot.cn/bing?jump=true', +} + +export const randomVideosUrl = { + acg: 'https://t.alcy.cc/acg', +} + +export function getPicsum(width: number = 800, height?: number) { + if (!height) { + return `https://picsum.photos/${width}` + } + return `https://picsum.photos/${width}/${height}` +} diff --git a/src/utils/toLoginPage.ts b/src/utils/toLoginPage.ts index de2f8dd..4a3d912 100644 --- a/src/utils/toLoginPage.ts +++ b/src/utils/toLoginPage.ts @@ -1,5 +1,5 @@ import { getLastPage } from '@/utils' -import { debounce } from '@/utils/debounce' +import { debounce } from '@/utils/base/debounce' interface ToLoginPageOptions { /**