【HarmonyOS】项目工程化工具 持续更迭...

320 阅读28分钟

在 HarmonyOS 开发中,工具类的封装可以极大提升开发效率和代码可维护性。本文将详细讲解我封装的一套工具类,涵盖用户认证、沉浸式全屏、日志打印、网络请求、组件截图保存、状态栏颜色控制、轻提示、系统能力管理等常用功能,并提供完整的使用示例。

FullScreen 沉浸式全屏设置

功能说明:

  • 开启/关闭沉浸式全屏(穿透手机顶底栏)
  • 获取状态栏和导航栏高度,用于页面布局适配

FullScreen封装的工具类中需要获取上下文this

  1. EntryAbilityonCreate生命周期钩子函数中:
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  ...
   // 通过应用状态管理对上下文进行状态创建设置
   AppStorage.setOrCreate('context', this.context)
}
  1. FullScreen工具类中进行获取使用
class FullScreen {
  //开启沉浸式全屏
  async enable() {
    try {
      // 获取当前窗口Context对象
      const ctx = AppStorage.get<Context>(CONTEXT)
      ...
    }
  }
  //关闭沉浸式全屏
  async disable() {
    try {
      // 获取当前窗口Context对象
      const ctx = AppStorage.get<Context>(CONTEXT)
      ...
    }
  }
}

工具类方法:

/**
 * description: [沉侵式系统:穿透手机顶底栏]
 * 1.通过内置window对象获取当前窗口对象
 * 2.调用window对象setWindowLayoutFullScreen方法开启沉浸式全屏
 * 3.通过window对象getWindowAvoidArea方法获取状态栏高度和导航条高度
 * 4.将状态栏高度和导航条高度存储在AppStorage中
 * */

import { CONTEXT, NAVIGATION_BAR_HEIGHT, STATUS_BAR_HEIGHT } from '../constants'
import { window } from '@kit.ArkUI';
import { logger } from './Logger';

class FullScreen {
  //开启沉浸式全屏
  async enable() {
    try {
      // 获取当前窗口Context对象
      const ctx = AppStorage.get<Context>(CONTEXT)
      if (ctx) {
        // 获取窗口对象
        const win = await window.getLastWindow(ctx);
        // 开启全屏(沉浸式)
        await win.setWindowLayoutFullScreen(true)
        // 获取状态栏高度
        const statusBarHeight = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
        // 存储状态栏高度
        AppStorage.setOrCreate(STATUS_BAR_HEIGHT, px2vp(statusBarHeight.topRect.height))
        logger.info('FullScreen StatusBarHeight', px2vp(statusBarHeight.topRect.height) + '')
        // 获取导航条高度
        const navigationBarHeight = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR)
        // 存储导航条高度
        AppStorage.setOrCreate(NAVIGATION_BAR_HEIGHT, px2vp(navigationBarHeight.bottomRect.height))
        logger.info('FullScreen StatusBarHeight', px2vp(navigationBarHeight.bottomRect.height) + '')
      }
    } catch (err) {
      logger.error('FullScreen Open Error', err)
    }
  }

  //关闭沉浸式全屏
  async disable() {
    try {
      // 获取当前窗口Context对象
      const ctx = AppStorage.get<Context>(CONTEXT)
      if (ctx) {
        // 获取窗口对象
        const win = await window.getLastWindow(ctx)
        // 关闭全屏(沉浸式)
        win.setWindowLayoutFullScreen(false)

        // 恢复状态栏高度和导航条高度为0
        AppStorage.setOrCreate(STATUS_BAR_HEIGHT, 0)
        AppStorage.setOrCreate(NAVIGATION_BAR_HEIGHT, 0)
      }
    } catch (err) {
      logger.error('FullScreen Close Error', err)
    }
  }
}

/**
 * 【使用方法】
 * 开启沉浸式全屏 fullScreen.enable()
 * 1.可在UIAbility中生命周期onWindowStageCreate钩子函数中开启沉浸式全屏
 * 2.通过`@StorageProp(...)`获取本地存储的状态栏高度和导航条高度
 * 3.页面中以获取的本地存储值动态设置状态栏高度和导航条高度为上下padding进行避让
 *
 * 相关沉侵式系统API:
 * expandSafeArea 控制组件扩展其安全区域
 * setKeyboardAvoidMode 控制虚拟键盘抬起时页面的避让模式
 * getKeyboardAvoidMode 返回虚拟键盘抬起时页面的避让模式
 *
 * 关闭沉浸式全屏 fullScreen.disable()
 * */

export const fullScreen = new FullScreen()

StatusBar 状态栏文字颜色控制

功能说明:

  • 切换手机顶部状态栏文字颜色(深色/浅色)

工具类方法:

/**
 * description: [顶部状态栏文字颜色切换 黑色/白色]
 * */

import { CONTEXT } from "../constants"
import { window } from "@kit.ArkUI"

class StatusBar {
  // 深色模式
  setDarkBar() {
    this.setBar({ statusBarContentColor: '#000000' })
  }

  // 浅色模式
  ssetLightBar() {
    this.setBar({ statusBarContentColor: '#ffffff' })
  }

  // 设置颜色API
  async setBar(config: window.SystemBarProperties) {
    //获取context对象
    const ctx = AppStorage.get<Context>(CONTEXT)
    //判断context对象是否存在 健壮性检测
    if (ctx) {
      //获取当前窗口对象
      const win = await window.getLastWindow(ctx)
      //调用API设置状态栏颜色
      win.setWindowSystemBarProperties(config)
    }
  }
}


/**
 * 【使用方法】
 * statusBar.setDarkBar() //设置顶部状态栏【时间/电量..】文字深色模式为 黑色
 * statusBar.setLightBar() //设置顶部状态栏【时间/电量..】文字浅色模式为 白色
 * */

export const statusBar = new StatusBar()

Logger 日志打印封装

功能说明:

  • 引用鸿蒙三方库 @abner/log二次封装配置,便于使用
  • 支持多种日志级别(info/debug/warn/error/fatal)
  • 自动识别 JSON 数据并格式化输出

二次封装:

/**
 * description: [日志打印] 引用三方库 @abner/log 进行二次封装
 * */

import { Log } from '@abner/log'
import { describe } from '@ohos/hypium'

Log.init({
  tag: 'hm_interview', //打印的标签,默认为: hm_interview
  domain: 0x0000, //输出日志所对应的业务领域,默认为0x0000
  close: false, //关闭日志输出,默认true为关闭,false为开启
  isHilog: true, //是否为鸿蒙日志系统
  showLogLocation: true, //是否展示点击的位置,默认为true是展示 ,false为不展示
  logSize: 800 //日志每次输出大小,最大1024字节
})

/**
 * 【使用方法】
 * logger.info("我是一个info类型日志", "testTag")
 * logger.debug("我是一个debug类型日志", "testTag")
 * logger.warn("我是一个warn类型日志", "testTag")
 * logger.error("我是一个error类型日志", "testTag")
 * logger.fatal("我是一个fatal类型日志", "testTag")
 *
 * 直接传递JSON对象 会自动转换成JSON字符串
 * logger.info({ "name": "AbnerMing", "age": 18 })
 *
 * */

export { Log as logger }

SaveAlbum 组件截图并保存至相册(截图分享)

✅ 使用说明

  • 作用.id('share') 为当前组件设置一个唯一的标识符,便于后续通过 componentSnapshot.get('share') 获取该组件的视图快照。
  • 应用场景:常用于分享弹窗、海报生成等需要对 UI 组件进行截图并保存的场景。
  • 配合工具类:与 SaveAlbum 工具类结合使用,实现截图 → 保存至沙箱 → 写入相册的完整流程。

工具类方法:

