面试录音 

应用权限 
应用权限概述 
系统提供了一种允许应用访问系统资源(如:通讯录等)和系统能力(如:访问摄像头、麦克风等)的通用权限访问方式,来保护系统数据(包括用户个人数据)或功能,避免它们被不当或恶意使用。
应用申请敏感权限时,必须填写权限使用理由字段,敏感权限通常是指与用户隐私密切相关的权限,包括地理位置、相机、麦克风、日历、健身运动、身体传感器、音乐、文件、图片视频等权限。参考向用户申请授权。
- system_grant
在配置文件中,声明应用需要请求的权限后,系统会在安装应用时自动为其进行权限预授予,开发者不需要做其他操作即可使用权限。
- user_grant - 在配置文件中,声明应用需要请求的权限,且要设置需要使用的场景+使用原因
- 调用 requestPermissionsFromUser() 方法后,应用程序将等待用户授权的结果。如果用户授权,则可以继续访问目标操作。如果用户拒绝授权,则需要提示用户必须授权才能访问当前页面的功能,并引导用户到系统应用“设置”中打开相应的权限。可参考二次向用户申请权限 requestPermissionOnSetting() 。
 
module.json5
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" },
      {
        "name": "ohos.permission.MICROPHONE",
        "usedScene": {},
        "reason": "$string:reason_microphone"
      }
    ],原因格式:用于xxx模块xxx功能
permission 工具 
目标:封装权限工具,提供请求用户权限,拉起用户权限设置的能力
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
class Permission {
  // 请求用户授权
  async requestPermissions(permissions: Permissions[]) {
    const atManager = abilityAccessCtrl.createAtManager()
    const ctx = AppStorage.get<Context>('context')
    if (ctx) {
      const result = await atManager.requestPermissionsFromUser(ctx, permissions)
      return result.authResults.every(result => result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED)
    }
    return false
  }
  // 打开权限设置 beta3
  async openPermissionSetting(permissions: Permissions[]) {
    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)
    }
    return false
  }
}
export const permission = new Permission()录音授权 
目标:使用权限请求工具,在录音页面实现请求权限,无权限不可进入



permissions: Permissions[] = ['ohos.permission.MICROPHONE']
  confirmConfig:  promptAction.ShowDialogOptions = {
    title: "温馨提示",
    message: "未授权使用麦克风将无法使用该面试录音功能,是否前往设置进行授权?",
    buttons: [
      { text: '离开', color: $r('app.color.common_gray_01') },
      { text: '去授权', color: $r('app.color.black') }
    ]
  }
  async getPermission () {
    try {
      // 第一请求授权
      const isOk = await permission.requestPermissions(this.permissions)
      if (isOk) return
      // 弹窗提示
      const confirm = await promptAction.showDialog(this.confirmConfig)
      if (confirm.index === 1) {
        const isOk2 = await permission.openPermissionSetting(this.permissions)
        if (isOk2) return
      }
      router.back()
    } catch (e) {
      promptAction.showToast({ message: '未授权' })
      router.back()
    }
  }
  aboutToAppear(): void {
    this.getPermission()
  }录音知识 
使用 AvRecorder 录音 
目标:使用 AvRecorder 实现音频录制存储到应用沙箱

实现步骤:
- 需要一个文件接收音频数据
- 准备录音配置
- 使用 AvRecorder 实现开始录音,结束录音
落地代码:
avRecorder?: media.AVRecorder
  fd?: number
  filePath?: string
  async startRecord() {
    // 1. 准备一个文件接收录音
    const ctx = getContext(this)
    const filePath = ctx.filesDir + '/' + Date.now() + '.m4a'
    this.filePath = filePath
    const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
    this.fd = file.fd
    // 2. 准备路由配置对象
    const config: media.AVRecorderConfig = {
      audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
      profile: {
        audioBitrate: 100000, // 音频比特率
        audioChannels: 1, // 音频声道数
        audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac
        audioSampleRate: 48000, // 音频采样率
        fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
      },
      url: `fd://${file.fd}`
    }
    // 3. 开始录制
    const avRecorder = await media.createAVRecorder()
    await avRecorder.prepare(config)
    await avRecorder.start()
    this.avRecorder = avRecorder
  }
  async stopRecord() {
    if (this.avRecorder) {
      await this.avRecorder.stop()
      await this.avRecorder.release()
      fileIo.closeSync(this.fd)
    }
  } Button('开始录音')
        .onClick(() => {
          this.startRecord()
        })
      Button('结束录音')
        .onClick(() => {
          this.stopRecord()
        })录音声音振动效果 
