【HarmonyOS】工具函数封装 持续更迭...

221 阅读7分钟

在鸿蒙应用开发过程中,为了提高代码复用性和可维护性,通常会将一些通用的功能封装到工具类中。本文将介绍几个常见的工具类及其方法,包括字符串、数组、对象处理、日期格式化等。

1. 字符串处理工具函数 String

1.1 根据指定的空格截取类型对字符串进行空格处理

/**
 * 空格截取类型枚举,用于定义字符串中空格去除的方式。
 */
enum ESpaceTrimType {
  ALL, // 去除所有空格(包括前、后、中间)
  BEFORE, // 仅去除前面的空格
  AFTER, // 仅去除后面的空格
  BEFORE_AND_AFTER // 仅去除前后空格,保留中间空格
}

/**
 * @param {string} str - 需要去除空格的原始字符串。
 * @param {ESpaceTrimType} [trimType=ESpaceTrimType.BEFORE_AND_AFTER] 
 * 指定空格去除方式,默认为去除前后空格。
 *
 * @returns {string} 处理后的字符串,根据 trimType 移除了相应位置的空格。
 *
 * @example
 * SpaceTrim('  Hello World  ', ESpaceTrimType.ALL)
 * // 返回 'HelloWorld'
 *
 * SpaceTrim('  Hello World  ', ESpaceTrimType.BEFORE)
 * // 返回 'Hello World  '
 *
 * SpaceTrim('  Hello World  ', ESpaceTrimType.AFTER)
 * // 返回 '  Hello World'
 *
 * SpaceTrim('  Hello World  ', ESpaceTrimType.BEFORE_AND_AFTER)
 * // 返回 'Hello World'
 */
const SpaceTrim = (str: string, trimType: ESpaceTrimType = ESpaceTrimType.BEFORE_AND_AFTER):  string => {
   switch (trimType){
     case ESpaceTrimType.ALL:
       return str.replace(/\s+/g, '')
     case ESpaceTrimType.BEFORE:
       return str.replace(/(^\s*)/g, '')
     case ESpaceTrimType.AFTER:
       return str.replace(/(\s*$)/g, '')
     case ESpaceTrimType.BEFORE_AND_AFTER:
       return str.replace(/(^\s*)|(\s*$)/g, '')
   }
}

1.2 对目标字符串或数字进行脱敏处理,保留前后位数,中间部分用星号(*)代替

/**
 * 适用于手机号、身份证号、银行卡号等敏感信息的显示保护。
 *
 * @param {number | string} target - 需要脱敏的目标值,可以是数字或字符串形式的数字。
 * @param {number} [prefixLength=3] - 保留的前缀字符数,默认为 3。
 * @param {number} [suffixLength=4] - 保留的后缀字符数,默认为 4。
 *
 * @returns {string | number} 脱敏后的字符串;如果原始值为空或长度不足以脱敏,则返回原值。
 *
 * @example
 * Desensitization('13800001111') 
 * // 返回 '138******1111'
 *
 * Desensitization(123456789, 2, 2)
 * // 返回 '12****89'
 *
 * Desensitization('1234', 2, 3)
 * // 返回 '1234' (因为总长度小于 prefixLength + suffixLength)
 */
 
const Desensitization = (target: number | string, prefixLength: number = 3, suffixLength: number = 4) => {
  if (target) {
    let targetStr = target.toString()
    const prefix = targetStr.substring(0, prefixLength) // 前几位
    const suffix = targetStr.substring(targetStr.length - suffixLength) // 后几位
    if (targetStr.length > prefixLength + suffixLength) {
      const maskedPart = '*'.repeat(targetStr.length - prefixLength - suffixLength) // 中间部分用 * 替换
      return `${prefix}${maskedPart}${suffix}`
    }
  }
  return target
}

1.3 生成一个模拟 UUID 的随机字符串(遵循 UUID v4 格式规范)

/**
 * UUID v4 是基于随机数生成的唯一标识符,格式为:
 * `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`
 * 其中:
 * - 第3段以 `4` 开头(表示 UUID 版本 4)
 * - 第4段以 `8`, `9`, `a`, 或 `b` 开头(符合 RFC 4122 规范)
 *
 * @returns {string} 返回一个模拟 UUID v4 的随机字符串。
 *
 * @example
 * toAnyString()
 * // 返回类似 '6a2e8f50-a219-4d7d-bf8c-0a9d3e1f7b5c'
 */
