
引言
工具类是应用开发中常用的辅助模块,用于封装通用的工具函数。本文将介绍如何封装常用的工具类,包括:
- DateUtils日期工具类
- SeasonUtils季节工具类
- 常用工具函数
通过本文,你将掌握如何构建实用的工具类。
学习目标
完成本文后,你将能够:
- ✅ 封装DateUtils日期工具类
- ✅ 封装SeasonUtils季节工具类
- ✅ 实现常用工具函数
- ✅ 提高代码复用性
需求分析
功能模块设计
| 模块 | 功能描述 | 技术要点 |
|---|---|---|
| DateUtils | 日期格式化、计算 | 日期格式化、天数计算 |
| SeasonUtils | 季节判断、节气计算 | 节气判断、季节对应 |
| StringUtils | 字符串处理 | 字符串格式化、验证 |
| ArrayUtils | 数组处理 | 数组操作、去重 |
核心实现
步骤1: DateUtils日期工具类
typescript
// utils/DateUtils.ets
/**
* 日期工具类
*/
export class DateUtils {
/**
* 格式化日期
* @param date 日期对象
* @param format 格式化字符串
* @returns 格式化后的日期字符串
*/
static format(date: Date, format: string = 'yyyy-MM-dd'): string {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
return format
.replace('yyyy', year.toString())
.replace('MM', this.padZero(month))
.replace('dd', this.padZero(day))
.replace('HH', this.padZero(hours))
.replace('mm', this.padZero(minutes))
.replace('ss', this.padZero(seconds));
}
/**
* 格式化时间(时分秒)
* @param date 日期对象
* @returns 格式化后的时间字符串
*/
static formatTime(date: Date): string {
return this.format(date, 'HH:mm:ss');
}
/**
* 格式化日期时间
* @param date 日期对象
* @returns 格式化后的日期时间字符串
*/
static formatDateTime(date: Date): string {
return this.format(date, 'yyyy-MM-dd HH:mm:ss');
}
/**
* 格式化为中文日期
* @param date 日期对象
* @returns 中文日期字符串
*/
static formatChinese(date: Date): string {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
const weekDay = weekDays[date.getDay()];
return `${year}年${month}月${day}日 星期${weekDay}`;
}
/**
* 获取两个日期之间的天数
* @param startDate 开始日期
* @param endDate 结束日期
* @returns 天数差
*/
static daysBetween(startDate: Date, endDate: Date): number {
const diff = endDate.getTime() - startDate.getTime();
return Math.floor(diff / (1000 * 60 * 60 * 24));
}
/**
* 获取今天是星期几
* @param date 日期对象
* @returns 星期几(中文)
*/
static getDayOfWeek(date: Date): string {
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
return weekDays[date.getDay()];
}
/**
* 判断是否是今天
* @param date 日期对象
* @returns 是否是今天
*/
static isToday(date: Date): boolean {
const today = new Date();
return this.format(date, 'yyyy-MM-dd') === this.format(today, 'yyyy-MM-dd');
}
/**
* 判断是否是昨天
* @param date 日期对象
* @returns 是否是昨天
*/
static isYesterday(date: Date): boolean {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return this.format(date, 'yyyy-MM-dd') === this.format(yesterday, 'yyyy-MM-dd');
}
/**
* 判断是否是本周
* @param date 日期对象
* @returns 是否是本周
*/
static isThisWeek(date: Date): boolean {
const today = new Date();
const dayOfWeek = today.getDay();
const monday = new Date(today);
monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1));
return date >= monday && date <= today;
}
/**
* 判断是否是本月
* @param date 日期对象
* @returns 是否是本月
*/
static isThisMonth(date: Date): boolean {
const today = new Date();
return date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
}
/**
* 获取指定天数后的日期
* @param date 起始日期
* @param days 天数
* @returns 新日期
*/
static addDays(date: Date, days: number): Date {
const newDate = new Date(date);
newDate.setDate(newDate.getDate() + days);
return newDate;
}
/**
* 获取指定月数后的日期
* @param date 起始日期
* @param months 月数
* @returns 新日期
*/
static addMonths(date: Date, months: number): Date {
const newDate = new Date(date);
newDate.setMonth(newDate.getMonth() + months);
return newDate;
}
/**
* 获取相对时间描述
* @param date 日期对象
* @returns 相对时间字符串
*/
static getRelativeTime(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const minute = 60 * 1000;
const hour = 60 * minute;
const day = 24 * hour;
const week = 7 * day;
const month = 30 * day;
const year = 365 * day;
if (diff < minute) {
return '刚刚';
} else if (diff < hour) {
return `${Math.floor(diff / minute)}分钟前`;
} else if (diff < day) {
return `${Math.floor(diff / hour)}小时前`;
} else if (diff < week) {
return `${Math.floor(diff / day)}天前`;
} else if (diff < month) {
return `${Math.floor(diff / week)}周前`;
} else if (diff < year) {
return `${Math.floor(diff / month)}个月前`;
} else {
return `${Math.floor(diff / year)}年前`;
}
}
/**
* 数字补零
* @param num 数字
* @returns 补零后的字符串
*/
private static padZero(num: number): string {
return num < 10 ? `0${num}` : num.toString();
}
}
设计要点:
- 日期格式化
- 相对时间计算
- 日期比较
步骤2: SeasonUtils季节工具类
typescript
// utils/SeasonUtils.ets
/**
* 季节工具类
*/
export class SeasonUtils {
/**
* 季节枚举
*/
static readonly SEASONS = {
SPRING: 'spring',
SUMMER: 'summer',
AUTUMN: 'autumn',
WINTER: 'winter'
};
/**
* 季节信息配置
*/
static readonly SEASON_INFO: Record<string, SeasonConfig> = {
spring: {
name: '春季',
nameEn: 'Spring',
startMonth: 3,
endMonth: 5,
color: '#4A9B6D',
icon: 'ic_spring'
},
summer: {
name: '夏季',
nameEn: 'Summer',
startMonth: 6,
endMonth: 8,
color: '#FF5722',
icon: 'ic_summer'
},
autumn: {
name: '秋季',
nameEn: 'Autumn',
startMonth: 9,
endMonth: 11,
color: '#FFB300',
icon: 'ic_autumn'
},
winter: {
name: '冬季',
nameEn: 'Winter',
startMonth: 12,
endMonth: 2,
color: '#00BCD4',
icon: 'ic_winter'
}
};
/**
* 24节气列表
*/
static readonly SOLAR_TERMS = [
{ name: '立春', month: 2, day: 4, season: 'spring' },
{ name: '雨水', month: 2, day: 19, season: 'spring' },
{ name: '惊蛰', month: 3, day: 6, season: 'spring' },
{ name: '春分', month: 3, day: 21, season: 'spring' },
{ name: '清明', month: 4, day: 5, season: 'spring' },
{ name: '谷雨', month: 4, day: 20, season: 'spring' },
{ name: '立夏', month: 5, day: 6, season: 'summer' },
{ name: '小满', month: 5, day: 21, season: 'summer' },
{ name: '芒种', month: 6, day: 6, season: 'summer' },
{ name: '夏至', month: 6, day: 22, season: 'summer' },
{ name: '小暑', month: 7, day: 7, season: 'summer' },
{ name: '大暑', month: 7, day: 23, season: 'summer' },
{ name: '立秋', month: 8, day: 8, season: 'autumn' },
{ name: '处暑', month: 8, day: 23, season: 'autumn' },
{ name: '白露', month: 9, day: 8, season: 'autumn' },
{ name: '秋分', month: 9, day: 23, season: 'autumn' },
{ name: '寒露', month: 10, day: 8, season: 'autumn' },
{ name: '霜降', month: 10, day: 24, season: 'autumn' },
{ name: '立冬', month: 11, day: 8, season: 'winter' },
{ name: '小雪', month: 11, day: 22, season: 'winter' },
{ name: '大雪', month: 12, day: 7, season: 'winter' },
{ name: '冬至', month: 12, day: 22, season: 'winter' },
{ name: '小寒', month: 1, day: 6, season: 'winter' },
{ name: '大寒', month: 1, day: 20, season: 'winter' }
];
/**
* 根据月份获取季节
* @param month 月份(1-12)
* @returns 季节标识
*/
static getSeasonByMonth(month: number): string {
if (month >= 3 && month <= 5) return this.SEASONS.SPRING;
if (month >= 6 && month <= 8) return this.SEASONS.SUMMER;
if (month >= 9 && month <= 11) return this.SEASONS.AUTUMN;
return this.SEASONS.WINTER;
}
/**
* 根据日期获取季节
* @param date 日期对象
* @returns 季节标识
*/
static getSeason(date: Date): string {
return this.getSeasonByMonth(date.getMonth() + 1);
}
/**
* 获取季节信息
* @param season 季节标识
* @returns 季节信息配置
*/
static getSeasonInfo(season: string): SeasonConfig {
return this.SEASON_INFO[season] || this.SEASON_INFO.spring;
}
/**
* 获取当前季节信息
* @returns 当前季节信息配置
*/
static getCurrentSeasonInfo(): SeasonConfig {
const season = this.getSeason(new Date());
return this.getSeasonInfo(season);
}
/**
* 获取季节背景图
* @param season 季节标识
* @returns 背景图路径
*/
static getSeasonBackground(season: string): string {
const backgrounds: Record<string, string> = {
spring: 'bg/seasons/chun.png',
summer: 'bg/seasons/xia.png',
autumn: 'bg/seasons/qiu.png',
winter: 'bg/seasons/dong.png'
};
return backgrounds[season] || backgrounds.spring;
}
/**
* 获取当前节气
* @param date 日期对象(可选,默认今天)
* @returns 当前节气信息
*/
static getCurrentSolarTerm(date?: Date): SolarTerm | null {
const today = date || new Date();
const month = today.getMonth() + 1;
const day = today.getDate();
// 查找今天所在的节气
for (let i = 0; i < this.SOLAR_TERMS.length; i++) {
const term = this.SOLAR_TERMS[i];
const nextTerm = this.SOLAR_TERMS[(i + 1) % this.SOLAR_TERMS.length];
// 判断日期是否在当前节气范围内
if (this.isDateInRange(today, term, nextTerm)) {
return term;
}
}
return null;
}
/**
* 判断日期是否在节气范围内
*/
private static isDateInRange(date: Date, startTerm: SolarTerm, endTerm: SolarTerm): boolean {
const startDate = new Date(date.getFullYear(), startTerm.month - 1, startTerm.day);
const endDate = new Date(date.getFullYear(), endTerm.month - 1, endTerm.day);
// 处理跨年份的情况(如大寒到立春)
if (endDate < startDate) {
return date >= startDate || date <= endDate;
}
return date >= startDate && date < endDate;
}
/**
* 获取下一个节气
* @param date 日期对象(可选,默认今天)
* @returns 下一个节气信息
*/
static getNextSolarTerm(date?: Date): SolarTerm {
const today = date || new Date();
const currentTerm = this.getCurrentSolarTerm(today);
if (!currentTerm) {
return this.SOLAR_TERMS[0];
}
const currentIndex = this.SOLAR_TERMS.findIndex(t => t.name === currentTerm.name);
return this.SOLAR_TERMS[(currentIndex + 1) % this.SOLAR_TERMS.length];
}
/**
* 获取节气距离今天的天数
* @param term 节气
* @returns 天数差
*/
static getDaysToSolarTerm(term: SolarTerm): number {
const today = new Date();
const termDate = new Date(today.getFullYear(), term.month - 1, term.day);
if (termDate < today) {
// 如果今年的节气已过,计算到明年的天数
const nextYearTerm = new Date(today.getFullYear() + 1, term.month - 1, term.day);
return Math.ceil((nextYearTerm.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
}
return Math.ceil((termDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
}
}
interface SeasonConfig {
name: string;
nameEn: string;
startMonth: number;
endMonth: number;
color: string;
icon: string;
}
interface SolarTerm {
name: string;
month: number;
day: number;
season: string;
}
设计要点:
- 季节判断
- 节气查询
- 背景图获取
步骤3: StringUtils字符串工具类
typescript
// utils/StringUtils.ets
/**
* 字符串工具类
*/
export class StringUtils {
/**
* 判断字符串是否为空
* @param str 字符串
* @returns 是否为空
*/
static isEmpty(str: string | null | undefined): boolean {
return !str || str.trim() === '';
}
/**
* 判断字符串是否不为空
* @param str 字符串
* @returns 是否不为空
*/
static isNotEmpty(str: string | null | undefined): boolean {
return !this.isEmpty(str);
}
/**
* 去除字符串两端空格
* @param str 字符串
* @returns 去除空格后的字符串
*/
static trim(str: string): string {
return str.trim();
}
/**
* 截取字符串并添加省略号
* @param str 字符串
* @param maxLength 最大长度
* @returns 截取后的字符串
*/
static truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) {
return str;
}
return str.slice(0, maxLength) + '...';
}
/**
* 格式化字符串(替换占位符)
* @param str 模板字符串
* @param params 参数对象
* @returns 格式化后的字符串
*/
static format(str: string, params: Record<string, any>): string {
let result = str;
Object.keys(params).forEach(key => {
const regex = new RegExp(`\\{${key}\\}`, 'g');
result = result.replace(regex, params[key]);
});
return result;
}
/**
* 验证邮箱格式
* @param email 邮箱地址
* @returns 是否有效
*/
static isValidEmail(email: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
/**
* 验证手机号格式
* @param phone 手机号
* @returns 是否有效
*/
static isValidPhone(phone: string): boolean {
const regex = /^1[3-9]\d{9}$/;
return regex.test(phone);
}
/**
* 验证身份证号格式
* @param idCard 身份证号
* @returns 是否有效
*/
static isValidIdCard(idCard: string): boolean {
const regex = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/;
return regex.test(idCard);
}
/**
* 隐藏手机号中间四位
* @param phone 手机号
* @returns 隐藏后的手机号
*/
static hidePhone(phone: string): string {
if (!this.isValidPhone(phone)) {
return phone;
}
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
/**
* 隐藏邮箱中间部分
* @param email 邮箱
* @returns 隐藏后的邮箱
*/
static hideEmail(email: string): string {
if (!this.isValidEmail(email)) {
return email;
}
const [name, domain] = email.split('@');
if (name.length <= 2) {
return email;
}
return name.slice(0, 2) + '***@' + domain;
}
/**
* 生成随机字符串
* @param length 长度
* @returns 随机字符串
*/
static randomString(length: number): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 首字母大写
* @param str 字符串
* @returns 首字母大写后的字符串
*/
static capitalize(str: string): string {
if (this.isEmpty(str)) {
return str;
}
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* 驼峰命名转换为短横线命名
* @param str 驼峰命名字符串
* @returns 短横线命名字符串
*/
static camelToKebab(str: string): string {
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
}
/**
* 短横线命名转换为驼峰命名
* @param str 短横线命名字符串
* @returns 驼峰命名字符串
*/
static kebabToCamel(str: string): string {
return str.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
}
}
设计要点:
- 字符串验证
- 格式化处理
- 脱敏处理
步骤4: ArrayUtils数组工具类
typescript
// utils/ArrayUtils.ets
/**
* 数组工具类
*/
export class ArrayUtils {
/**
* 判断数组是否为空
* @param arr 数组
* @returns 是否为空
*/
static isEmpty<T>(arr: T[] | null | undefined): boolean {
return !arr || arr.length === 0;
}
/**
* 判断数组是否不为空
* @param arr 数组
* @returns 是否不为空
*/
static isNotEmpty<T>(arr: T[] | null | undefined): boolean {
return !this.isEmpty(arr);
}
/**
* 数组去重
* @param arr 数组
* @param keyFn 用于比较的键函数(可选)
* @returns 去重后的数组
*/
static unique<T>(arr: T[], keyFn?: (item: T) => any): T[] {
if (this.isEmpty(arr)) {
return arr;
}
if (keyFn) {
const seen = new Set();
return arr.filter(item => {
const key = keyFn(item);
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
return [...new Set(arr)];
}
/**
* 数组分组
* @param arr 数组
* @param keyFn 分组键函数
* @returns 分组后的对象
*/
static groupBy<T>(arr: T[], keyFn: (item: T) => string): Record<string, T[]> {
if (this.isEmpty(arr)) {
return {};
}
return arr.reduce((acc, item) => {
const key = keyFn(item);
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(item);
return acc;
}, {} as Record<string, T[]>);
}
/**
* 数组排序
* @param arr 数组
* @param keyFn 排序键函数
* @param order 排序顺序(asc/desc)
* @returns 排序后的数组
*/
static sortBy<T>(arr: T[], keyFn: (item: T) => any, order: 'asc' | 'desc' = 'asc'): T[] {
if (this.isEmpty(arr)) {
return arr;
}
return [...arr].sort((a, b) => {
const valueA = keyFn(a);
const valueB = keyFn(b);
if (valueA < valueB) {
return order === 'asc' ? -1 : 1;
}
if (valueA > valueB) {
return order === 'asc' ? 1 : -1;
}
return 0;
});
}
/**
* 数组分页
* @param arr 数组
* @param page 页码(从1开始)
* @param pageSize 每页大小
* @returns 分页后的数组
*/
static paginate<T>(arr: T[], page: number, pageSize: number): T[] {
if (this.isEmpty(arr)) {
return arr;
}
const start = (page - 1) * pageSize;
const end = start + pageSize;
return arr.slice(start, end);
}
/**
* 获取数组中的随机元素
* @param arr 数组
* @returns 随机元素
*/
static random<T>(arr: T[]): T | undefined {
if (this.isEmpty(arr)) {
return undefined;
}
return arr[Math.floor(Math.random() * arr.length)];
}
/**
* 数组扁平化
* @param arr 多维数组
* @returns 扁平化后的数组
*/
static flatten<T>(arr: T[][]): T[] {
return arr.reduce((acc, item) => acc.concat(item), []);
}
/**
* 数组交集
* @param arr1 数组1
* @param arr2 数组2
* @returns 交集数组
*/
static intersection<T>(arr1: T[], arr2: T[]): T[] {
if (this.isEmpty(arr1) || this.isEmpty(arr2)) {
return [];
}
return arr1.filter(item => arr2.includes(item));
}
/**
* 数组差集
* @param arr1 数组1
* @param arr2 数组2
* @returns 差集数组
*/
static difference<T>(arr1: T[], arr2: T[]): T[] {
if (this.isEmpty(arr1)) {
return [];
}
if (this.isEmpty(arr2)) {
return [...arr1];
}
return arr1.filter(item => !arr2.includes(item));
}
/**
* 数组并集
* @param arr1 数组1
* @param arr2 数组2
* @returns 并集数组
*/
static union<T>(arr1: T[], arr2: T[]): T[] {
return this.unique([...arr1, ...arr2]);
}
/**
* 数组转对象
* @param arr 数组
* @param keyFn 键函数
* @param valueFn 值函数(可选)
* @returns 对象
*/
static toObject<T>(arr: T[], keyFn: (item: T) => string, valueFn?: (item: T) => any): Record<string, any> {
if (this.isEmpty(arr)) {
return {};
}
return arr.reduce((acc, item) => {
const key = keyFn(item);
const value = valueFn ? valueFn(item) : item;
acc[key] = value;
return acc;
}, {} as Record<string, any>);
}
}
设计要点:
- 数组操作
- 排序和分组
- 集合运算
本章小结
核心知识点
本文完成了工具类的封装:
1. DateUtils
- 日期格式化
- 相对时间计算
- 日期比较
2. SeasonUtils
- 季节判断
- 节气查询
- 背景图获取
3. StringUtils
- 字符串验证
- 格式化处理
- 脱敏处理
4. ArrayUtils
- 数组操作
- 排序和分组
- 集合运算
下一步预告
工具类封装已经完成!在下一篇文章中,我们将学习:
- 错误处理与日志管理
- 统一错误处理
- 日志管理
- 异常捕获