import { componentSnapshot } from '@kit.ArkUI'
import { image } from '@kit.ImageKit'
import { fileIo, fileUri } from '@kit.CoreFileKit'
import { photoAccessHelper } from '@kit.MediaLibraryKit'
import { CONTEXT } from '../constants'

class SaveAlbum {
  // 组件截图
  static async componentShot(compoentID: string) {
    // 获取组件截图
    const img = await componentSnapshot.get(compoentID)
    // 创建imagePacker对象实例
    const imagePacker = image.createImagePacker()
    // 获取到图片arrayBuffer 返回 图片二进制流
    return await imagePacker.packToData(img, { format: 'image/png', quality: 99 })
  }

  // 图片写入沙箱
  static imgWriteToSandbox(imgArrayBuffer: ArrayBuffer) {
    // 获取上下文
    const ctx = AppStorage.get<Context>(CONTEXT)
    // 创建图片路径
    const imgPath = ctx?.cacheDir + '/' + Date.now() + '.png'
    // 创建文件
    const file = fileIo.openSync(imgPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
    // 写入文件
    fileIo.writeSync(file.fd, imgArrayBuffer)
    // 关闭文件
    fileIo.closeSync(file.fd)
    return imgPath
  }

  // 将沙箱图片写入相册
  static async sandboxImgToAlbum(imgPath: string) {
    // 获取上下文
    const ctx = AppStorage.get<Context>(CONTEXT)
    // 获取图片uri
    const uri = fileUri.getUriFromPath(imgPath)
    // 创建相册资源请求
    const photoRequest = photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(ctx, uri)
    // 获取相册访问实例
    const photoHelper = photoAccessHelper.getPhotoAccessHelper(ctx)
    // 写入相册
    await photoHelper.applyChanges(photoRequest)
  }
}


export { SaveAlbum }

📌 示例调用方式

// 截图组件
const imgArrayBuffer = await SaveAlbum.componentShot('share')

// 图片写入沙箱
const imgPath = SaveAlbum.imgWriteToSandbox(imgArrayBuffer)

// 将沙箱图片写入相册
await SaveAlbum.sandboxImgToAlbum(imgPath)

ShowToast 轻提示封装

功能说明:

  • 引用鸿蒙原生promptAction二次封装,提供统一调用接口
  • 对于简单的提示场景,直接使用 showToastmsg 即可
  • 对于简单的交互场景,直接使用 showDialog 即可

工具类方法:

import promptAction from '@ohos.promptAction';
import { window } from '@kit.ArkUI';
import { logger } from '.'

interface IShowDialog {
  title: string;
  message: string;
  buttons?: IDialogButton[]
}

interface IShowToast {
  message: string;
  type?: EToastTypeColor;
  duration?: number;
}

enum EToastTypeColor {
  Success = '#00B400',
  Warning = '#FFA500',
  Error = '#FF0000',
  Info = '#666666'
}


/**
 * ShowToast 类
 * 功能:封装统一的提示工具,包括轻提示(Toast)、弹窗确认(Dialog)和第三方 HUD 的初始化配置。
 */
class ShowToast {

  /**
   * 显示轻提示 Toast
   * 默认类型为 Info,时长为 2000 毫秒。
   *
   * @param opt - 提示参数对象,包含 message、type 和 duration
   */
  showToastmsg(opt: IShowToast) {
    promptAction.showToast({
      message: opt.message,
      textColor: opt.type || EToastTypeColor.Info, // 文字颜色,默认 Info 类型颜色
      duration: opt.duration || 2000, // 显示时长,默认 2 秒
    })
  }

  /**
   * 显示确认对话框 Dialog
   * 支持自定义标题、内容和按钮。默认按钮为“取消”和“确定”。
   *
   * @param opt - 弹窗参数对象,包含 title、message 和 buttons
   * @returns Promise<number> 点击按钮的索引(0 取消,1 确定)
   */
  showDialog(opt: IShowDialog) {
    return promptAction.showDialog({
      title: opt.title || '提示',  // 标题,默认 "提示"
      message: opt.message || '确定?', // 内容,默认 "确定?"
      buttons: [
        {
          text: (opt.buttons && opt.buttons[0].text) || '取消',
          color: $r('app.color.common_gray_03'), // 取消按钮颜色
        },
        {
          text: (opt.buttons && opt.buttons[1].text) || '确定',
          color: $r('app.color.common_main_color'),// 确定按钮颜色
        },
      ],
    })
  }
}

// 导出单例实例
export const showToast = new ShowToast();

使用方法

// 显示轻提示 Toast
showToast.showToastmsg({ message: '这是一条提示', type: EToastTypeColor.Info });

// 显示确认对话框 Dialog
showToast.showDialog({ title: '退出登录', message: '是否确认退出?',
  buttons: [
    { text: '取消', color: '#999' },
    { text: '确定', color: '#FF0000' }
  ]
})
const res = await showToast.showDialog({ title: '提示',message: '确定执行此操作?'})
if (res.index === 1) {  
   // 用户点击了“确定”
}

XTHUD 三方全局消息提示工具封装

功能说明:

  • 引用鸿蒙三方库 @jxt/xt_hud二次封装配置,便于使用
  • 对于复杂交互或需要 Loading、Progress的场景,可结合该三方库调用

ShowToast封装的工具类中需要获取上下文this

    • EntryAbilityonCreate生命周期钩子函数中:
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  ...
  // 通过应用状态管理对上下文进行状态创建设置
  AppStorage.setOrCreate('context', this.context)
}
    • XTPromptInit工具方法获取窗口对象中进行获取使用
async XTPromptInit() {
    try {
      const ctx = AppStorage.get<Context>(CONTEXT)
      if (ctx) {
        // 获取窗口对象
        const win = await window.getLastWindow(ctx);
        ...
      }
    }
  }
}

工具类方法:

import {
  XTPromptHUD,
  XTHUDToastOptions,
  XTHUDLoadingOptions,
  XTHUDProgressOptions,
  XTHUDReactiveBaseOptions
} from '@jxt/xt_hud'
import { window } from '@kit.ArkUI';
import { logger } from '.';
import { CONTEXT } from '../constants';


/**
 * [XTPrompt](#class-xtprompt) 类用于封装第三方 HUD 提示组件提供统一的初始化接口和全局配置能力。
 *
 * 该类负责完成以下功能:
 * - 异步初始化 HUD 组件并配置其全局样式与行为
 * - 提供单例访问方式以调用具体提示功能(如 Toast、Loading、Progress 等)
 * - 支持延迟初始化,确保资源按需加载
 *
 * ## 使用说明
 * 在项目中使用前,请先确保已安装依赖库:@jxt/xt_hud
 *
 * ### 示例代码
 * import { xTPrompt } from './commons/utils/XTPrompt'
 *
 * // 初始化 HUD(首次调用时会触发全局配置)
 * const prompt = xTPrompt.init()
 *
 * // 显示 Toast 提示
 * prompt.showToast('操作成功')
 *
 * // 显示 Loading 加载动画
 * prompt.showLoading('数据加载中...')
 *
 * // 显示带进度条的提示
 * prompt.showProgress(0.5, '上传中...') // 50% 进度
 *
 * // 隐藏所有提示
 * prompt.hideAll()
 *
 */

class XTPrompt {
  /**
   * 获取或初始化 `XTPromptHUD` 实例。
   * 如果实例尚未创建,则调用 `XTPromptInit()` 方法进行初始化。
   * 此方法支持延迟初始化,避免不必要的资源浪费。
   */
  init() {
    const ctx = AppStorage.get<Context>(CONTEXT) // 获取上下文
    ctx && this.XTPromptInit(ctx)
  }