function toAnyString() {
  const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => {
    const r: number = (Math.random() * 16) | 0
    const v: number = c === 'x' ? r : (r & 0x3) | 0x8
    return v.toString()
  })
  return str
}

1.4 将字符串中每个单词的首字母转换为大写,其余字母保持小写

/**
 * 适用于格式化标题、姓名、标签等需要首字母大写的场景。
 *
 * @param {string} str - 需要处理的原始字符串。
 * @returns {string} 每个单词首字母大写的新字符串。
 *
 * @example
 * firstUpperCase('hello world')
 * // 返回 'Hello World'
 *
 * firstUpperCase('THIS IS A TEST')
 * // 返回 'This Is A Test'
 */
function firstUpperCase(str: string) {
  return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())
}

1.5 十六进制随机颜色生成器

// 十六进制颜色生成器
const getRandomHexColor = (): string => {
  const randomColor = Math.floor(Math.random() * 0xffffff).toString(16);
  return `#${randomColor.padStart(6, '0')}`;
}

1.6 将十六进制颜色字符串(HEX)转换为 RGB 数值数组

/**
 * 支持标准格式如 `#FF5500` 和简写格式如 `#FFF`。
 * 转换后返回一个包含红、绿、蓝三个颜色通道数值的数组,每个数值范围为 0 到 255。
 *
 * @param {string} 【color】 
 *    十六进制颜色字符串。可以是 3 位(简写)或 6 位(标准),可选前缀 `#`。
 *    示例:'#FF5500'、'FF5500'、'#ABC'、'ABC'
 *
 * @returns {number[]} 
 *    包含三个数字元素的数组,分别代表红(Red)、绿(Green)、蓝(Blue)通道的值。
 *    示例:'#FF5500' → [255, 85, 0]、'#ABC' → [170, 187, 204]
 *
 * @throws {Error} 如果传入的颜色字符串不是 3 位或 6 位长度,则抛出错误。
 */
function getColorRGB(color: string): number[] {
  // 去除可能的 # 并转为小写
  let hex = color.replace(/^#/, '').toLowerCase();
  // 检查是否为合法长度(3 或 6)
  if (![3, 6].includes(hex.length)) {
    throw new Error('Invalid hex color format. Expected 3 or 6 characters.');
  }
  // 如果是3位简写,扩展为6位(例如 'f' -> 'ff')
  if (hex.length === 3) {
    hex = hex.replace(/./g, match => match + match);
  }
  // 使用正则将字符串按每两位一组分割,并转换为十进制数值
  const rgb = hex.match(/.{2}/g)?.map(channel => parseInt(`0x${channel}`)) || [];

  return rgb;
}

1.7 将十六进制颜色值转换为 RGBA 字符串格式

/**
 * @param {string} _color - 十六进制颜色字符串,如 '#FF5733' 或 '#F5A'
 * @param {number} _opacity - 透明度(范围 0 到 1)
 * @returns {string} 转换后的 RGBA 字符串,例如 'rgba(255, 87, 51, 0.5)'
 *                   如果输入无效,则返回原始字符串或空字符串
 */
const hexToRGBA = (_color: string, _opacity: number): string => {
  let sColor = _color?.toLowerCase();

  // 定义十六进制颜色的正则表达式,支持 3 位或 6 位格式
  const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;

  // 检查是否为有效的十六进制颜色格式
  if (sColor && reg.test(sColor)) {
    // 如果是简写的 3 位格式(如 #F5A),将其扩展为标准的 6 位格式(如 #FF55AA)
    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;
    }

    // 处理标准的 6 位十六进制颜色值,提取 R、G、B 分量
    const sColorChange: number[] = [];
    for (let i = 1; i < 7; i += 2) {
      // 将每个两位的十六进制值转换为十进制数
      sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2), 16));
    }

    // 构造并返回 RGBA 字符串
    return `rgba(${sColorChange.join(", ")}, ${_opacity})`;
  }

  // 如果输入无效,返回原始颜色值
  return sColor ?? '';
};


1.8 将对象参数转换为查询字符串并拼接到 URL 上