目标:根据声音的大小实现声音振动特效
实现步骤:
- 通过 getAudioCapturerMaxAmplitude 观察音频区间
- 封装振动组件,通过声音振幅数据实现振动效果
落地代码:
1)获取振幅数据,出入振动组件 AudioPage.ets
  timer?: number
  @State maxAmplitude: number = 0    // startRecord 4. 每100ms获取一下声音振幅
    this.timer = setInterval(async () => {
      this.maxAmplitude = await avRecorder.getAudioCapturerMaxAmplitude()
      logger.debug('startRecord', this.maxAmplitude.toString())
    }, 100)    // stopRecord 清理定时器
    clearInterval(this.timer)AudioBoComp({ maxAmplitude: this.maxAmplitude })2)实现振动组件 Audio/AudioBoComp.ets
@Component
struct AudioBoComp {
  @Prop @Watch('onChange') maxAmplitude: number
  @State per: number = 0
  onChange() {
    animateTo({ duration: 100 }, () => {
      if (this.maxAmplitude < 500) {
        this.per = 0
      } else if (this.maxAmplitude > 30000) {
        this.per = 1
      } else {
        this.per = this.maxAmplitude / 30000
      }
    })
  }
  build() {
    Row({ space: 5 }) {
      ForEach(Array.from({ length: 30 }), () => {
        Column()
          .layoutWeight(1)
          .height(this.per * 100 * Math.random())
          .backgroundColor($r('app.color.common_blue'))
      })
    }
    .width('100%')
    .height(100)
  }
}使用 AvPlayer 播放 
目标:能够使用 AvPlayer 播放应用沙箱中的音频文件,且显示进度条
落地代码:
  avPlayer?: media.AVPlayer
  @State total: number = 0
  @State value: number = 0
  async startPlay() {
    try {
      const file = fileIo.openSync(this.filePath, fileIo.OpenMode.READ_ONLY)
      const avPlayer = await media.createAVPlayer()
      avPlayer.on('stateChange', state => {
        if (state === 'initialized') {
          avPlayer.prepare()
        } else if ( state === 'prepared') {
          avPlayer.loop = true
          this.total = avPlayer.duration
          avPlayer.play()
        }
      })
      // 当前播放时间改变
      avPlayer.on('timeUpdate', (time) => {
        this.value = time
      })
      avPlayer.url = `fd://${file.fd}`
      this.avPlayer = avPlayer
    } catch (e) {
      logger.error('startPlay', JSON.stringify(e))
    }
  }
  stopPlay() {
    if (this.avPlayer) {
      this.avPlayer.stop()
      this.avPlayer.release()
    }
  }     Button('开始播放')
        .onClick(() => {
          this.startPlay()
        })
      Button('停止播放')
        .onClick(() => {
          this.stopPlay()
        })
      Progress({ total: this.total, value: this.value })
        .width('100%')关系型数据库知识 
数据库概述 
关系型数据库(Relational Database,RDB)是一种基于关系模型来管理数据的数据库。关系型数据库基于SQLite组件提供了一套完整的对本地数据库进行管理的机制,对外提供了一系列的增、删、改、查等接口,也可以直接运行用户输入的SQL语句来满足复杂的场景需要。不支持Worker线程。
ArkTS侧支持的基本数据类型:number、string、二进制类型数据、boolean。为保证插入并读取数据成功,建议一条数据不要超过2M。超出该大小,插入成功,读取失败。
该模块提供以下关系型数据库相关的常用功能:
- RdbStore:提供管理关系数据库(RDB)方法的接口。
- RdbPredicates:数据库中用来代表数据实体的性质、特征或者数据实体之间关系的词项,主要用来定义数据库的操作条件。
- ResultSet:提供用户调用关系型数据库查询接口之后返回的结果集合。

创建数据库 