  /**
   * 异步初始化 `XTPromptHUD` 并配置全局样式和行为。
   * 该方法主要执行以下配置:
   * - **Toast**:启用队列模式,防止多个提示同时弹出造成混乱
   * - **Loading**:设置模态显示,遮罩层透明度为 50%,字体大小为 12
   * - **Progress**:设置模态显示
   * - **ReactiveBase**:用于可交互进度条等场景,同样设置为模态显示
   *
   * @throws 如果获取窗口失败或配置过程中出现异常,错误信息将被记录到日志中
   */
  private async XTPromptInit(ctx: Context) {
    try {
      // const ctx = AppStorage.get<Context>(CONTEXT) // 获取上下文
      if (ctx) {
        // 获取窗口对象
        const win = await window.getLastWindow(ctx);

        // Toast 全局配置:启用队列模式
        XTPromptHUD.globalConfigToast(win.getUIContext(),
          (options: XTHUDToastOptions) => {
            options.isQueueMode = true
          })

        // Loading 全局配置:模态显示,遮罩层透明度为 50%,字体大小为 12
        XTPromptHUD.globalConfigLoading(win.getUIContext(),
          (options: XTHUDLoadingOptions) => {
            options.isModal = true
            options.maskColor = 'rgba(0,0,0,0.5)'
            options.fontSize = 12
          })

        // Progress 全局配置:模态显示
        XTPromptHUD.globalConfigProgress(win.getUIContext(),
          (options: XTHUDProgressOptions) => {
            options.isModal = true
          })

        // ReactiveBase 全局配置(用于进度条等):模态显示
        XTPromptHUD.globalConfigProgress(win.getUIContext(),
          (options: XTHUDReactiveBaseOptions) => {
            options.isModal = true
          })
      }
    } catch (err) {
      logger.error('ShowToast XTPromptInit Error', err)
    }
  }

  //  使用 XTPromptHUD
  use() {
    this.init()
    return XTPromptHUD
  }
}


export const xTPrompt = new XTPrompt()

使用方法

import { xTPrompt } from './commons/utils/XTPrompt'

// Toast 轻提示
xTPrompt.useXTPrompt().showToast('success')

//Loading 加载状态
xTPrompt.useXTPrompt().showLoading('加载中...')  //开启Loading
xTPrompt.useXTPrompt().hideLoading()  //关闭Loading

//Progress 加载进度条
this.progress = 0
this.interval = setInterval(() => {
  this.progress++
  xTPrompt.useXTPrompt().showProgress(this.progress)
  if (this.progress >= 100) {
    clearInterval(this.interval)
    this.interval = null
  }
}, 100)

Request 网络请求封装(基于 Axios)

功能说明:

  • instance创建 axios 实例并设置基础 URL 和超时时间
  • request interceptor自动在请求头中携带 Token
  • response interceptor 统一处理响应数据、Token 失效跳转、提示等
  • Request class封装请求方法,支持泛型调用
  • curl 实例导出可复用的请求工具实例

使用方法:

import axios, { 
  AxiosRequestConfig, 
  AxiosResponse, 
  AxiosError, 
  InternalAxiosRequestConfig 
} from '@ohos/axios'

import { 
  BASE_URL,        // 请求基础地址
  TIME_OUT,        // 请求超时时间
  SUCCESS_CODE,    // 成功状态码
  TOKEN_INVALID_CODE // Token 失效状态码
} from '../constants'

import { 
  authUser,   // 用户认证工具类
  logger,     // 日志记录工具
  showToast   // 提示工具类
} from '../utils'

import { router } from '@kit.ArkUI';         // HarmonyOS 路由模块
import { EToastTypeColor } from '../types';  // Toast 颜色类型枚举


/**
 * 创建一个 axios 实例,并配置基础请求参数。
 *
 * baseURL: 所有请求的基础路径
 * timeout: 请求超时时间(毫秒)
 */
const instance = axios.create({
  baseURL: BASE_URL,
  timeout: TIME_OUT,
})


/**
 * 请求拦截器
 * 在请求发送前对配置进行处理,例如添加 token 到请求头
 *
 * @param config - 当前请求的配置对象
 * @returns 修改后的请求配置对象
 */
instance.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    // 获取用户 token 并添加到 Authorization 请求头
    const token = authUser.getToken()
    config.headers['Authorization'] = token ? 'Bearer ' + token : ''
    return config;
  },
  (error: AxiosError) => {
    // 请求出错时的日志记录
    logger.info(error)
    return Promise.reject(error);
  }
)


/**
 * 响应拦截器
 * 对响应数据进行统一处理,如判断是否登录过期、跳转登录页等
 *
 * @param response - 响应数据
 * @returns 响应中的业务数据或抛出错误
 */
instance.interceptors.response.use(
  (response: AxiosResponse) => {
    // 如果返回的状态码为成功码,返回 data 字段数据
    if (response.data.code === SUCCESS_CODE) {
      return response.data.data
    }
    // 否则以 reject 的方式返回错误信息,便于 catch 捕获
    return Promise.reject(response.data)
  },
  (error: AxiosError) => {
    // 如果是 Token 失效,则清除用户信息并跳转登录页
    if (error.response?.status === TOKEN_INVALID_CODE) {
      authUser.clearUser()
      router.pushUrl({ url: 'pages/LoginPage' }, router.RouterMode.Single)
      showToast.showToastmsg({ 
        message: '登录已过期,请重新登录', 
        type: EToastTypeColor.Warning 
      })
    }
    // 返回错误信息
    return Promise.reject(error.message)
  }
)


/**
 * Request 类
 * 封装基于 axios 实例的通用请求方法
 */
class Request {

  /**
   * 发起网络请求
   *
   * @param config - 请求配置对象
   * @returns 返回泛型 R 类型的数据
   * 泛型R:响应数据类型
   * 泛型Q:请求数据类型
   */
  request<R, Q>(config: AxiosRequestConfig<Q>) {
    return instance<R, R, Q>(config)
  }
}

/**
 * 导出封装好的 http 实例
 * 使用方式:http.request<ResponseData,RequestData>({method: 'GET',url: '/api/user'})
 */
export const curl = new Request()

AuthUser 用户鉴权

功能说明:

  • init()初始化本地持久化存储(用户信息和 Token)
  • getUserInfo()获取当前用户信息
  • setUserInfo() 设置用户信息并更新 Token,触发事件通知
  • setToken()单独设置 Token
  • getToken()获取 Token
  • clearUser()清除用户信息和 Token,触发事件通知
  • authRoutePermission()控制页面跳转权限,无 Token 则跳转登录页

🧠 使用建议:

  • 初始化调用:在应用启动时调用 userStore.initStoragePersistent()
  • 用户登录 设置用户信息:使用 userStore.setUserInfo(userInfo) 设置用户信息。
  • 用户登录 设置TOKEN:使用 userStore.setUserToken(token) 设置TOKEN。
  • 用户登出 清除全部信息:调用 userStore.clearUser() 清除用户信息及TOKEN信息。
  • 页面跳转鉴权:使用 userStore.routerPromise({ url: 'pages/Profile' }) 控制路由权限
  • 按钮点击鉴权:使用 userStore.routerPromise(()=> {}) 控制按钮权限
import { 
  USER_INFO,   // 用户信息存储 key
  TOKEN_KEY,   // Token 存储 key
  EMIT_EVENT   // 用户状态变更事件名
} from '../../commons/constants'

import { IUserData } from '../../models/types' // 用户数据类型定义
import { router } from '@kit.ArkUI'    // HarmonyOS 路由模块
import emitter from '@ohos.events.emitter'  // 事件发射器,用于全局通信


/**
 * AuthUser 类
 * 功能:用户认证与权限管理工具类
 *
 * 包括:
 * - 初始化本地持久化存储
 * - 获取/设置用户信息和 Token
 * - 清除用户信息
 * - 页面跳转鉴权控制
 */
class UserStore {
  initStoragePersistent() {
    PersistentStorage.persistProp(USER_TOKEN, '')
    PersistentStorage.persistProp(USER_INFO, {} as IUserData)
  }