/**
 * @param baseUrl - 基础 URL
 * @param obj - 要转换为查询参数的对象
 * @returns 拼接后的完整 URL
 *
 * 示例:
 * const url = setObjToUrlParams("https://api.example.com", { a: 1, b: "hello" });
 * // 结果: "https://api.example.com?a=1&b=hello"
 *
 * const urlWithParams = setObjToUrlParams("https://api.example.com?a=1", { b: "test" });
 * // 结果: "https://api.example.com?a=1&b=test"
 */
function setObjToUrlParams(baseUrl: string, obj: object): string {
  const params = Object.keys(obj).map(key => {
    return `${key}=${encodeURIComponent(obj[key])}`;
  }).join('&');

  const separator = baseUrl.includes('?') ? '' : '?';
  const cleanUrl = baseUrl.endsWith('?') || baseUrl.endsWith('&') ? baseUrl : baseUrl;

  return `${cleanUrl}${separator}${params}`;
}

1.9 判断传入的字符串是否为 Base64 编码的图片数据 URI

/**
 * 支持格式如:
 * - data:image/png;base64,iVBORw0KG...
 * - data:image/jpeg;base64,/9j/4AAQ...
 *
 * @param str - 需要判断的字符串
 * @returns 如果是 Base64 格式的图片数据,返回 `true`;否则返回 `false`
 *
 * 示例:
 * isBase64Image('data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...')
 * // true
 *
 * isBase64Image('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFQQMEDQsKDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwN/wAARCAABAAEDASIAAhEBAxEB/2gAMAwEAAhEDEQA/APf6ACiiigD//Z')
 * // true
 *
 * isBase64Image('https://example.com/image.png')
 * // false
 *
 * isBase64Image('not-a-base64-string')
 * // false
 */
function isBase64Image(str: string): boolean {
  // 判断字符串是否以 "data:image/" 开头并且包含 "base64"
  const base64PrefixRegex = /^data:image/([a-zA-Z]*);base64,/;
  return base64PrefixRegex.test(str);
}

1.10 移除 Base64 图片数据的前缀部分(如 "data:image/png;base64,")

/**
 * @param base64String - 包含前缀的完整 Base64 图片字符串
 * @returns 去除前缀后的纯 Base64 编码字符串
 *
 * ### 示例
 * removeBase64Prefix('data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...')
 * // 返回: 'iVBORw0KGgoAAAANSUhEUg...'
 *
 * removeBase64Prefix('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFQQMEDQsKDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwN/wAARCAABAAEDASIAAhEBAxEB/2gAMAwEAAhEDEQA/APf6ACiiigD//Z')
 * // 返回: '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFQQMEDQsKDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwN/wAARCAABAAEDASIAAhEBAxEB/2gAMAwEAAhEDEQA/APf6ACiiigD//Z'
 *
 * removeBase64Prefix('iVBORw0KGgoAAAANSUhEUg...')
 * // 返回: 'iVBORw0KGgoAAAANSUhEUg...'
 */
function removeBase64Prefix(base64String: string) {
  const regex = /^data:image\/[^;]+;base64,/;
  return base64String.replace(regex, "");
}

1.11 提取身份证中的出生年月日

/**
 * @param idCard 身份证号码
 * @returns 出生日期字符串(格式:YYYY-MM-DD),失败返回 null
 *
 * @example
 * getBirthdayFromIdCard("110101199003072316") // => "1990-03-07"
 * getBirthdayFromIdCard("110101900307231")     // => "1990-03-07"
 * getBirthdayFromIdCard("ABC")                  // => null
 */
const getBirthdayFromIdCard = (idCard: string): string | null => {
  if (!idCard || idCard.trim() === '') return null;

  let birthday = '';
  if (idCard.length === 15) {
    // 15位身份证:第7~8位为年份(YY),第9~10位为月份,第11~12位为日期
    const year = '19' + idCard.substring(6, 8);
    const month = idCard.substring(8, 10);
    const day = idCard.substring(10, 12);
    birthday = `${year}-${month}-${day}`;
  } else if (idCard.length === 18) {
    // 18位身份证:第7~10位为年份(YYYY),第11~12位为月份,第13~14位为日期
    const year = idCard.substring(6, 10);
    const month = idCard.substring(10, 12);
    const day = idCard.substring(12, 14);
    birthday = `${year}-${month}-${day}`;
  } else {
    return null;
  }

  // 简单校验日期是否合法
  const date = new Date(birthday);
  if (date.toString() === 'Invalid Date') return null;
  return birthday;
};