CREATE TABLE IF NOT EXISTS article (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
create_time INTEGER NOT NULL
)创建一个文章数据库:
store?: relationalStore.RdbStore
  tableName = 'article'
  async createStore () {
    const store = await relationalStore.getRdbStore(getContext(this), {
      name: 'interview_tong.db',
      securityLevel: relationalStore.SecurityLevel.S1
    })
    store.executeSql(`
        CREATE TABLE IF NOT EXISTS ${this.tableName} (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          title TEXT NOT NULL,
          content TEXT NOT NULL,
          create_time INTEGER NOT NULL
        )
      `)
    this.store = store
  }
  aboutToAppear(): void {
    this.createStore()
  }插入数据 
      Button('添加')
        .onClick(() => {
          this.store?.insert(this.tableName, {
            id: null,
            title: '测试' + Math.random(),
            content: '我是一篇测试文章' + Math.random(),
            create_time: Date.now()
          })
        })查询数据 
Button('查询总条数')
        .onClick(async () => {
          const predicates = new relationalStore.RdbPredicates(this.tableName)
          const resultSet = await this.store?.query(predicates)
          this.total = resultSet?.rowCount || 0
        })
      Text('总条数' + this.total)
      Button('查询所有数据')
        .onClick(async () => {
          const predicates = new relationalStore.RdbPredicates(this.tableName)
          const resultSet = await this.store?.query(predicates)
          const list: ArticleItem[] = []
          while (resultSet?.goToNextRow()) {
            list.push({
              id: resultSet.getLong(resultSet.getColumnIndex('id')),
              title: resultSet.getString(resultSet.getColumnIndex('title')),
              content: resultSet.getString(resultSet.getColumnIndex('content')),
              create_time: resultSet.getLong(resultSet.getColumnIndex('create_time'))
            })
          }
          resultSet?.close()
          this.list = list
        })
      Text(JSON.stringify(this.list))修改数据 
 Button('修改第一条')
        .onClick(() => {
          const item = this.list[0]
          item.title = '修改标题' + Math.random()
          const predicates = new relationalStore.RdbPredicates(this.tableName)
          predicates.equalTo('id', item.id)
           this.store?.updateSync(item, predicates)
        })删除数据 
 Button('删除第一条')
        .onClick(() => {
          const item = this.list[0]
          const predicates = new relationalStore.RdbPredicates(this.tableName)
          predicates.equalTo('id', item.id)
          this.store?.deleteSync(predicates)
        })删除数据库 
  deleteStore () {
    relationalStore.deleteRdbStore(getContext(this), {
      name: 'interview.db',
      securityLevel: relationalStore.SecurityLevel.S1
    })
  }      Button('删除数据库')
        .onClick(() => {
          this.deleteStore()
        })audioDB工具-创建数据库 