  // 获取用户TOKEN
  getUserToken(): string {
    return AppStorage.get(USER_TOKEN) || ''
  }

  // 设置用户TOKEN
  setUserToken(token: string) {
    AppStorage.set(USER_TOKEN, token)
  }

  // 获取用户信息
  getUserInfo(): IUserData {
    return AppStorage.get(USER_INFO) as IUserData
  }

  // 设置用户信息
  setUserInfo(userInfo: IUserData) {
    AppStorage.set(USER_INFO, userInfo)
  }

  // 清除用户信息以及Token
  clearUser() {
    AppStorage.set(USER_TOKEN, '')
    AppStorage.set(USER_INFO, {} as IUserData)
  }

  /**
   * 路由跳转鉴权控制
   * 如果未登录,则跳转至登录页
   *
   * @param opt - 跳转参数或回调函数
   */
  routerPromise(opt: router.RouterOptions | Function) {
    const isHasToken = this.getUserToken()
    if (typeof opt === 'function') {
      // 如果是函数,则根据是否有 Token 决定是否执行
      isHasToken ? opt() : router.pushUrl({ url: 'pages/LoginPage' })
    } else {
      logger.warn('routerPromise opt: ' + opt)
      // 如果是路由配置对象,则包装参数后跳转
      const jumpToLogin = () => {
        const target_url = opt.url
        opt.params = { url: target_url, params: opt.params }
        router.pushUrl({ url: 'pages/LoginPage', params: opt })
      }
      isHasToken ? router.pushUrl(opt.url ? opt : { url: 'pages/Index' }) : jumpToLogin()
    }
  }
}

export const userStore = new UserStore()

SystemCapability 系统能力管理类

HarmonyOS 开放权限文档:系统授权 用户授权

系统权限说明:

  • system_grant 系统权限:在配置文件中,声明应用需要请求的权限后,系统会在安装应用时自动为其进行权限预授予,开发者不需要做其他操作即可使用权限。
  • user_grant 用户权限
    • 在配置文件中,声明应用需要请求的权限,且要设置需要使用的场景+使用原因
    • 调用方法后,应用程序将等待用户授权的结果。如果用户授权,则可以继续访问目标操作。如果用户拒绝授权,则需要提示用户必须授权才能访问当前页面的功能,并引导用户到系统应用“设置”中打开相应的权限。

权限配置:

  • module.json5 通过 "requestPermissions"进行权限配置
"requestPermissions": [
  { "name": "ohos.permission.INTERNET" },
  {
    "name": "ohos.permission.MICROPHONE", // 表示应用需要访问设备的麦克风
    "usedScene": {},  // 定义权限的使用场景 空对象表示默认场景
    "reason": "$string:reason_microphone"
  }
],
  • $string:reason_microphone 资源配置:须对权限场景进行描述

工具类方法:

import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';

/**
 * 系统能力管理类:处理应用权限请求与系统设置跳转
 * 
 * 重要依赖:
 * - 需预先通过 AppStorage 存储 Context 对象
 * - 需要 @kit.AbilityKit 权限管理模块支持
 */