1.11.1 根据身份证号码计算当前年龄

  • 需要getBirthdayFromIdCard先解析出生日期
/**
 * @param idCard 身份证号码
 * @returns 年龄(整数),失败返回 null
 *
 * @example
 * getAgeFromIdCard("110101199003072316") // => 实际年龄(如:34)
 * getAgeFromIdCard("110101900307231")     // => 同上
 * getAgeFromIdCard("ABC")                  // => null
 */
const getAgeFromIdCard = (idCard: string): number | null => {
  const birthday = getBirthdayFromIdCard(idCard);
  if (!birthday) return null;

  const birthDate = new Date(birthday);
  const today = new Date();

  let age = today.getFullYear() - birthDate.getFullYear();
  const m = today.getMonth() - birthDate.getMonth();
  if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
    age--;
  }

  return age >= 0 ? age : null;
};

1.12 解析身份证中的性别信息(仅支持18位)

/**
 * @param idCard 18位身份证号码
 * @returns "Male" 表示男性,"Female" 表示女性,否则返回 null
 *
 * @example
 * getGenderFromIdCard("110101199003072316") // => "male"
 * getGenderFromIdCard("110101199003072326") // => "female"
 * getGenderFromIdCard("110101900307231")     // => null (非18位)
 */
const getGenderFromIdCard = (idCard: string): 'Male' | 'Female' | null => {
  if (idCard.length !== 18) {
    return null;
  }

  const genderCode = parseInt(idCard[16], 10); // 第17位数字
  return genderCode % 2 === 1 ? 'Male' : 'Female';
};

2. 数组|对象工具函数 Array | Object

2.1 根据指定的键对对象数组进行分组,返回一个以组合键为索引、对应元素数组为值的对象

/**
 * @template T - 数组中每个元素的类型,必须是一个对象(object)
 * @template K - 指定分组依据的键的类型,必须是 T 的 key
 * @param {T[]} arr - 要分组的对象数组
 * @param {K[]} keys - 用于分组的一个或多个键(属性名)
 * @returns {Record<string, T[]>} 分组后的结果,键为组合键字符串,值为对应的元素数组
 *
 * @example
 * const testData1 = [
 *   { name: 'Alice', age: 25, city: 'Beijing' },
 *   { name: 'Bob', age: 30, city: 'Shanghai' },
 *   { name: 'Charlie', age: 25, city: 'Beijing' }
 * ];
 *
 * groupByKeys(testData1, ['city']);
 * // 返回:
 * // {
 * //   'Beijing': [{ name: 'Alice', ... }, { name: 'Charlie', ... }],
 * //   'Shanghai': [{ name: 'Bob', ... }]
 * // }
 */
const groupByKeys = <T extends object, K extends keyof T>(arr: T[], keys: K[]): Record<string, T[]> => {
  return arr.reduce<Record<string, T[]>>((acc, current) => {
    const groupKey = keys.map((key) => Reflect.get(current, key) as T).join('-')
    if (!acc[groupKey]) {
      acc[groupKey] = []
    }
    acc[groupKey].push(current)
    return acc
  }, {} as Record<string, T[]>)
}

2.2 深拷贝函数,用于复制对象或数组及其嵌套结构,避免引用共享

/**
 * @param {ESObject} obj - 要深拷贝的对象或数组
 * @returns {ESObject} 返回一个与原对象结构相同但内存独立的新对象
 */
function deepCopy(obj: ESObject): ESObject {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  let copy: ESObject;
  if (Array.isArray(obj)) {
    copy = [];
    for (let i = 0; i < obj.length; i++) {
      copy[i] = deepCopy(obj[i]);
    }
  } else {
    copy = {};
    for (let i = 0; i < obj.length(); i++) {
      let key: ESObject = obj[i];
      if (obj.hasOwnProperty(key)) {
        copy[key] = deepCopy(obj[key]);
      }
    }
  }
  return copy;
}

2.3 对两个数组进行对比,删除数组 A 中与数组 B 相同的数据(根据指定的唯一标识字段)


