HarmonyOS应用<节气通>开发第28篇:工具类封装

引言

工具类是应用开发中常用的辅助模块,用于封装通用的工具函数。本文将介绍如何封装常用的工具类,包括:

  • 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

  • 数组操作
  • 排序和分组
  • 集合运算

下一步预告

工具类封装已经完成!在下一篇文章中,我们将学习:

  • 错误处理与日志管理
  • 统一错误处理
  • 日志管理
  • 异常捕获

相关链接

相关推荐
伶俜661 小时前
鸿蒙原生应用实战(七)ArkUI 文件管理器:目录浏览 + 文件操作 + 搜索筛选
学习·华为·harmonyos
大雷神2 小时前
第96篇 | HarmonyOS 异常合集:权限拒绝、网络失败、模型失败、相机失败
harmonyos
Swift社区2 小时前
AI Native 鸿蒙 App:从页面驱动到智能驱动的架构革命
人工智能·架构·harmonyos
木咺吟2 小时前
鸿蒙原生应用实战(五):数据统计与个人中心——柱状图实现、统计计算与设置面板
harmonyos
浮芷.2 小时前
鸿蒙PC-HarmonyOS 6.1 60fps 流畅动画实现与 ArkTS 常见错误深度剖析
华为·harmonyos·鸿蒙
风满城332 小时前
鸿蒙原生应用实战(四):成就系统与排行榜开发 — 数据展示与交互进阶
harmonyos
伶俜662 小时前
鸿蒙原生应用实战(五)ArkUI 图片拼接/长图生成:多图合并 + Canvas 绘制 + 导出分享
华为·harmonyos
前端不太难2 小时前
当 AI 接管 Workspace:鸿蒙 PC Agent 架构设计实践
人工智能·状态模式·harmonyos
伶俜662 小时前
鸿蒙原生应用实战(六)ArkUI 屏幕录制 + GIF 截取:录屏 + 裁剪关键帧 + 转 GIF
华为·harmonyos