class SystemCapability {
  // 弹框提示用户 - 用户授权
  async requestPermissions(permissions: Permissions[]) {
    // 限管理器实例atManager
    const atManager = abilityAccessCtrl.createAtManager()
    const ctx = AppStorage.get<Context>('context')
    if (ctx) {
      // 使用权限管理器异步地向用户请求权限,传入上下文和权限列表
      const result = await atManager.requestPermissionsFromUser(ctx, permissions)
      // 检查权限请求结果,使用every方法确保所有请求的权限状态都是PERMISSION_GRANTED(已授予)
      // 如果所有权限都被授予,返回true,否则返回false
      return result.authResults.every(result => result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
    }
    // 如果没有获取到上下文对象(ctx为null或undefined),直接返回false
    return false
  }

  // 打开权限设置 - 用户授权
  async openPermissionSetting(permissions: Permissions[]) {
    // 创建权限管理器实例 atManager
    const atManager = abilityAccessCtrl.createAtManager()
    const ctx = AppStorage.get<Context>('context')
    if (ctx) {
      // 这会直接打开系统权限设置界面,而不是在应用内弹出权限请求对话框
      const authResults = await atManager.requestPermissionOnSetting(ctx, permissions)
      // 检查所有权限是否都被授予
      return authResults.every(result => result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
    }
    // 如果没有上下文对象,直接返回 false
    return false
  }
}

export const systemCapability = new SystemCapability()

🧠 使用方法:

  /**
   * 麦克风权限授权校验及引导流程
   * 
   * 执行流程:
   * 1. 请求麦克风权限 → 2. 权限拒绝 → 3. 弹窗提示 → 
   * 4a. 用户选择"去授权" → 跳转设置页 → 未授权则路由返回
   * 4b. 用户选择"离开" → 直接返回首页
   *
   * 执行流程:
   * ┌───────────────────────┐
   * │ 请求麦克风权限          │
   * └──────────┬────────────┘
   *          否│
   * ┌──────────▼────────────┐
   * │ 显示授权引导对话框       │
   * └────┬──────┬───────────┘
   *   是 │      │ 否
   * ┌────▼──┐ ┌▼─────────┐
   * │跳转设置│ │返回首页    │
   * └────┬──┘ └──────────┘
   *     成功?
   *     ┌▼┐
   *   ┌─┴─┴─┐
   *   │返回  │
   *   │首页  │
   *   └─────┘
   * 
   * 注意事项:
   * - 依赖 systemCapability 权限管理类
   * - 使用全局上下文中的路由(router)进行页面跳转
   * - 提示文案需符合隐私政策规范
   */
  async authorizePermission() {
    // 请求麦克风权限(HarmonyOS权限标识符)
    const promise = await systemCapability.requestPermissions(['ohos.permission.MICROPHONE'])

    if (!promise) { // 权限请求被拒绝
      // 显示自定义对话框引导用户授权
      const choose = await showToast.showDialog({
        title: '温馨提示',
        message: '未授权使用麦克风将无法使用该面试录音功能,是否前往设置进行授权?',
        buttons: [{ text: '离开' }, { text: '去授权' }]
      })

      if (choose.index > 0) { // 用户选择"去授权"
        // 跳转系统权限设置页
        const settingResult = await systemCapability.openPermissionSetting(['ohos.permission.MICROPHONE'])
        
        // 设置页未授权则返回上一页
        if (!settingResult) {
          router.back()
        }
      } else { // 用户选择"离开"
        router.back()
      }
    }
  }

Tracking 数据埋点 + 首选项持久化存储

✅ 使用说明

  • 初始化:确保在 AppStorage 中已注入 Context,使用 preferences 实现本地持久化存储
  • 启动追踪:调用 startTracking(startTime, endTime, questionId) 记录单个问题时间
  • 停止并上传:调用 stopUploadTracking()stopUploadTracking(true) 强制上传
  • 清除缓存:手动调用 clearPersistTrackingList() 清除本地数据

Preferences 首选项【持久化存储】

  • 数据存储形式为键值对,键的类型为字符串型,值的存储数据类型仅限数字型、字符型、布尔型、字符串
  • Key键为string类型,要求非空且长度不超过1024个字节
  • 最大长度不超过16 * 1024 * 1024个字节(16MB)
import { preferences } from '@kit.ArkData';

const ctx = AppStorage.get<Context>(CONTEXT)
store = preferences.getPreferencesSync(ctx, { name: 'myStore' });

// 写入
this.store.putSync('startup', 'auto');
// 修改
this.store.putSync('startup', '新值');
// 持久化
this.store.flush()

// 读取
this.store.getSync('startup','')

// 删除
this.store.deleteSync('startup');
// 持久化
this.store.flush()

// 删除实例
preferences.deletePreferences(context, { name: 'myStore' })


工具类方法:

import { IQuestionTarkingReq, IQuesttionTimeItemTarking } from '../../models/types'
import { http, logger } from '../../commons/utils'
import { preferences } from '@kit.ArkData'
import { CONTEXT, TRACK_STORE, TRACK_QUESTION_TIME_KEY } from '../constants'

/**
 * 跟踪类(Tracking)
 *
 * 用于记录和上报用户答题时间数据。
 *
 * 功能概述:
 * - 使用 preferences 实现本地持久化存储
 * - 支持启动追踪、停止追踪并上传数据
 * - 自动批量上传(默认5条)或强制上传
 *
 */
class Tracking {
  mySrore?: preferences.Preferences

  /**
   * 初始化本地存储仓库
   *
   * 如果尚未初始化,则从 AppStorage 获取上下文并创建 preferences 存储实例。
   * 只初始化一次,保证单例模式。
   *
   * @returns 返回初始化后的 preferences 实例
   */
  initMyStore() {
    if (!this.mySrore) {
      const ctx = AppStorage.get<Context>(CONTEXT)
      this.mySrore = preferences.getPreferencesSync(ctx, { name: TRACK_STORE })
    }
    return this.mySrore
  }

  /**
   * 开始记录一个题目的使用时间
   *
   * @param startTime - 题目开始时间戳(毫秒)
   * @param endTime - 题目结束时间戳(毫秒)
   * @param sign - 题目唯一标识符(如 questionId)
   */
  startTracking(startTime: number, endTime: number, sign: string) {
    const trackingItem: IQuesttionTimeItemTarking = {
      startTime,
      endTime,
      questionId: sign
    }
    let trackingList = this.getPersistTrackingList()
    trackingList.push(trackingItem)
    this.setPersistTrackingList(trackingList)
  }

  /**
   * 获取本地存储中的追踪列表
   *
   * @returns 当前存储的所有题目时间记录
   */
  getPersistTrackingList(): IQuesttionTimeItemTarking[] {
    const list = this.initMyStore().getSync(TRACK_QUESTION_TIME_KEY, '[]')
    return JSON.parse(list as string)
  }

  /**
   * 将追踪列表保存到本地存储
   *
   * @param list - 题目时间记录数组
   */
  setPersistTrackingList(list: IQuesttionTimeItemTarking[]) {
    this.initMyStore().putSync(TRACK_QUESTION_TIME_KEY, JSON.stringify(list))
    this.initMyStore().flush()
  }

  /**
   * 清空本地存储的追踪数据
   */
  clearPersistTrackingList() {
    this.initMyStore().deleteSync(TRACK_QUESTION_TIME_KEY)
    this.initMyStore().flush()
  }

  /**
   * 停止追踪并上传数据
   *
   * 当本地记录达到5条时自动上传,也可通过 forceUpload 参数强制上传。
   *
   * @param forceUpload - 是否强制上传,默认 false
   */
  async stopUploadTracking(forceUpload: boolean = false) {
    try {
      const trackingList = this.getPersistTrackingList()
      if (trackingList.length === 0) {
        return
      } else {
        logger.info(trackingList)
      }
      if (trackingList.length >= 5 || forceUpload) {
        await http.request<null, IQuestionTarkingReq>({
          url: 'time/tracking',
          method: 'POST',
          data: {
            timeList: trackingList
          }
        })
        logger.info('上传成功')
        this.clearPersistTrackingList()
      }
    } catch (err) {
      logger.error(err)
    }
  }
}

// 导出单例实例,供全局使用
export const tracking = new Tracking()

📌 示例调用方式

// 记录用户停留时间
tracking.startTracking(Date.now(), Date.now() + 60000, 'question_001')
  
// 上传数据
tracking.stopUploadTracking() // 条件满足时自动上传
tracking.stopUploadTracking(true) // 强制上传

// 清除缓存数据
tracking.clearPersistTrackingList()

AvPlay 音频播放类封装

✅ 重要依赖

  • 需要 @kit.MediaKit 媒体模块 media支持
  • 需要 @kit.CoreFileKit 文件操作模块 fileIo支持
import { media } from '@kit.MediaKit' // HarmonyOS 媒体播放模块
import { fileIo } from '@kit.CoreFileKit' // 文件操作模块
import { SOUND_BASE_URL } from '../constants' // 网络音频资源基础路径

/**
 * 音频播放类封装,用于统一管理网络和本地音频播放逻辑
 */
class AvPlay {
  // 可选的 AVPlayer 实例,用于控制音频播放
  avplay?: media.AVPlayer

  // 音频资源的基础路径
  // 有道在线播放Url:SOUND_BASE_URL = 'https://dict.youdao.com/dictvoice?type=1&audio='
  private sourceUrl: string = SOUND_BASE_URL

  /**
   * 初始化 AVPlayer 实例
   * 创建一个音视频播放器对象,用于后续播放操作
   *
   * @returns 返回创建好的 AVPlayer 实例
   *
   * ✅ 使用方式:
   * const player = await avPlay.initAvplay()
   */
  private async initAvplay(): Promise<media.AVPlayer> {
    this.avplay = await media.createAVPlayer()
    return this.avplay
  }

  /**
   * 播放网络音频文件
   * 根据传入的音频文件名拼接完整 URL,并绑定状态监听器进行播放控制
   *
   * @param oriSoundText 原始音频文件名称(不包含路径)
   * @returns 返回已开始播放的 AVPlayer 实例
   *
   * ✅ 使用方式:
   * 直接传入需要播放的英文单词
   * avPlay.playAvplaySource('music')
   *
   * 📌 获取播放时间示例:
   * const _avplayer = await avPlay.playAvplaySource('music')
   * _avplayer.on('timeUpdate', t => {
   *   console.log(`当前播放时间:${t}ms`)
   * })
   */
  async playAvplaySource(oriSoundText: string): Promise<media.AVPlayer> {
    const _avplay = await this.initAvplay()

    // 设置音频播放地址
    _avplay.url = this.sourceUrl + oriSoundText

    // 监听播放器状态变化
    _avplay.on('stateChange', state => {
      if (state === 'initialized') {
        // 当播放器初始化完成,准备加载音频
        _avplay.prepare()
      } else if (state === 'prepared') {
        // 音频加载完成后开启循环播放并开始播放
        _avplay.loop = true
        _avplay.play()
      }
    })

    return _avplay
  }

  /**
   * 播放本地指定路径的音频文件
   * 根据传入的本地音频文件路径,创建一个 AVPlayer 实例并播放该音频
   *
   * @param filePath 本地音频文件路径
   * @returns 返回已开始播放的 AVPlayer 实例
   *
   * ✅ 使用方式:
   * 传入音频文件地址filePath
   * avPlay.playLocalSource('/storage/sdcard/music.mp3')
   *
   * 📌 获取播放时间示例:
   * const _avplayer = await avPlay.playLocalSource('/storage/sdcard/music.mp3')
   * _avplayer.on('timeUpdate', t => {
   *   console.log(`当前播放时间:${t}ms`)
   * })
   */
  async playLocalSource(filePath: string): Promise<media.AVPlayer> {
    const _avplay = await this.initAvplay()
    const file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY)

    // 监听播放器状态变化
    _avplay.on('stateChange', state => {
      if (state === 'initialized') {
        // 当播放器初始化完成,准备加载音频
        _avplay.prepare()
      } else if (state === 'prepared') {
        // 音频加载完成后开启循环播放并开始播放
        _avplay.loop = true
        _avplay.play()
      }
    })

    // 设置本地音频文件描述符作为播放源
    _avplay.url = `fd://${file.fd}`

    return _avplay
  }

  /**
   * 停止并释放指定的 AVPlayer 实例
   *
   * @param _avplay 要关闭的 AVPlayer 实例
   *
   * ✅ 使用方式:
   * const player = await avPlay.playAvplaySource('music.mp3')
   * avPlay.stopAvplay(player)
   */
  stopAvplay(_avplay?: media.AVPlayer) {
    if (_avplay) {
      _avplay.stop()
      _avplay.release()
    }
  }
}

// 导出单例实例,供全局使用
export const avPlay = new AvPlay()

AvRecord 音频录制类封装

✅ 重要依赖