/**
 * @param dataA - 数组 A,从中移除匹配项
 * @param dataB - 数组 B,包含需要匹配的项
 * @param id - 用于比较的唯一标识字段名称(必须是对象中的键)
 * @returns 过滤后的数组 A,不包含在数组 B 中出现过的数据
 *
 * @typeParam T - 泛型类型,表示数组中对象的类型
 * @typeParam K - 泛型类型,表示用于比较的字段名,必须是 `T` 的键
 *
 * 示例:
 * const arrayA = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
 * const arrayB = [{ id: 2, name: 'Bob' }];
 * const result = removeMatchingItemsById(arrayA, arrayB, 'id');
 * // result => [{ id: 1, name: 'Alice' }]
 */

function removeMatchingItemsById<T extends Record<K, string | number>, K extends keyof T>(
  dataA: T[],
  dataB: T[],
  id: K
): T[] {
  if (!Array.isArray(dataA) || !Array.isArray(dataB)) {
    throw new Error('dataA and dataB must be arrays');
  }

  const idsInB = new Set(dataB.map(item => item[id]));

  return dataA.filter(item => !idsInB.has(item[id]));
}

2.4 泛型防抖函数:在指定的时间间隔内多次触发时,只执行最后一次调用

/**
 * 防止高频事件(如输入框变化、窗口调整等)频繁执行函数,提升性能和用户体验。
 * 使用泛型 `<T>` 支持任意类型的参数传入,保持类型安全。
 *
 * @param func 要执行的目标函数,接受一个可选的泛型参数 `T`,也可以无参
 * @param delay 延迟时间,单位为毫秒(ms),默认值为 300ms
 * @returns 返回一个新的包装函数,具备防抖能力,调用该函数即触发防抖逻辑
 *
 * @example
 * // 示例1:基本用法 - 字符串参数
 * const searchHandler = debounce<string>((query: string) => {
 *   console.log('搜索内容:', query);
 * }, 500);
 *
 * searchHandler('Harmony');
 * searchHandler('HarmonyOS'); // 上一次调用被取消,只有最后的 'HarmonyOS' 会被打印
 *
 * @example
 * // 示例2:传递对象参数
 * interface User {
 *   id: number;
 *   name: string;
 * }
 *
 * const saveUser = debounce<User>((user: User) => {
 *   console.log('保存用户:', user);
 * }, 1000);
 *
 * saveUser({ id: 1, name: 'Alice' });
 * saveUser({ id: 2, name: 'Bob' }); // 最终只会保存 Bob
 *
 * @example
 * // 示例3:不带参数的情况
 * const logMessage = debounce(() => {
 *   console.log('页面加载完成');
 * }, 800);
 *
 * logMessage(); // 页面加载完成后会延迟输出日志
 */
 
const debounce = <T>(func: (arg?: T) => void, delay: number = 300) => {
  let timer: number | null = null;

  return (arg?: T): void => {
    if (timer !== null) {
      clearTimeout(timer); // 清除之前的定时器,避免重复执行
    }

    timer = setTimeout(() => {
      func(arg); // 在 delay 时间后执行目标函数
    }, delay);
  };
};

3. 日期处理工具类 Date

3.1 获取当前时间戳(毫秒数)

/**
 * 如果传入一个有效的 Date 对象,则返回该对象对应的时间戳;
 * 否则返回当前系统时间的时间戳。
 *
 * @param {Date} [date] - 可选参数,一个有效的 Date 对象
 * @returns {number} 表示时间戳的数字(单位:毫秒)
 *
 * @example
 * getCurrentTimestamp(); // 返回当前时间戳,例如 1712345678901
 * getCurrentTimestamp(new Date(2023, 0, 1)); // 返回 2023 年 1 月 1 日对应的时间戳
 */
function getCurrentTimestamp(date?: Date): number {
  if (date instanceof Date && !isNaN(date.getTime())) {
    return date.getTime();
  }
  return Date.now();
}

3.2 计算两个日期之间的天数差(向上取整)

/**
 * @param {Date} startDate - 开始日期对象
 * @param {Date} endDate - 结束日期对象
 * @returns {number} 两个日期之间的天数差(向上取整)
 *
 * @example
 * const start = new Date('2024-04-01');
 * const end = new Date('2024-04-03');
 * getDaysBetweenDates(start, end); // 返回 2
 */