目标:封装一个操作录音数据库的工具,提供创建数据库的方法
实现步骤:
- 约定好数据库的表结构
- 封装工具类,提供一个创建数据库的方法
落地代码:
1)表结构
CREATE TABLE IF NOT EXISTS interview_audio (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id TEXT NOT NULL,
  name TEXT NOT NULL,
  path TEXT NOT NULL,
  duration INTEGER NOT NULL,
  size INTEGER NOT NULL
)2)封装类
import { relationalStore } from '@kit.ArkData'
class AudioDB {
  store?: relationalStore.RdbStore
  tableName = 'interview_audio'
  // 初始化数据库
  async initStore() {
    const ctx = AppStorage.get<Context>('context')
    if (ctx) {
      const store = await relationalStore.getRdbStore(ctx, {
        name: 'interview_audio.db',
        securityLevel: relationalStore.SecurityLevel.S1
      })
      const sql = `
        CREATE TABLE IF NOT EXISTS ${this.tableName} (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          user_id TEXT NOT NULL,
          name TEXT NOT NULL,
          path TEXT NOT NULL,
          duration INTEGER NOT NULL,
          size INTEGER NOT NULL
        )
      `
      await store.executeSql(sql)
      this.store = store
    }
  }
}audioDB工具-数据操作方法 
目标:提供 添加 删除 查询 修改 数据库的方法
import { relationalStore, ValuesBucket } from '@kit.ArkData'
export interface InterviewAudioItem extends ValuesBucket {
  id: number | null
  user_id: string
  name: string
  path: string
  duration: number
  size: number
  create_time: number
}
class AudioDB {
  store?: relationalStore.RdbStore
  tableName = 'interview_audio'
  // 初始化数据库
  async initStore() {
    const ctx = AppStorage.get<Context>('context')
    if (ctx) {
      const store = await relationalStore.getRdbStore(ctx, {
        name: 'interview_audio.db',
        securityLevel: relationalStore.SecurityLevel.S1
      })
      const sql = `
        CREATE TABLE IF NOT EXISTS ${this.tableName} (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          user_id TEXT NOT NULL,
          name TEXT NOT NULL,
          path TEXT NOT NULL,
          duration INTEGER NOT NULL,
          size INTEGER NOT NULL,
          create_time INTEGER NOT NULL
        )
      `
      await store.executeSql(sql)
      this.store = store
    }
  }
  // 添加
  async insert(item: InterviewAudioItem) {
    const rowId = await this.store?.insert(this.tableName, item)
    if (rowId === undefined || rowId === -1) {
      return Promise.reject('insert fail')
    } else {
      return Promise.resolve()
    }
  }
  // 删除
  async delete(id: number) {
    const predicates = new relationalStore.RdbPredicates(this.tableName)
    predicates.equalTo('id', id)
    const rowCount = await this.store?.delete(predicates)
    if (rowCount === undefined || rowCount <= 0) {
      return Promise.reject('delete fail')
    } else {
      return Promise.resolve()
    }
  }
  // 修改
  async update(item: InterviewAudioItem) {
    const predicates = new relationalStore.RdbPredicates(this.tableName)
    predicates.equalTo('id', item.id)
    const rowCount = await this.store?.update(item, predicates)
    if (rowCount === undefined || rowCount <= 0) {
      return Promise.reject('update fail')
    } else {
      return Promise.resolve()
    }
  }
  // 查询用户
  async query(userId: string) {
    const predicates = new relationalStore.RdbPredicates(this.tableName)
    predicates.equalTo('user_id', userId)
    const resultSet = await this.store?.query(predicates)
    if (!resultSet) {
      return Promise.reject('query fail')
    }
    const list: InterviewAudioItem[] = []
    while (resultSet.goToNextRow()) {
      list.push({
        id: resultSet.getLong(resultSet.getColumnIndex('id')),
        user_id: resultSet.getString(resultSet.getColumnIndex('user_id')),
        name: resultSet.getString(resultSet.getColumnIndex('name')),
        path: resultSet.getString(resultSet.getColumnIndex('path')),
        duration: resultSet.getLong(resultSet.getColumnIndex('duration')),
        size: resultSet.getLong(resultSet.getColumnIndex('size')),
        create_time: resultSet.getLong(resultSet.getColumnIndex('create_time'))
      })
    }
    resultSet.close()
    return Promise.resolve(list)
  }
}
export const audioDB = new AudioDB()面试录音 
页面结构 
目的:准备页面的组件结构,搭建页面基本效果
pages/AudioPage.ets
import { permission } from '../commons/utils'
import { promptAction, router } from '@kit.ArkUI'
import { Permissions } from '@kit.AbilityKit'
import { AudioView } from '../views/Audio/AudioView'
@Entry
@Component
struct AudioPage {
  permissions: Permissions[] = ['ohos.permission.MICROPHONE']
  confirmConfig: promptAction.ShowDialogOptions = {
    title: "温馨提示",
    message: "未授权使用麦克风将无法使用该面试录音功能,是否前往设置进行授权?",
    buttons: [
      { text: '离开', color: $r('app.color.common_gray_01') },
      { text: '去授权', color: $r('app.color.black') }
    ]
  }
  async getPermission() {
    try {
      // 第一请求授权
      const isOk = await permission.requestPermissions(this.permissions)
      if (isOk) {
        return
      }
      // 未授权弹窗提示
      const confirm = await promptAction.showDialog(this.confirmConfig)
      if (confirm.index === 1) {
        // 第二次请求权限
        const isOk2 = await permission.openPermissionSetting(this.permissions)
        if (isOk2) {
          return
        }
      }
      router.back()
    } catch (e) {
      promptAction.showToast({ message: '未授权' })
      router.back()
    }
  }
  build() {
    Column() {
      AudioView()
    }
  }
}views/AudioView.ets 录音视图
import { HcNavBar } from '../../commons/components/HcNavBar'
import { InterviewAudioItem } from '../../commons/utils'
import { AudioItemComp } from './AudioItemComp'
import { AudioRecordComp } from './AudioRecordComp'
@Component
export struct AudioView {
  @State list: InterviewAudioItem[] = [{} as InterviewAudioItem, {} as InterviewAudioItem ] 
  build() {
    Column() {
      HcNavBar({ title: '面试录音', showRightIcon: false })
      Column() {
        List() {
          ForEach(this.list, (item: InterviewAudioItem) => {
            ListItem() {
              AudioItemComp({
                item: {
                  id: 1,
                  name: '2024年10月01日_10点10分10秒',
                  path: '/data/el/xxx',
                  user_id: 100,
                  duration: 10000,
                  size: 10000,
                  create_time: 10000
                }
              })
            }
          })
        }
        .width('100%')
        .height('100%')
      }
      .width('100%')
      .layoutWeight(1)
      AudioRecordComp()
    }
    .width('100%')
    .height('100%')
  }
}views/AudioItemComp.ets 单条录音数据数组
import { InterviewAudioItem } from '../../commons/utils'
@Component
export struct AudioItemComp {
  @Prop
  item: InterviewAudioItem = {} as InterviewAudioItem
  build() {
    Row({ space: 15 }) {
      Image($r('app.media.ic_mine_audio'))
        .width(50)
        .aspectRatio(1)
      Column({ space: 10 }) {
        Text(this.item.name)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        Row({ space: 20 }) {
          Text(`时长:${(this.item.duration / 1000).toFixed(0)} 秒`)
            .fontSize(14)
            .fontColor($r('app.color.common_gray_03'))
          Text(`大小:${(this.item.size / 1000).toFixed(0)} KB`)
            .fontSize(14)
            .fontColor($r('app.color.common_gray_03'))
        }
        .width('100%')
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .alignSelf(ItemAlign.Start)
    }
    .padding(15)
    .height(80)
    .width('100%')
  }
}views/AudioRecordComp.ets 录音组件
import { media } from '@kit.MediaKit'
import { fileIo } from '@kit.CoreFileKit'
@Component
export struct AudioRecordComp {
  @StorageProp('bottomHeight') bottomHeight: number = 0
  avRecorder?: media.AVRecorder
  fd?: number
  filePath?: string
  timer?: number
  @State maxAmplitude: number = 0
  async startRecord() {
    // 1. 准备一个文件接收录音
    const ctx = getContext(this)
    const filePath = ctx.filesDir + '/' + Date.now() + '.m4a'
    this.filePath = filePath
    const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
    this.fd = file.fd
    // 2. 准备路由配置对象
    const config: media.AVRecorderConfig = {
      audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
      profile: {
        audioBitrate: 100000, // 音频比特率
        audioChannels: 1, // 音频声道数
        audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac
        audioSampleRate: 48000, // 音频采样率
        fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
      },
      url: `fd://${file.fd}`
    }
    // 3. 开始录制
    const avRecorder = await media.createAVRecorder()
    await avRecorder.prepare(config)
    await avRecorder.start()
    this.avRecorder = avRecorder
    // 4. 每100ms获取一下声音振幅
    this.timer = setInterval(async () => {
      this.maxAmplitude = await avRecorder.getAudioCapturerMaxAmplitude()
    }, 100)
  }
  async stopRecord() {
    if (this.avRecorder) {
      clearInterval(this.timer)
      await this.avRecorder.stop()
      await this.avRecorder.release()
      fileIo.closeSync(this.fd)
      this.maxAmplitude = 0
    }
  }
  build() {
    Column() {
      AudioBoComp({ maxAmplitude: this.maxAmplitude })
      Row() {
        Image($r('sys.media.ohos_ic_public_voice'))
          .width(24)
          .aspectRatio(1)
          .fillColor($r('app.color.white'))
          .onClick(async () => {
            // TODO 开始和停止录音
          })
      }
      .justifyContent(FlexAlign.Center)
      .height(50)
      .width(50)
      .borderRadius(25)
      .margin({ top: 20 })
      .backgroundColor($r('app.color.black'))
    }
    .width('100%')
    .height(240)
    .backgroundColor($r('app.color.common_gray_bg'))
    .padding({ bottom: this.bottomHeight, left: 80, right: 80, top: 20 })
  }
}
@Component
export struct AudioBoComp {
  @Prop @Watch('onChange') maxAmplitude: number
  @State per: number = 0
  onChange() {
    animateTo({ duration: 100 }, () => {
      if (this.maxAmplitude < 500) {
        this.per = 0
      } else if (this.maxAmplitude > 30000) {
        this.per = 1
      } else {
        this.per = this.maxAmplitude / 30000
      }
    })
  }
  build() {
    Row({ space: 5 }) {
      ForEach(Array.from({ length: 30 }), () => {
        Column()
          .layoutWeight(1)
          .height(this.per * 100 * Math.random())
          .backgroundColor($r('app.color.common_blue'))
      })
    }
    .width('100%')
    .height(100)
    .backgroundColor($r('app.color.common_gray_bg'))
  }
}添加录音 
目标:点击录音按钮开启录音,再次点击结束录音,存储录音信息
落地代码:
1)组件实现录制状态切换 views/AudioRecordComp.ets
 @State recording: boolean = falseImage($r('sys.media.ohos_ic_public_voice'))
          .width(24)
          .aspectRatio(1)
          .fillColor($r('app.color.white'))
          .onClick(async () => {
            if (this.recording) {
              await this.stopRecord()
              this.recording = false
              // TODO 记录录音
            } else {
              await this.startRecord()
              this.recording = true
            }
          })2)组件暴露录制结束事件
  onRecordEnd: (item: InterviewAudioItem) => void = () => {}            const stat = fileIo.statSync(this.filePath)
              this.onRecordEnd({
                id: null,
                name: dayjs().format('YYYY年MM月DD日_HH时mm分ss秒'),
                path  : this.filePath || '',
                duration:  Date.now() - this.startTime,
                size: stat.size,
                user_id: auth.getUser().id,
                create_time: Date.now()
              })3)父组件在录制结束后,插入数据库完成添加