  • 需要 @kit.MediaKit 媒体模块 media支持
  • 需要 @kit.CoreFileKit 文件操作模块 fileIo支持
  • 确保在 AppStorage 中已注入 Context
import { media } from "@kit.MediaKit";
import { fileIo } from "@kit.CoreFileKit";
import { CONTEXT } from '../constants';

/**
 * 音频录制类 `AvRecord`
 * 提供基于 ArkTS 的音频录制功能封装。
 * 支持多种音频格式(如 MP4、M4A、MP3、WAV 等),通过 `ContainerFormatType` 控制输出格式。
 */
 
class AvRecord {
  // AVRecorder 实例,用于控制音频录制。
  avRecord?: media.AVRecorder;
  // 文件描述符,用于标识打开的音频文件。
  fileFD?: number

  /**
   * 根据指定的容器格式配置生成录制配置对象。
   * @param type - 容器格式类型
   *
   * - `MPEG_4` (mp4)
   * - `CFT_MP3` (mp3)
   * - `CFT_MPEG_4A` (m4a)
   * - `CFT_WAV` (wav)
   *
   * 默认配置:
   * - 音频源:麦克风输入 (`AUDIO_SOURCE_TYPE_MIC`)
   * - 编码格式:AAC (`AUDIO_AAC`)
   * - 比特率:100000 bps
   * - 声道数:1 (单声道)
   * - 采样率:48000 Hz
   * - 输出路径:使用文件描述符 URL 格式 `fd://${this.fileFD}`
   *
   * @returns 返回一个 `AVRecorderConfig` 对象
   */
  private record_config(type: media.ContainerFormatType): media.AVRecorderConfig {
    return {
      audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
      profile: {
        audioBitrate: 100000,
        audioChannels: 1,
        audioCodec: media.CodecMimeType.AUDIO_AAC,
        audioSampleRate: 48000,
        fileFormat: type,
      },
      url: `fd://${this.fileFD}`
    }
  }

  /**
   * 开始录制音频。
   *
   * ✅ 使用方式:
   * await avRecord.startRecord('recording_20250405', media.ContainerFormatType.MPEG_4);
   *
   * 支持的音频格式包括但不限于:
   * - `MPEG_4` (mp4)
   * - `CFT_MP3` (mp3)
   * - `CFT_MPEG_4A` (m4a)
   * - `CFT_WAV` (wav)
   *
   * @param uniqueID - 录音文件的唯一标识符(通常为文件名前缀)
   * @param type - 容器格式类型
   */
  async startRecord<T>(uniqueID: T, type: media.ContainerFormatType) {
    const ctx = AppStorage.get<Context>(CONTEXT);
    const filePath = ctx?.filesDir + '/' + uniqueID + '.' + type
    const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
    this.fileFD = file.fd
    const red_config = this.record_config(type)
    const avRecord = await media.createAVRecorder()
    await avRecord.prepare(red_config)
    await avRecord.start()
    this.avRecord = avRecord
    return filePath
  }

   /**
   * 获取当前录音的最大振幅值(用于实现录音波形或音量指示)
   *
   * ✅ 使用方式:
   * const amplitude = await avRecord.getRecorderAmplitude();
   * console.log(当前振幅:${amplitude});
   *
   * @returns 返回当前音频采集的最大振幅值
   */
  async getRecorderAmplitude(): Promise<number> {
    if (this.avRecord) {
      return await this.avRecord?.getAudioCapturerMaxAmplitude()
    }
    return 0
  }

  /**
   * 停止并释放当前录音资源。
   *
   * ✅ 使用方式:
   * - await avRecord.stopRecord();
   *
   * 此方法会执行以下操作:
   * 1. 停止正在运行的录音任务;
   * 2. 释放 `AVRecorder` 资源;
   * 3. 关闭对应的文件描述符,确保文件正确保存。
   */
  async stopRecord() {
    if (this.avRecord) {
      await this.avRecord.stop()
      await this.avRecord.release()
      fileIo.closeSync(this.fileFD)
    }
  }
}

/**
 * 导出 `AvRecord` 单例实例,供全局调用。
 *
 * 使用方式:
 * import { avRecord } from '@/utils/AvRecord';
 * await avRecord.startRecord('demo', media.ContainerFormatType.CFT_MPEG_4A);
 * await avRecord.stopRecord();
 */
export const avRecord = new AvRecord();


UpLoadIMG 调取图片选择器 图片上传

✅ 重要依赖

  • 需要 @kit.MediaLibraryKit 媒体管理服务 photoAccessHelper 相册模块支持
  • 需要 @ohos.file.fs 文件管理 fs 文件模块支持
  • 需要 @kit.CoreFileKit 文件基础服务 fileIo操作能力支持
  • 需要 @kit.ArkTSutil 工具支持
  • 需要 @kit.ImageKit图片服务 image 图片模块支持
  • 确保在 AppStorage 中已注入 Context
import fs from '@ohos.file.fs';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';
import { image } from '@kit.ImageKit';
import { CONTEXT } from '../constants';

type ImageFormat = '.jpg' | '.jpeg' | '.png' | '.gif' | '.webp' | '.bmp' | '.svg' | '.avif';

class UpLoadIMG {
  /**
   * 设置视图选择器,用于从相册中选择图片
   * 通过配置相册访问参数并调用相册选择器,以获取用户选择的图片路径
   * 主要涉及相册访问配置、相册实例创建、图片选择和路径获取等步骤
   *
   * @returns {Promise<Array<string>>}
   * 返回一个Promise,解析为用户选择的图片的完整路径数组
   */
  async setViewPicker() {
    // 配置图片参数实例
    const opt = new photoAccessHelper.PhotoSelectOptions()
    // 设置相册最大选择图片数量
    opt.maxSelectNumber = 1
    // 指定可选择类型 (image/* | video/* | image/movingPhoto)
    opt.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE

    // 创建调用相册实例
    const albumEx = new photoAccessHelper.PhotoViewPicker()
    // 选择图片 返回promise  可通过 async/await 获取结果
    const selectRes = await albumEx.select(opt)
    // 获取图片完整路径数组
    return selectRes.photoUris
  }

  /**
   * 将图片文件 拷贝/压缩 到沙箱环境
   *
   * 此函数用于将给定URI的文件复制到应用的沙箱目录中,在复制过程中可以保留原文件的格式
   * 它首先生成一个唯一的文件名,以避免文件重复,然后打开源文件,并在沙箱目录中创建一个新文件,
   * 最后将源文件内容复制到新文件中此函数支持常见的图片格式复制
   *
   * 参数说明:
   * @param uri 文件的URI,指定要复制的文件路径
   * @param fileSuffix 文件后缀名,决定了复制后的文件格式,默认为'.jpg',
   * @param isCompress 是否压缩,默认为false,不压缩
   *
   * 可选格式包括 '.jpg' | '.jpeg' | '.png' | '.gif' | '.webp' | '.bmp' | '.svg' | '.avif';
   *
   * @returns 返回复制到沙箱后的文件名
   */
  async fileToSandbox(uri: string, fileSuffix: ImageFormat = '.jpg', isCompress: boolean = false) {
    // 获取应用状态管理的’context‘上下文
    const ctx = AppStorage.get<Context>(CONTEXT)
    // 调用util工具模块生成唯一的文件名称
    const fileName_new = util.generateRandomUUID() + fileSuffix
    // 定义目标文件路径,存储拷贝后的文件(沙箱目录)
    const targetFilePath = getContext(ctx).cacheDir + '/' + fileName_new

    // 【只读模式】打开源文件
    const sourceFile = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY)
    // 【读写模式】创建目标文件
    const targetFile = fileIo.openSync(targetFilePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)