function getDaysBetweenDates(startDate: Date, endDate: Date): number {
    const diffTime = Math.abs(endDate.getTime() - startDate.getTime());
    return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}

3.3 计算两个日期之间的年数和剩余月份数,结果不进行四舍五入


interface YearsAndMonths {
  years: number;
  months: number;
}

/**
 * @param {Date} startDate - 起始日期
 * @param {Date} endDate - 结束日期
 * @returns {YearsAndMonths} 返回包含年份差和剩余月份的对象
 * @throws {Error} 如果传入的日期无效或结束日期早于开始日期
 *
 * @example
 * const start = new Date('2020-05-15');
 * const end = new Date('2023-07-20');
 * CalculateYearsAndMonths(start, end); // 返回 { years: 3, months: 2 }
 */
const CalculateYearsAndMonths = (startDate: Date, endDate: Date) => {
  const start = new Date(startDate);
  const end = new Date(endDate);

  // 校验是否为有效日期
  if (isNaN(start.getTime()) || isNaN(end.getTime())) {
    throw new Error('Invalid date input');
  }

  // 确保结束日期不早于开始日期
  if (end < start) {
    throw new Error('End date cannot be earlier than start date');
  }

  let years = end.getFullYear() - start.getFullYear();
  let months = end.getMonth() - start.getMonth();

  // 调整年份和月份:如果月份为负,则借位一年并加12个月
  if (months < 0) {
    years--;
    months += 12;
  }

  return { years, months } as YearsAndMonths;
};

3.4 将时间格式化为指定字符串格式

interface TimeFormatObject {
  y: number; // 年
  m: number; // 月(1-12)
  d: number; // 日
  h: number; // 时
  i: number; // 分
  s: number; // 秒
  a: number; // 星期几(0=周日,1=周一,... 6=周六)
}


/**
 * 支持传入 Date 对象、时间戳(10位或13位)或日期字符串(如 '2024-01-01')
 * 可自定义输出格式,支持年(y)、月(m)、日(d)、时(h)、分(i)、秒(s)、星期(a)等占位符
 *
 * 示例:
 *   parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}') => "2024-05-30 15:30:45"
 *   parseTime(1717029200, '{y}年{m}月{d}日 星期{a}') => "2024年5月30日 星期四"
 *
 * @param {Date | string | number} [time]
 *    要格式化的时间,支持 Date / 时间戳 / 日期字符串
 * @param {string} [cFormat]
 *。  格式化模板,默认为 '{y}-{m}-{d} {h}:{i}:{s}'
 *    支持字段:{y}年,{m}月,{d}日,{h}时,{i}分,{s}秒,{a}星期
 * @returns {string} 格式化后的时间字符串,若时间无效则返回空字符串
 */
 
function parseTime<K extends keyof TimeFormatObject>(time: Date | string | number, cFormat?: string): string {
  if (arguments.length === 0) {
    return ''
  }

  if (time == null || time === 'null') {
    return ''
  }

  let date: Date

  if (time instanceof Date) {
    date = time
  } else {
    if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
      time = parseInt(time)
    }

    if (typeof time === 'number') {
      const floorTime = Math.floor(time)
      if (floorTime.toString().length === 10) {
        time = time * 1000
      }
    }

    date = new Date(time as number | string)

    if (isNaN(date.getTime())) {
      return ''
    }
  }

  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
  const formatObj: TimeFormatObject = {
    y: date.getFullYear(),
    m: date.getMonth() + 1,
    d: date.getDate(),
    h: date.getHours(),
    i: date.getMinutes(),
    s: date.getSeconds(),
    a: date.getDay()
  }

  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key: K) => {
    const value = Reflect.get(formatObj, key) ?? ''

    if (key === 'a') {
      return ['日', '一', '二', '三', '四', '五', '六'][value as number] || ''
    }

    if (result.length > 0 && typeof value === 'number' && value < 10) {
      return '0' + value
    }

    return value.toString()
  })

  return time_str
}

3.4.1 格式化时间戳为可读性更强的时间描述

  • parsedTime需要用到上面封装的时间格式化工具函数