AudioView.ets
  async aboutToAppear() {
    await audioDB.initStore()
  }    AudioRecordComp({
      onRecordEnd: async (item: InterviewAudioItem) => {
        await audioDB.insert(item)
        // TODO 更新列表
      }
    })渲染列表 
目标:完成录音列表展示
1)获取数据库录音数据
  async getList() {
    const user = auth.getUser()
    const rows = await audioDB.query(user.id)
    this.list = rows
  }  async aboutToAppear() {
    await audioDB.initStore()
    await this.getList()
  }2)渲染列表
ForEach(this.list, (item: InterviewAudioItem) => {
  ListItem() {
    AudioItemComp({
      item: item
    })
  }
})删除录音 
目标:通过滑动操作完成录音删除
1)准备滑动删除和编辑效果
@Builder
  ListItemSwiperBuilder(item: InterviewAudioItem) {
    Row() {
      Text('编辑')
        .actionButton($r('app.color.common_blue'))
      Text('删除')
        .actionButton('#FF0033')
    }
    .height('100%')
  }@Extend(Text)
function actionButton(color: ResourceColor) {
  .width(80)
  .aspectRatio(1)
  .backgroundColor(color)
  .textAlign(TextAlign.Center)
  .fontColor($r('app.color.white'))
}ListItem() {
  AudioItemComp({
    item: item
  })
}
.swipeAction({
  end: this.ListItemSwiperBuilder(item)
})2)实现删除
      Text('删除')
        .actionButton('#FF0033')
        .onClick(async () => {
          await audioDB.delete(item.id!)
          this.getList()
        })编辑录音 