    // 【压缩】是否压缩图片
    if (isCompress) {
      // 【可对原始图片进行压缩处理】 使用 packing 压缩图片,二进制图片数据
      //  通过文件描述符创建图像源,用于后续压缩处理;
      const imageSource = image.createImageSource(sourceFile.fd)
      //  创建图像打包器,用于将图像数据打包为特定格式
      const imagePacker = image.createImagePacker()
      //  使用 packToData 方法将图像压缩为 JPEG 格式,质量设置为 70%
      const arrayBuffer = await imagePacker.packToData(imageSource, { format: 'image/jpeg', quality: 70 })

      // 【将压缩后的图像数据写入目标文件
      fileIo.writeSync(targetFile.fd, arrayBuffer)
    } else {
      // 【复制】将打开的文件同步复制到新构建文件路径
      fileIo.copyFileSync(sourceFile.fd, targetFile.fd)
    }

    //关闭打开的【源文件|目标文件】[文件必须关闭,可能会导致资源泄露/文件锁定]
    fileIo.closeSync(sourceFile.fd)
    fileIo.closeSync(targetFile.fd)

    return fileName_new
  }

  /**
   * 文件转换为ArrayBuffer格式
   * 部门接口需求要求上传文件格式为ArrayBuffer格式
   *
   * @param file_name 沙箱文件名
   * @returns 返回内容为ArrayBuffer格式
   *
   *  使用说明:
   *  1.创建一个新的FormData对象,用于准备文件上传 这是axios上传图片需要的对象类型 FormData
   *  const formData: FormData = new FormData()
   *  2.将读取到的二进制数据作为字段 'file' 添加到 FormData 对象中
   *  formData.append('file', buf2);
   *  3.这在上传时可以告诉服务器这个文件的名称和类型
   *  formData.append('file', buf2, { filename: 'text.txt', type: 'text/plain'});
   *  4.设置请求头
   *  {headers: {'Content-Type': 'multipart/form-data'},context: getContext(ctx)}
   */
  isTypeArrayBuffer(file_name: string) {
    // 获取应用状态管理的’context‘上下文
    const ctx = AppStorage.get<Context>(CONTEXT)
    // 获取当前应用的缓存目录路径
    let cacheDir = getContext(ctx).cacheDir

    try {
      let path = cacheDir + file_name;
      // 读取
      // 以只读模式 (0o2) 打开文件
      let file2 = fs.openSync(path, 0o2);
      // lstatSync(): 获取文件元信息,如大小
      let stat = fs.lstatSync(path);
      // 创建一个 ArrayBuffer 用于存储文件内容
      let targetArrayBuffer = new ArrayBuffer(stat.size);
      // 同步从文件中读取数据到 buf2 缓冲区
      fs.readSync(file2.fd, targetArrayBuffer);
      // 同步刷新缓冲区并关闭文件
      fs.fsyncSync(file2.fd);
      fs.closeSync(file2.fd);
      return targetArrayBuffer
    } catch (err) {
      // 捕获可能发生的异常,例如文件权限问题、路径无效等
      console.info('err:' + JSON.stringify(err));
      return ''
    }
  }

  /**
   * 文件转换为uri格式
   * uri支持: internal协议类型(internal) | 沙箱路径(path)
   *
   *  使用说明:
   *  1.创建一个新的FormData对象,用于准备文件上传 这是axios上传图片需要的对象类型 FormData
   *  const formData: FormData = new FormData()
   *  2.添加一个名字叫img的数据  数据需要 `internal://cache/在沙箱目录的文件地址`
   *  formData.append('img', 'internal://cache/' + uri)
   *  3.设置请求头
   *  {headers: {'Content-Type': 'multipart/form-data'},context: getContext(ctx)}
   * */
  isTypeURI(file_name: string, type: 'internal' | 'path') {
    // 获取应用状态管理的’context‘上下文
    const ctx = AppStorage.get<Context>(CONTEXT)
    if (type === 'internal') {
      // internal协议
      return 'internal://cache/' + file_name
    } else {
      // 沙箱路径
      let cacheDir = getContext(ctx).cacheDir
      return cacheDir + '/' + file_name
    }
  }
}

export const upLoadIMG = new UpLoadIMG()

JsonFileToObject JSON文件内容转对象数据

✅ 重要依赖

  • 需要 @kit.ArkTSutil JSON 工具支持
  • 确保在 AppStorage 中已注入 Context

⚠️ .json文件存放路径 src/main/resources/rawfile/xxx.json

import { jsonFileToObject, logger } from '.'
import { IVocabulary } from '../types'

/**
 * JsonFileToObject 类用于将 JSON 文件转换为指定类型的对象。
 * 将指定的 JSON 文件内容解析为类型为 T 的对象。
 *
 * @param jsonFileName - 要读取的 JSON 文件名(包含路径)。
 * @returns 返回解析后的对象,类型为 T。
 *
 * **使用示例:**
 * 1.json文件路径 /main/resources/fawfile/data.json
 * 2.定义接口
 * interface Ixxx {
 *    name: string;
 *    age: number;
 * }
 *
 * const originalData = jsonFileToObject.jsonToObject<Record<string, Ixxx[]>>("xxx.json");
 * Object.keys(originalData)
 */
class JsonFileToObject {
  jsonToObject<T>(jsonFileName: string): T {
    const ctx = AppStorage.get<Context>(CONTEXT)
    // 获取数据文件转为二进制
    const buffer = ctx?.resourceManager.getRawFileContentSync(jsonFileName)
    // 实例化解析文本
    const text = new util.TextDecoder()
    // 将二进制转为json文本
    const jsonText = text.decodeToString(buffer)
    // 将json文本转为对象
    return JSON.parse(jsonText.toString()) as T
  }
}

export const jsonFileToObject = new JsonFileToObject()

Theme 应用主题色

✅ 重要依赖

  • 需要 @kit.AbilityKitConfigurationConstant 支持
  • 确保在 AppStorage 中已注入 Context
import { CONTEXT, THEME_MODE_COLOR } from "../constants"
import { ConfigurationConstant } from "@kit.AbilityKit"

/**
 * 设置应用主题色
 * */
class Theme {
  // 初始化主题色本地持久化仓库 初始化应用默认: 亮色
  initTheme() {
    PersistentStorage.persistProp(THEME_MODE_COLOR, ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT)
    this.setTheme()
  }

  // 设置主题色
  setTheme() {
    const ctx = AppStorage.get<Context>(CONTEXT)
    const mode = AppStorage.get<ConfigurationConstant.ColorMode>(THEME_MODE_COLOR)
    if (ctx) {
      ctx.getApplicationContext().setColorMode(mode)
    }
  }

  //跟随系统
  notSet() {
    AppStorage.setOrCreate(THEME_MODE_COLOR, ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET)
    this.setTheme()
  }

  //设置深色
  setDark() {
    AppStorage.setOrCreate(THEME_MODE_COLOR, ConfigurationConstant.ColorMode.COLOR_MODE_DARK)
    this.setTheme()
  }

  //设置浅色
  setLight() {
    AppStorage.setOrCreate(THEME_MODE_COLOR, ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT)
    this.setTheme()
  }
}

export const theme = new Theme()

SetCache 应用缓存

✅ 重要依赖

  • 需要 @kit.CoreFileKitstorageStatistics fileIo 支持
  • 确保在 AppStorage 中已注入 Context
import { fileIo, storageStatistics } from '@kit.CoreFileKit'
import { CONTEXT } from '../constants'

class SetCache {
  // 将字节转换为MB
  private byteToMB(byte: number): string {
    return (byte / 1024 / 1024).toFixed(2) + 'MB'
  }