/**
 * @param {number} 【time】 - 时间戳(秒或毫秒)。
 * @param {string | undefined} 【option】
 * 可自定义输出格式,支持年(y)、月(m)、日(d)、时(h)、分(i)、秒(s)、星期(a)等占位符
 * @returns {string} - 格式化后的时间描述。
 *
 * 该函数首先检查时间戳的长度是否为10位(即是否是秒级时间戳),如果是,则将其转换为毫秒级时间戳。
 * 然后创建一个 Date 对象,并计算当前时间和给定时间之间的差异(以秒为单位)。
 * 根据时间差,返回不同的时间描述:
 * - 如果时间差小于30秒,返回“刚刚”。
 * - 如果时间差小于1小时,返回“X分钟前”。
 * - 如果时间差小于24小时,返回“X小时前”。
 * - 如果时间差小于2天,返回“1天前”。
 * 如果提供了格式字符串,则使用 parseTime 函数按照指定格式返回时间。
 * 否则,返回默认格式的时间字符串,包括月份、日期、小时和分钟。
 */
function formatTime(time: number, option?: string): string {
  let parsedTime = time;

  if (('' + parsedTime).length === 10) {
    parsedTime = parseInt('' + parsedTime) * 1000;
  } else {
    parsedTime = +parsedTime;
  }

  const d = new Date(parsedTime);
  const now = Date.now();

  const diff = (now - d.getTime()) / 1000;

  if (diff < 30) {
    return '刚刚';
  } else if (diff < 3600) {
    // less 1 hour
    return Math.ceil(diff / 60) + '分钟前';
  } else if (diff < 3600 * 24) {
    return Math.ceil(diff / 3600) + '小时前';
  } else if (diff < 3600 * 24 * 2) {
    return '1天前';
  }

  if (option) {
    return parseTime(parsedTime, option);
  } else {
    return (
      d.getMonth() +
        1 +
        '月' +
      d.getDate() +
        '日' +
      d.getHours() +
        '时' +
      d.getMinutes() +
        '分'
    );
  }
}

4. 规则校验

4.1 验证中国大陆身份证号码(支持15位、18位,含校验码验证)

/**
 * @param {string} idCard - 待验证的身份证号码字符串
 * @returns {boolean} 返回 true 表示合法,false 表示不合法
 *
 * @example
 * VerifyCertificate("110101199003072316") // => true
 * VerifyCertificate("110101900307231")     // => true (15位)
 * VerifyCertificate("11010119900307239X")   // => false
 * VerifyCertificate("")                    // => false
 */
const VerifyCertificate = (idCard: string): boolean => {
  // 空值检查
  if (!idCard || idCard.trim() === '') return false;

  // 格式校验正则(兼容15位、18位)
  const regIdCard = /^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/;
  if (!regIdCard.test(idCard)) return false;

  // 18位身份证校验
  if (idCard.length === 18) {
    const Wi = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; // 加权因子
    const Y = [1, 0, 10, 9, 8, 7, 6, 5, 4, 3, 2]; // 校验码对应值
    let sum = 0;
    for (let i = 0; i < 17; i++) {
      sum += parseInt(idCard[i], 10) * Wi[i];
    }
    const mod = sum % 11;
    const lastChar = idCard[17].toUpperCase();

    // 判断最后一位是否匹配
    if (mod === 2) {
      return lastChar === 'X';
    } else {
      return lastChar === Y[mod].toString();
    }

    // 15位身份证号校验
  } else if (idCard.length === 15) {
    const testV15 = /^[1-9]\d{5}\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{2}[0-9Xx]$/i;
    return testV15.test(idCard);

    // 16位老版身份证号校验(非标准,视需求保留)
  } else if (idCard.length === 16) {
    const testV16 = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{2}$/;
    return testV16.test(idCard);
  }

  return false;
};

4.2 使用 Luhn 算法校验银行卡号是否有效

/**
 * Luhn 算法用于验证银行卡号码的校验位(即最后一位),以确保输入的卡号格式正确。
 * 该算法不验证卡号是否真实存在,仅用于初步校验格式合法性。
 *
 * @param {string} bankno - 需要校验的银行卡号(包含最后一位校验位)
 * @returns {boolean} 校验结果,true 表示银行卡号合法,false 表示不合法
 *
 * @example
 * luhnCheck("6225880123456789"); // 返回 true 或 false,取决于卡号是否符合 Luhn 规则
 */