目标:实现弹窗对话框,修改录音名称
1)准备对话框
@CustomDialog
struct InputDialog {
  controller: CustomDialogController
  @Prop name: string = ''
  onSubmit: (name: string) => void = () => {
  }
  build() {
    Column({ space: 12 }) {
      Text('修改名字:')
        .height(40)
        .fontWeight(500)
      TextInput({ text: $$this.name })
      Row({ space: 120 }) {
        Text('取消')
          .fontWeight(500)
          .fontColor($r('app.color.common_gray_02'))
          .onClick(() => {
            this.controller.close()
          })
        Text('确认')
          .fontWeight(500)
          .fontColor($r('app.color.common_blue'))
          .onClick(() => {
            this.onSubmit(this.name)
          })
      }
      .height(40)
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .alignItems(HorizontalAlign.Start)
    .padding(16)
    .borderRadius(12)
    .width('80%')
    .backgroundColor($r('app.color.white'))
  }
}2)弹出对话框
  @State currentItem: InterviewAudioItem = {} as InterviewAudioItemdialog = new CustomDialogController({
    builder: InputDialog({
      name: this.currentItem.name,
      onSubmit: async (name) => {
        // TODO 实现修改
      }
    }),
    customStyle: true,
    alignment: DialogAlignment.Center
  })    Row() {
      Text('编辑')
        .actionButton($r('app.color.common_blue'))
        .onClick(() => {
          this.currentItem = item
          this.dialog.open()
        })3)完成修改
  dialog = new CustomDialogController({
    builder: InputDialog({
      name: this.currentItem.name,
      onSubmit: async (name) => {
        const item = this.currentItem
        item.name = name
        await audioDB.update(item)
        await this.getList()
        this.dialog.close()
      }
    }),
    customStyle: true,
    alignment: DialogAlignment.Center
  })录音播放 