  // 获取应用缓存大小
  async getAbilityCacheSize(): Promise<string> {
    // 获取应用缓存大小
    const r = await storageStatistics.getCurrentBundleStats()
    return this.byteToMB(r.cacheSize)
  }

  /**
   * 清空应用缓存
   *
   * 返回 清除缓存后的大小
   * @returns Promise<string>
   *
   * */
  clearAbilityCache(): Promise<string> {
    const ctx = AppStorage.get<Context>(CONTEXT)

    // Hap包 缓存清除
    const cacheDir = ctx?.cacheDir
    const isPackageFlag = fileIo.accessSync(cacheDir)
    isPackageFlag && fileIo.rmdirSync(cacheDir)

    // 应用缓存
    const appCacheDir = ctx?.getApplicationContext()
    const appFlag = fileIo.accessSync(appCacheDir?.cacheDir)
    appFlag && fileIo.rmdirSync(appCacheDir?.cacheDir)

    return this.getAbilityCacheSize()
  }
}

export const setCache = new SetCache()

CheckSystemInfo 应用信息查询

✅ 重要依赖

  • 需要 @kit.AbilityKitbundleManager 支持
  • 确保在 AppStorage 中已注入 Context
import { bundleManager } from "@kit.AbilityKit"

class CheckSystemInfo {
  //  获取应用版本号
  async getVersion(): Promise<string> {
    const r = await bundleManager.getBundleInfoForSelf(
      bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
    )
    return r.versionName
  }
}

export const checkVersion = new CheckSystemInfo()

SetBreakPoint 一多页面断点适配工具

import { window } from '@kit.ArkUI';
import { BREAK_POINT_W } from 'commonconstant';

/**
 * 接口 IBreakPonit 定义了断点配置的结构。
 * @typeparam T - 配置值的类型。
 */
interface IBreakPonit<T> {
  XS?: T; // 超小屏幕断点配置
  SM?: T; // 小屏幕断点配置
  MD?: T; // 中等屏幕断点配置
  LG?: T; // 大屏幕断点配置
  XL?: T; // 超大屏幕断点配置
}

/**
 * 类 SetBreakPoint 用于根据窗口宽度设置和获取断点配置。
 * @typeparam T - 配置值的类型。
 */
class SetBreakPoint<T> {
  opt?: IBreakPonit<T>;

  /**
   * 构造函数,初始化断点配置。
   * @param opt - 可选的断点配置对象。
   */
  constructor(opt?: IBreakPonit<T>) {
    this.opt = opt;
  }

  /**
   * 获取指定断点的配置值。
   * @param widthBreakpoint - 断点名称('XS' | 'SM' | 'MD' | 'LG' | 'XL')。
   * @returns 返回对应的配置值。
   *
   * 示例:
   * const config = new SetBreakPoint({ XS: 400, SM: 600 });
   * console.log(config.getBreakPointResult('XS')); // 输出: 400
   */
  getBreakPointResult(widthBreakpoint: 'XS' | 'SM' | 'MD' | 'LG' | 'XL') {
    switch (widthBreakpoint) {
      case 'XS':
        return this.opt?.XS;
      case 'SM':
        return this.opt?.SM;
      case 'MD':
        return this.opt?.MD;
      case 'LG':
        return this.opt?.LG;
      case 'XL':
        return this.opt?.XL;
    }
  }

  /**
   * 将窗口宽度转换为对应的断点名称。
   * @param size - 窗口宽度断点(WidthBreakpoint 类型)。
   * @returns 返回对应的断点名称。
   *
   * 示例:
   * const breakpoint = config.setBreakPointSize(WidthBreakpoint.WIDTH_SM);
   * console.log(breakpoint); // 输出: 'SM'
   */
  private setBreakPointSize(size: WidthBreakpoint) {
    switch (size) {
      case WidthBreakpoint.WIDTH_XS:
        return 'XS';
      case WidthBreakpoint.WIDTH_SM:
        return 'SM';
      case WidthBreakpoint.WIDTH_MD:
        return 'MD';
      case WidthBreakpoint.WIDTH_LG:
        return 'LG';
      case WidthBreakpoint.WIDTH_XL:
        return 'XL';
    }
  }

  /**
   * 获取当前窗口的断点,并存储到 AppStorage 中。
   * @param window - 当前窗口对象。
   *
   * 示例:
   * const windowStage = getWindowStage(); // 假设 getWindowStage 是获取 WindowStage 的方法
   * config.getBreakPoint(windowStage);
   * console.log(AppStorage.get(BREAK_POINT_W)); // 输出: 当前窗口对应的断点名称
   */
  getBreakPoint(window: window.WindowStage) {
    const w_breakpoint = window.getMainWindowSync().getUIContext().getWindowWidthBreakpoint();
    const target_res = this.setBreakPointSize(w_breakpoint);
    AppStorage.setOrCreate(BREAK_POINT_W, target_res);
  }
}

export { SetBreakPoint };

/**
 * 获取断点示例:
 * 1.UIAbility生命周期中应用窗口加载成功后,获取窗口断点
 * new SetBreakPoint<null>().getBreakPoint(windowStage)
 * 2.同时注册监听窗口变化事件,当窗口发生变化实时更新Appstore中存储的断点值
 * windowStage.getMainWindowSync().on('windowSizeChange', () => {
 *   new SetBreakPoint<null>().getBreakPoint(windowStage)
 * })
 */

LazyForEachListen LazyForEach循环

/**
 * LazyForEachListen 
 * 类实现了一个数据源接口 `IDataSource`,
 * 用于管理监听器并支持懒加载的数据更新机制。
 *
 * @typeparam T - 数据数组的泛型类型。
 */
class LazyForEachListen<T> implements IDataSource {
  /**
   * 存储注册的数据。
   */
  private listeners: DataChangeListener[] = [];

  /**
   * 存储原始数据数组。
   */
  private originDataArray: T[] = [];

  /**
   * 获取当前数据源中的总数据项数量。
   * @returns 数据项的数量。
   */
  public totalCount(): number {
    return this.originDataArray.length;
  }

  /**
   * 根据索引获取指定位置的数据项。
   * @param index - 数据项的索引。
   * @returns 对应索引的数据项。
   */
  public getData(index: number): T {
    return this.originDataArray[index];
  }

  /**
   * 向监听的数组添加数据。
   * @param listener - 新的数据。
   */
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  /**
   * 从监听数组数据删除指定数据。
   * @param listener - 要删除的指定数据。
   */
  unregisterDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) >= 0) {
      this.listeners.splice(this.listeners.indexOf(listener), 1);
    }
  }

  /**
   * 更新数据源的数据,并注册的监听器。
   * @param list - 新的数据数组
   */
  upDataTargetArr(list: T[]) {
    this.originDataArray = list;
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    });
  }
}

export { LazyForEachListen };

/**
 * @example
 * LazyForEach使用示例:
 *
 * 1.通过懒加载工具类实例化懒加载对象
 * lazyTargetArray: LazyForEachListen<T> = new LazyForEachListen<T>()
 *
 * 2.通过实例对象upDataTargetArr方法传入新数据,会监听器触发onDataReloaded()方法更新
 * this.lazyTargetArray.upDataTargetArr(newList)
 *
 * 3.通过LazyForEach懒加载目标数据源
 * LazyForEach(this.lazyTargetArray, (item: T) => {
 *   ...
 * })
 */


🎯 总结

本篇文章详细介绍了我在 HarmonyOS 开发中常用的工具类及其使用方式,涵盖了从用户认证、网络请求、UI 控制到日志打印等多个方面,适合刚入门的小白直接复制粘贴使用。合理利用这些工具类,可以大幅提升开发效率和代码质量。

如需进一步扩展,也可以基于这些基础工具类添加更多业务逻辑封装。希望对你有所帮助!🚀