function luhnCheck(bankno: string): boolean {
  let lastNum = bankno.substr(bankno.length - 1, 1); // 取出最后一位
  let first15Num = bankno.substr(0, bankno.length - 1); // 前n-1位
  const newArr: number[] = [];

  for (let i = first15Num.length - 1; i > -1; i--) {
    newArr.push(parseInt(first15Num[i]));
  }

  const arrJiShu: number[] = []; // 奇数位*2 <9
  const arrJiShu2: number[] = []; // 奇数位*2 >=9
  const arrOuShu: number[] = []; // 偶数位数组

  for (let j = 0; j < newArr.length; j++) {
    if ((j + 1) % 2 === 1) { // 奇数位
      const doubled = newArr[j] * 2;
      if (doubled < 9) {
        arrJiShu.push(doubled);
      } else {
        arrJiShu2.push(doubled);
      }
    } else {
      arrOuShu.push(newArr[j]);
    }
  }

  const jishu_child1: number[] = []; // 分割后的个位
  const jishu_child2: number[] = []; // 分割后的十位

  for (let h = 0; h < arrJiShu2.length; h++) {
    jishu_child1.push(arrJiShu2[h] % 10);
    jishu_child2.push(Math.floor(arrJiShu2[h] / 10));
  }

  const sumJiShu = arrJiShu.reduce((a, b) => a + b, 0);
  const sumOuShu = arrOuShu.reduce((a, b) => a + b, 0);
  const sumJiShuChild1 = jishu_child1.reduce((a, b) => a + b, 0);
  const sumJiShuChild2 = jishu_child2.reduce((a, b) => a + b, 0);

  const sumTotal = sumJiShu + sumOuShu + sumJiShuChild1 + sumJiShuChild2;

  const k = sumTotal % 10 === 0 ? 10 : sumTotal % 10;
  const luhn = 10 - k;

  return parseInt(lastNum) === luhn;
}

4.3 验电话号码是否符合中国大陆的手机号或固定电话格式

/**
 * 该函数支持校验:
 * - 中国大陆手机号:以13、14、15、17、18、19开头的11位数字(含国际区号前缀可选)
 * - 固定电话号:格式为 区号-电话号码(如010-12345678)
 *
 * @param {string} phoneno - 需要校验的电话号码字符串
 * @returns {boolean} 校验结果,true 表示电话号码格式合法,false 表示不合法
 *
 * @example
 * telCheck("13812345678"); // 返回 true
 * telCheck("+8613812345678"); // 返回 true
 * telCheck("010-12345678"); // 返回 true
 * telCheck("1234567"); // 返回 false
 */
function telCheck(phoneno: string) {
  const phonerule =
    /^((\+?86)|(\(\+86\)))?(13[012356789][0-9]{8}|15[012356789][0-9]{8}|18[02356789][0-9]{8}|147[0-9]{8}|1349[0-9]{7})$/;
  const landlinerule = /^([0-9]{3,4}-)?[0-9]{7,8}$/;
  if (phonerule.test(phoneno) || landlinerule.test(phoneno)) {
    return true;
  } else {
    return false;
  }
}

4.4 校验字符串是否符合标准电子邮件地址格式

/**
 * 该函数使用正则表达式校验邮箱格式,要求:
 * - 用户名部分由字母、数字、下划线组成
 * - 域名部分由字母和数字组成,并以2到4个小写字母的顶级域结尾(如 .com, .org)
 *
 * @param {string} email - 需要校验的电子邮件地址
 * @returns {boolean} 校验结果,true 表示邮箱格式合法,false 表示不合法
 *
 * @example
 * emailCheck("user@example.com"); // 返回 true
 * emailCheck("user.name@domain.co.uk"); // 返回 true
 * emailCheck("invalid-email@domain"); // 返回 false
 */
function emailCheck(email: string) {
  const emailrule = /^\w+@[a-z0-9]+\.[a-z]{2,4}$/;
  if (emailrule.test(email)) {
    return true;
  } else {
    return false;
  }
}

总结

以上是一些常用的工具类和方法,涵盖了字符串处理、日期格式化、数组、对象等方面。通过这些工具类的封装,可以大大提升开发效率并保持代码的一致性。在实际开发中,可以根据具体需求进一步扩展和完善这些工具类。