目标:通过全屏模态框实现录音信息展示和播放
1)播放组件准备 views/AudioPlayer.ets 支持播放暂停和进度效果
import { InterviewAudioItem, logger } from '../../commons/utils'
import { media } from '@kit.MediaKit'
import { fileIo } from '@kit.CoreFileKit'
@Component
export struct AudioPlayer {
  @State
  playing: boolean = false
  @Prop item: InterviewAudioItem = {} as InterviewAudioItem
  avPlayer?: media.AVPlayer
  @State total: number = 0
  @State value: number = 0
  async startPlay() {
    try {
      const file = fileIo.openSync(this.item.path, fileIo.OpenMode.READ_ONLY)
      const avPlayer = await media.createAVPlayer()
      avPlayer.on('stateChange', state => {
        if (state === 'initialized') {
          avPlayer.prepare()
        } else if (state === 'prepared') {
          avPlayer.loop = true
          this.total = avPlayer.duration
          avPlayer.play()
        }
      })
      // 当前播放时间改变
      avPlayer.on('timeUpdate', (time) => {
        this.value = time
      })
      avPlayer.url = `fd://${file.fd}`
      this.avPlayer = avPlayer
      this.playing = true
    } catch (e) {
      logger.error('startPlay', JSON.stringify(e))
    }
  }
  stopPlay() {
    if (this.avPlayer) {
      this.avPlayer.stop()
      this.avPlayer.release()
      this.playing = false
    }
  }
  aboutToAppear(): void {
    if (this.playing) {
      this.stopPlay()
    }
  }
  build() {
    Column({ space: 20 }) {
      Image($r('app.media.ic_mine_audio'))
        .width(100)
        .aspectRatio(1)
      Text(this.item.name)
        .fontSize(18)
      Row({ space: 20 }) {
        Image(!this.playing ? $r('sys.media.ohos_ic_public_play') : $r('sys.media.ohos_ic_public_pause'))
          .width(24)
          .aspectRatio(1)
          .onClick(() => {
            if (!this.playing) {
              this.startPlay()
            } else {
              this.stopPlay()
            }
          })
        Progress({ value: this.value, total: this.total })
          .layoutWeight(1)
          .margin({ top: 20, bottom: 20 })
      }
      .width('80%')
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.white'))
    .onDisAppear(() => {
      this.stopPlay()
    })
  }
}2)绑定全屏模态框
  @Builder
  PlayerBuilder () {
    Column(){
      AudioPlayer({ item: this.currentItem })
    }
  }    .width('100%')
    .height('100%')
    .bindContentCover($$this.isShow, this.PlayerBuilder())              AudioItemComp({
                item: item
              })
                .onClick(() => {
                  this.currentItem = item
                  this.isShow = true
                })