1

【HarmonyOS NEXT】深入解析HarmonyOS NEXT中的媒体处理功能

在HarmonyOS NEXT中,媒体处理功能是应用开发的核心部分,包括照片上传、拍照上传、文件下载和文件预览。本文将详细介绍这些功能的实现方法和代码细节,帮助你更好地理解和应用HarmonyOS NEXT的API。

@TOC

HarmonyOS NEXT中的媒体处理功能

1. 照片上传与拍照上传

在HarmonyOS NEXT中,照片上传和拍照上传功能通常涉及权限申请、媒体选择或拍摄以及文件上传三个步骤。

1.1 权限申请

在进行任何媒体操作之前,应用需要获取用户的权限。以下是如何检查和申请媒体读写权限的代码:

import { Permissions } from '@ohos.abilityAccessCtrl';
import { applyPermission } from './permissions';

async function checkAndRequestPermissions() {
  const permissions = [Permissions.READ_MEDIA, Permissions.WRITE_MEDIA];
  const status = await applyPermission(context, permissions);
  if (status) {
    console.log('All permissions are granted.');
  } else {
    console.error('Some permissions are not granted.');
  }
}

在这段代码中,我们使用了applyPermission函数来申请媒体相关的权限。具体逻辑如下:

  1. 定义权限列表:需要请求读写媒体文件的权限,定义一个包含Permissions.READ_MEDIAPermissions.WRITE_MEDIA的数组。
  2. 请求权限:调用applyPermission函数请求权限,并等待用户响应。
  3. 检查权限状态:通过返回值status检查用户是否授予了所有请求的权限。如果全部授予,则输出成功信息;否则,输出错误信息。
1.2 从相册选择照片或拍照

使用photoAccessHelper模块,我们可以方便地从相册选择照片或使用相机拍照。以下是如何实现这一功能的代码:

import { photoAccessHelper } from '@kit.MediaLibraryKit';

async function selectPhotoOrTakePhoto() {
  const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
  photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
  photoSelectOptions.maxSelectNumber = 1;

  const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
  try {
    const photoSelectResult = await photoViewPicker.select(photoSelectOptions);
    if (photoSelectResult.photoUris.length > 0) {
      const photoUri = photoSelectResult.photoUris[0];
      // 处理选中的照片
      handleSelectedPhoto(photoUri);
    }
  } catch (error) {
    console.error('Failed to select photo or take photo.', error);
  }
}

function handleSelectedPhoto(photoUri) {
  // 将照片复制到应用的沙箱目录
  copyFileToCache(photoUri, context).then((filePath) => {
    // 上传照片
    uploadPhoto(filePath);
  });
}

具体逻辑如下:

  1. 设置照片选择选项

    • 创建PhotoSelectOptions对象,设置MIMEType为图像类型,确保用户只能选择照片。
    • 设置maxSelectNumber为1,表示用户只能选择一张照片。
  2. 创建照片选择器:使用photoViewPicker对象来选择照片或拍照。
  3. 执行选择操作

    • 使用photoViewPicker.select方法选择照片,并等待用户响应。
    • 如果用户选中了照片,获取选中的照片URI。
  4. 处理选中的照片

    • 调用handleSelectedPhoto函数,将选中的照片复制到应用的沙箱目录。
    • 如果复制成功,调用uploadPhoto函数上传照片。
1.3 照片上传

上传照片的实现涉及到构建表单数据并发送HTTP请求。以下是如何实现照片上传的代码:

import { uploadImg } from '../api/User';

function uploadPhoto(filePath) {
  const formData = new FormData();
  formData.append('file', filePath);

  const config = {
    headers: {
      'Content-Type': 'multipart/form-data',
    },
    context: context,
  };

  uploadImg(formData, config).then((response) => {
    if (response.data && response.data.url) {
      console.log('Photo uploaded successfully.', response.data.url);
    }
  }).catch((error) => {
    console.error('Failed to upload photo.', error);
  });
}

具体逻辑如下:

  1. 构建表单数据

    • 创建FormData对象。
    • 使用formData.append方法将文件路径添加到表单数据中,键名为file
  2. 配置请求

    • 创建config对象,设置请求头Content-Typemultipart/form-data,并传入当前上下文context
  3. 发送HTTP请求

    • 调用uploadImg函数发送HTTP请求上传照片。
    • 如果上传成功,检查response.data中的url属性,输出上传成功的URL。
    • 如果上传失败,输出错误信息。

ps:uploadImg
这里的uploadImg 方法本质上是一个封装好的axios请求,最主要的内容是对headers、formData 设置;axios的封装就不在此赘述了。

2. 文件下载

文件下载功能可以通过request模块实现。以下是如何创建下载任务并监听进度和完成事件的代码:

import { request } from '@kit.BasicServicesKit';

function downloadFile(url, filePath) {
  const downloadConfig = {
    url: url,
    filePath: filePath,
    enableMetered: true,
  };

  request.downloadFile(downloadConfig).then((downloadTask) => {
    downloadTask.on('progress', (receivedSize, totalSize) => {
      console.info(`Download progress: ${receivedSize} of ${totalSize}`);
    });

    downloadTask.on('complete', () => {
      console.info('Download completed');
    });
  }).catch((error) => {
    console.error('Download failed', error);
  });
}

具体逻辑如下:

  1. 定义下载配置

    • 创建downloadConfig对象,包含文件的URL、本地保存路径和是否允许使用计量网络。
  2. 创建下载任务

    • 使用request.downloadFile方法创建下载任务,并等待任务创建的结果。
  3. 监听下载进度

    • 使用downloadTask.on('progress')方法监听下载进度,并在每次进度更新时输出已接收的字节数和总字节数。
  4. 监听下载完成

    • 使用downloadTask.on('complete')方法监听下载完成事件,并在下载完成后输出成功信息。
  5. 处理下载错误

    • 如果下载任务创建失败或下载过程中出现错误,输出错误信息。
3. 文件预览

文件预览功能可以通过PreviewKit模块实现。以下是如何预览文件的代码:

import { filePreview } from '@kit.PreviewKit';

function previewFile(filePath) {
  const fileUriObject = new fileUri.FileUri(filePath);
  const fileInfo = {
    title: 'File Name',
    uri: fileUriObject.getFullDirectoryUri() + '/File Name',
    mimeType: 'application/pdf', // 根据文件类型设置MIME类型
  };

  filePreview.openPreview(context, fileInfo).then(() => {
    console.info('File preview succeeded');
  }).catch((error) => {
    console.error('File preview failed', error);
  });
}

具体逻辑如下:

  1. 创建文件URI对象

    • 使用fileUri.FileUri构造函数创建fileUriObject对象,传入文件路径。
  2. 定义文件信息

    • 创建fileInfo对象,包含文件标题、完整URI和MIME类型。
    • 根据文件类型设置MIME类型,例如PDF文件的MIME类型为application/pdf
  3. 预览文件

    • 使用filePreview.openPreview方法打开文件预览,传入当前上下文context和文件信息。
    • 如果预览成功,输出成功信息。
    • 如果预览失败,输出错误信息。

辅助工具方法

为了更好地管理和处理权限、文件操作等,我们可以编写一些辅助工具方法。

复制文件到缓存目录
import fs from '@ohos.file.fs';
import { JSON } from '@kit.ArkTS';

/**
 * 复制文件到缓存目录下
 * @param path :文件路径
 * @param context :Context
 * @returns Promise<string> 移动后文件路径
 */
export async function copyFileToCache(path: string, context: Context): Promise<string> {
  try {
    const file = fs.openSync(path, fs.OpenMode.READ_ONLY);
    console.log('cwx-copyFileToCache-', JSON.stringify(file));
    if (file) {
      const fileDir = `${context.cacheDir}`; // 临时文件目录
      const filename = path.split('/').pop(); // 获取文件名
      const newPath = `${new Date().getTime()}_${filename}`;
      const targetPath = `${fileDir}/${newPath}`;
      fs.copyFileSync(file.fd, targetPath);
      fs.closeSync(file.fd);
      return newPath;
    } else {
      return '';
    }
  } catch (e) {
    console.error('cwx-copyFileToCache-err-', JSON.stringify(e));
    return '';
  }
}

具体逻辑如下:

  1. 打开文件:使用fs.openSync方法以只读模式打开文件。
  2. 检查文件:如果文件成功打开,继续执行后续操作。
  3. 设置目标路径

    • 获取临时文件目录context.cacheDir
    • 生成新的文件名,使用时间戳和原始文件名。
    • 组合目标路径。
  4. 复制文件:使用fs.copyFileSync方法将文件从原始路径复制到目标路径。
  5. 关闭文件:使用fs.closeSync方法关闭文件。
  6. 返回新路径:返回复制后的新文件路径。
  7. 处理异常:如果在操作过程中出现异常,输出错误信息并返回空字符串。
校验应用是否授予权限
import bundleManager from '@ohos.bundle.bundleManager';
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';

/**
 * 校验应用是否授予权限
 * @param permission :权限名称数组
 * @returns Promise<abilityAccessCtrl.GrantStatus> :权限状态
 */
async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  const atManager = abilityAccessCtrl.createAtManager();
  let grantStatus: abilityAccessCtrl.GrantStatus = 0;

  // 获取应用程序的accessTokenID
  try {
    const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    const appInfo = bundleInfo.appInfo;
    const tokenId = appInfo.accessTokenId;

    // 校验应用是否被授予权限
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
  } catch (err) {
    console.error(`getBundleInfoForSelf failed, code is ${err.code}, message is ${err.message}`);
  }

  return grantStatus;
}

具体逻辑如下:

  1. 创建权限管理器:使用abilityAccessCtrl.createAtManager方法创建权限管理器atManager
  2. 获取应用程序的accessTokenID

    • 调用bundleManager.getBundleInfoForSelf方法获取应用程序的信息。
    • 从返回的bundleInfo中提取appInfo,并获取accessTokenId
  3. 校验应用是否被授予权限:使用atManager.checkAccessToken方法校验应用是否被授予权限。
  4. 处理异常:如果在获取应用程序信息或校验权限过程中出现异常,输出错误信息。
  5. 返回权限状态:返回权限状态grantStatus
检查用户权限
import { abilityAccessCtrl } from '@ohos.abilityAccessCtrl';

/**
 * 检查用户权限
 * @param permissions :权限名称数组
 * @returns Promise<boolean> :是否授权成功
 */
export async function checkPermissions(permissions: Permissions[]): Promise<boolean> {
  try {
    const grantStatus = await checkAccessToken(permissions);
    return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
  } catch (e) {
    return Promise.reject(e);
  }
}

具体逻辑如下:

  1. 调用checkAccessToken函数:请求校验应用是否被授予权限。
  2. 检查权限状态:如果权限状态为PERMISSION_GRANTED,返回true;否则,返回false
  3. 处理异常:如果在请求权限状态过程中出现异常,返回Promise.reject
申请权限
import common from '@ohos.app.ability.common';
import { abilityAccessCtrl } from '@ohos.abilityAccessCtrl';

interface rejectObj {
  code: number;
  message: string;
}

/**
 * 申请权限
 * @param context :UIAbilityContext
 * @param permissions :权限名称数组
 * @returns Promise<boolean> :是否授权成功
 */
export async function applyPermission(context: common.UIAbilityContext, permissions: Permissions[]): Promise<boolean> {
  const atManager = abilityAccessCtrl.createAtManager();
  return new Promise((resolve, reject) => {
    atManager.requestPermissionsFromUser(context, permissions).then((data) => {
      const grantStatus = data.authResults;
      resolve(grantStatus.every(item => item === 0));
    }).catch((err) => {
      reject(err);
    });
  });
}

具体逻辑如下:

  1. 创建权限管理器:使用abilityAccessCtrl.createAtManager方法创建权限管理器atManager
  2. 请求用户权限:使用atManager.requestPermissionsFromUser方法请求用户权限,并传入当前上下文context和权限名称数组permissions
  3. 检查权限状态:如果用户授予了所有请求的权限,authResults数组中的每个元素都应为0,返回true;否则,返回false
  4. 处理异常:如果在请求用户权限过程中出现异常,返回Promise.reject

实战1——下载文件保存并或预览

我们实现一个方法,调用他下载一个网络资源、根据传参决定是下载并保存文件,还是下载后预览文件。

dowLoadFile(data:DownloadParam,handler: CompleteHandler,preview: boolean){
    data = JSON.parse(data+'') as DownloadParam
    let context = getContext(this) as common.UIAbilityContext;
    let fileName = data.name
    let filesDir = context.filesDir;
    let filePath = filesDir + '/'+fileName
    try {
      fs.accessSync(filePath);
      fs.unlinkSync(filePath);
    } catch (err) {
    }
    try {
      this.loaded=false//加载动画开关,根据自己代码实际情况设置即可
      request.downloadFile(context.getApplicationContext(), {
        url: data.url,
        filePath: filePath,
        enableMetered:true
      }).then((downloadTask: request.DownloadTask) => {
        downloadTask.on('progress', (receivedSize: number, totalSize: number)=>{
          console.info("downloadFile-receivedSize:" + receivedSize + " totalSize:" + totalSize);
        });
        downloadTask.on('complete', async () => {
          this.loaded=true
          console.info('downloadFile-complete');
          if(preview){
            this.doPreview(data.name,filePath)
          }else{
            let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE);
            let arrayBuffer = new ArrayBuffer(99999999);
            let readLen = fs.readSync(file.fd, arrayBuffer);
            let buf = buffer.from(arrayBuffer, 0, readLen);
            console.info(`downloadFile-file:${readLen}_${buf.toString()}`);
            fs.closeSync(file);
            const documentSaveOptions = new picker.DocumentSaveOptions();
            documentSaveOptions.newFileNames = [data.name];

            let uris: Array<string> = [];
            let context = getContext(this) as common.Context;
            const documentViewPicker = new picker.DocumentViewPicker(context);
            documentViewPicker.save(documentSaveOptions).then((documentSaveResult: Array<string>) => {
              uris = documentSaveResult;
              let uri = uris[0]
              try{
                let file = fs.openSync(uri, fs.OpenMode.READ_WRITE);
                let writeLen: number = fs.writeSync(file.fd, arrayBuffer);
                console.info('write data to file succeed and size is:' + writeLen);
                fs.closeSync(file);
                promptAction.showToast({
                  message: '下载成功,保存到:'+uri,
                  duration: 1000,
                });
              }catch (e){
                console.log('copyFile-err-',JSON.stringify((e)))
              }
              console.info('documentViewPicker.save to file succeed and uris are:' + uris);
            }).catch((err: BusinessError) => {
              console.error(`Invoke documentViewPicker.save failed, code is ${err.code}, message is ${err.message}`);
            })
          }
        })
      }).catch((err: BusinessError) => {
        this.loaded=true
        console.error('downloadFile-err-',JSON.stringify(err));
      });
    } catch (error) {
      let err: BusinessError = error as BusinessError;
      console.error(`Invoke downloadFile failed, code is ${err.code}, message is ${err.message}`);
    }
  }

这段代码定义了一个名为 dowLoadFile 的函数,用于下载文件并根据参数选择是否预览文件或保存文件。以下是对这段代码的详细解析:

函数签名
dowLoadFile(data: DownloadParam, handler: CompleteHandler, preview: boolean)
  • data: DownloadParam - 包含下载文件所需参数的对象,如文件名和下载URL。
  • handler: CompleteHandler - 完成下载后的回调函数。
  • preview: boolean - 是否预览文件的标志。
参数解析
  • data 是一个 DownloadParam 类型的对象,通常包含以下字段:

    • name - 文件名。
      {url} - 文件的下载URL。
  • handler 是一个 CompleteHandler 类型的函数,通常用于处理下载完成后的逻辑。
  • preview 是一个布尔值,决定是否预览文件。
函数体
  1. 数据解析

    data = JSON.parse(data+'') as DownloadParam
    • data 转换为字符串后解析为一个 DownloadParam 对象。
  2. 获取上下文

    let context = getContext(this) as common.UIAbilityContext;
    • 获取当前上下文并将其类型转换为 common.UIAbilityContext,用于访问应用的文件目录和其他上下文信息。
  3. 文件路径构建

    let fileName = data.name
    let filesDir = context.filesDir;
    let filePath = filesDir + '/'+fileName
    • data 中提取文件名。
    • 获取应用的文件目录路径。
    • 构建完整的文件路径。
  4. 检查并删除已存在的文件

    try {
      fs.accessSync(filePath);
      fs.unlinkSync(filePath);
    } catch (err) {
    }
    • 使用 fs.accessSync 检查文件是否已存在。
    • 如果文件存在,使用 fs.unlinkSync 删除文件。
  5. 设置加载状态

    this.loaded=false
    • 将加载状态设置为 false,表示下载尚未完成。
  6. 下载文件

    request.downloadFile(context.getApplicationContext(), {
      url: data.url,
      filePath: filePath,
      enableMetered: true
    }).then((downloadTask: request.DownloadTask) => {
    • 使用 request.downloadFile 方法开始下载文件。
    • url 是文件的下载URL。
    • filePath 是文件的保存路径。
    • enableMetered 设置为 true,表示允许在计量网络下下载文件。
  7. 处理下载进度

    downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
      console.info("downloadFile-receivedSize:" + receivedSize + " totalSize:" + totalSize);
    });
    • 监听下载任务的 progress 事件,获取已下载的文件大小和总文件大小,并打印日志。
  8. 处理下载完成

    downloadTask.on('complete', async () => {
      this.loaded=true
      console.info('downloadFile-complete');
    • 监听下载任务的 complete 事件,表示下载已完成。
    • 将加载状态设置为 true,表示下载已完成。
    • 打印下载完成的日志。
  9. 预览文件

    if(preview){
      this.doPreview(data.name, filePath)
    }
    • 如果 previewtrue,调用 this.doPreview 方法预览文件。
  10. 读取文件内容

    else{
      let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE);
      let arrayBuffer = new ArrayBuffer(99999999);
      let readLen = fs.readSync(file.fd, arrayBuffer);
      let buf = buffer.from(arrayBuffer, 0, readLen);
      console.info(`downloadFile-file:${readLen}_${buf.toString()}`);
      fs.closeSync(file);
    }
    • 如果 previewfalse,打开下载的文件。
    • 创建一个 ArrayBuffer 对象,用于存储文件内容。
    • 使用 fs.readSync 读取文件内容到 ArrayBuffer
    • 打印读取的文件内容长度和内容。
    • 关闭文件。
  11. 获取任务信息并保存文件

    const taskInfo = await downloadTask.getTaskInfo();
    console.info('downloadFile-task-complete:'+`status: ${taskInfo.status}`);
    const documentSaveOptions = new picker.DocumentSaveOptions();
    documentSaveOptions.newFileNames = [data.name];
    
    let uris: Array<string> = [];
    let context = getContext(this) as common.Context;
    const documentViewPicker = new picker.DocumentViewPicker(context);
    documentViewPicker.save(documentSaveOptions).then((documentSaveResult: Array<string>) => {
      uris = documentSaveResult;
      let uri = uris[0]
      try{
        let file = fs.openSync(uri, fs.OpenMode.READ_WRITE);
        let writeLen: number = fs.writeSync(file.fd, arrayBuffer);
        console.info('write data to file succeed and size is:' + writeLen);
        fs.closeSync(file);
        promptAction.showToast({
          message: '下载成功,保存到:'+uri,
          duration: 1000,
        });
      }catch (e){
        console.log('copyFile-err-',JSON.stringify((e)))
      }
      console.info('documentViewPicker.save to file succeed and uris are:' + uris);
    }).catch((err: BusinessError) => {
      console.error(`Invoke documentViewPicker.save failed, code is ${err.code}, message is ${err.message}`);
    })
    • 获取下载任务的详细信息,并打印状态。
    • 创建 DocumentSaveOptions 对象,设置新的文件名。
    • 使用 DocumentViewPicker 保存文件。
    • 如果保存成功,获取保存的文件URI。
    • 打开保存的文件,将 ArrayBuffer 中的内容写入文件。
    • 打印写入文件成功的日志。
    • 使用 promptAction.showToast 显示下载成功的提示信息。
    • 如果保存失败,捕获错误并打印日志。
  12. 处理下载错误

    }).catch((err: BusinessError) => {
      this.loaded=true
      console.error('downloadFile-err-',JSON.stringify(err));
    });
    • 如果下载文件失败,捕获错误,将加载状态设置为 true,并打印错误日志。
  13. 处理其他错误

    } catch (error) {
      let err: BusinessError = error as BusinessError;
      console.error(`Invoke downloadFile failed, code is ${err.code}, message is ${err.message}`);
    }
    • 捕获其他可能的错误,将错误信息打印到日志。

    ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/888b3b4256864d3d80cd04123f70...

下载成功效果图

在这里插入图片描述

实战2——选择图片或拍照上传

@Builder myBuilder() {
    Column() {
      List({ space: 1 }) {
        ListItem() {
          Text('拍照').fontSize(18).fontColor('#409EFF').margin({ left: 12 })
        }
        .width('100%')
        .height(56)
        .backgroundColor('#FFFFFF')
        .borderRadius(5)
        .align(Alignment.Center)
        .onClick(()=>{this.takePhoto()})
        ListItem() {
          Text('相册').fontSize(18).fontColor('#409EFF').margin({ left: 12 })
        }
        .width('100%')
        .height(56)
        .backgroundColor('#FFFFFF')
        .borderRadius(5)
        .align(Alignment.Center)
        .onClick(()=>{this.getPhoto()})
      }
      List({ space: 1 }) {
        ListItem() {
          Text('取消').fontSize(18).fontColor('#409EFF').margin({ left: 12 }).fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .height(56)
        .backgroundColor('#FFFFFF')
        .borderRadius(5)
        .align(Alignment.Center)
        .onClick(()=>{this.modalShow=false})
      }
      .margin({top:5})
    }
    .width('100%')
    .height('100%')
  }
......
async getPhoto(){
    const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
    photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 过滤选择媒体文件类型为IMAGE
    photoSelectOptions.maxSelectNumber = 1; // 选择媒体文件的最大数目
    const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
    photoViewPicker.select(photoSelectOptions).then(async(photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
      this.modalShow=false;
      this.loaded =false;
      if (photoSelectResult.photoUris.length) {
        try {
          let path = photoSelectResult.photoUris[0]
          // //复制图片到缓存目录(缓存目录才有读写权限)
          let filePath = await copyFileToCache(photoSelectResult.photoUris[0],this.context)
          if(filePath){
            this.uploadImg(filePath)
          }else{
            this.loaded =true;
          }
        } catch (err) {
          this.loaded =true;
        }
      }else{
        this.loaded =true;
      }
    }).catch((err: BusinessError) => {
      this.modalShow=false;
    })
  }
  async takePhoto(){
    try {
      let pickerProfile: cameraPicker.PickerProfile = {
        cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
      };
      let pickerResult: cameraPicker.PickerResult = await cameraPicker.pick(this.context,
        [cameraPicker.PickerMediaType.PHOTO, cameraPicker.PickerMediaType.PHOTO], pickerProfile);
      console.log('takePhoto',JSON.stringify(pickerResult))
      if (pickerResult?.resultUri) {
        //关闭弹窗
        this.modalShow=false;
        this.loaded =false;
          try {
            let path = pickerResult.resultUri
            // //复制图片到缓存目录(缓存目录才有读写权限)
            let filePath = await copyFileToCache(pickerResult.resultUri,this.context)
            if(filePath){
              this.uploadImg(filePath)
            }else{
              this.loaded =true;
            }
          } catch (err) {
            this.loaded =true;
          }
      }else{
        this.loaded =true;
      }
    } catch (error) {
      let err = error as BusinessError;
      console.error(`the pick call failed. error code: ${err.code}`);
    }
  }
  uploadImg(filePath:string){
    const formData = new FormData()
    formData.append('file', `internal://cache/${filePath}`)
    uploadImg<PermissionResponse>(formData,{
      headers: { 'Content-Type': 'multipart/form-data' },
      context: getContext(this),
    }).then(res=>{
      console.log('cwx-uploadImg-',JSON.stringify(res))
      if(res.data&&res.data.url){
        promptAction.showToast({
          message: '上传成功!',
          duration: 1000,
        });
        this.handlerUploadCallBack?.complete(JSON.stringify({url:res.data.url}))
      }
    }).finally(()=>{
      this.modalShow=false;
      this.loaded = true;
    })
  }

这段代码定义了一个用于选择照片(拍照或从相册选择)并上传的界面和相关的逻辑处理。以下是详细的代码分析:

函数签名
  • @Builder myBuilder() - 使用 @Builder 装饰器定义一个构建器方法 myBuilder,用于构建UI界面。
  • async getPhoto() - 从相册选择照片并上传。
  • async takePhoto() - 拍照并上传。
  • uploadImg(filePath: string) - 上传照片到服务器。
myBuilder 方法
@Builder myBuilder() {
  Column() {
    List({ space: 1 }) {
      ListItem() {
        Text('拍照').fontSize(18).fontColor('#409EFF').margin({ left: 12 })
      }
      .width('100%')
      .height(56)
      .backgroundColor('#FFFFFF')
      .borderRadius(5)
      .align(Alignment.Center)
      .onClick(() => { this.takePhoto() })
      ListItem() {
        Text('相册').fontSize(18).fontColor('#409EFF').margin({ left: 12 })
      }
      .width('100%')
      .height(56)
      .backgroundColor('#FFFFFF')
      .borderRadius(5)
      .align(Alignment.Center)
      .onClick(() => { this.getPhoto() })
    }
    List({ space: 1 }) {
      ListItem() {
        Text('取消').fontSize(18).fontColor('#409EFF').margin({ left: 12 }).fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .height(56)
      .backgroundColor('#FFFFFF')
      .borderRadius(5)
      .align(Alignment.Center)
      .onClick(() => { this.modalShow = false })
    }
    .margin({ top: 5 })
  }
  .width('100%')
  .height('100%')
}
  1. Column 布局

    • 使用 Column 布局容器,将内容垂直排列。
    • 设置 Column 的宽度和高度为100%,使其占满整个屏幕。
  2. 第一个 List 布局

    • 使用 List 布局容器,设置 space 为1,表示列表项之间的间距。
    • 包含两个 ListItem 元素,分别用于拍照和选择相册中的照片。
  3. ListItem 1 - 拍照

    • 使用 Text 组件显示“拍照”文字,设置字体大小为18,字体颜色为 #409EFF,左边距为12。
    • 设置 ListItem 的宽度为100%,高度为56,背景色为 #FFFFFF,圆角为5,居中对齐。
    • 绑定 onClick 事件,调用 this.takePhoto 方法。
  4. ListItem 2 - 相册

    • 使用 Text 组件显示“相册”文字,设置字体大小为18,字体颜色为 #409EFF,左边距为12。
    • 设置 ListItem 的宽度为100%,高度为56,背景色为 #FFFFFF,圆角为5,居中对齐。
    • 绑定 onClick 事件,调用 this.getPhoto 方法。
  5. 第二个 List 布局

    • 使用 List 布局容器,设置 space 为1,表示列表项之间的间距。
    • 包含一个 ListItem 元素,用于取消操作。
  6. ListItem 3 - 取消

    • 使用 Text 组件显示“取消”文字,设置字体大小为18,字体颜色为 #409EFF,左边距为12,字体加粗。
    • 设置 ListItem 的宽度为100%,高度为56,背景色为 #FFFFFF,圆角为5,居中对齐。
    • 绑定 onClick 事件,将 this.modalShow 设置为 false,关闭弹窗。
  7. 设置第二个 List 的外边距

    • 设置 List 的上边距为5。
getPhoto 方法
async getPhoto() {
  const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
  photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 过滤选择媒体文件类型为IMAGE
  photoSelectOptions.maxSelectNumber = 1; // 选择媒体文件的最大数目
  const photoViewPicker = new photoAccessHelper.PhotoViewPicker();
  photoViewPicker.select(photoSelectOptions).then(async (photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
    this.modalShow = false;
    this.loaded = false;
    if (photoSelectResult.photoUris.length) {
      try {
        let path = photoSelectResult.photoUris[0]
        // //复制图片到缓存目录(缓存目录才有读写权限)
        let filePath = await copyFileToCache(photoSelectResult.photoUris[0], this.context)
        if (filePath) {
          this.uploadImg(filePath)
        } else {
          this.loaded = true;
        }
      } catch (err) {
        this.loaded = true;
      }
    } else {
      this.loaded = true;
    }
  }).catch((err: BusinessError) => {
    this.modalShow = false;
  })
}
  1. 创建照片选择选项

    • 使用 photoAccessHelper.PhotoSelectOptions 创建照片选择选项对象。
    • 设置 MIMETypeIMAGE_TYPE,表示只选择图片类型。
    • 设置 maxSelectNumber 为1,表示最多选择一张图片。
  2. 创建照片选择器

    • 使用 photoAccessHelper.PhotoViewPicker 创建照片选择器对象。
  3. 选择照片

    • 调用 photoViewPicker.select 方法选择照片,传入选择选项。
    • 使用 then 处理选择结果。
    • 关闭弹窗并将加载状态设置为 false
    • 检查选择结果中是否有照片URI。
    • 使用 copyFileToCache 方法将选择的照片复制到缓存目录。
    • 如果复制成功,调用 this.uploadImg 方法上传照片。
    • 如果复制失败或没有选择照片,将加载状态设置为 true
  4. 处理选择错误

    • 使用 catch 捕获选择照片时的错误,并将弹窗显示状态设置为 false
takePhoto 方法
async takePhoto() {
  try {
    let pickerProfile: cameraPicker.PickerProfile = {
      cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
    };
    let pickerResult: cameraPicker.PickerResult = await cameraPicker.pick(this.context,
      [cameraPicker.PickerMediaType.PHOTO, cameraPicker.PickerMediaType.PHOTO], pickerProfile);
    console.log('takePhoto', JSON.stringify(pickerResult))
    if (pickerResult?.resultUri) {
      //关闭弹窗
      this.modalShow = false;
      this.loaded = false;
      try {
        let path = pickerResult.resultUri
        // //复制图片到缓存目录(缓存目录才有读写权限)
        let filePath = await copyFileToCache(pickerResult.resultUri, this.context)
        if (filePath) {
          this.uploadImg(filePath)
        } else {
          this.loaded = true;
        }
      } catch (err) {
        this.loaded = true;
      }
    } else {
      this.loaded = true;
    }
  } catch (error) {
    let err = error as BusinessError;
    console.error(`the pick call failed. error code: ${err.code}`);
  }
}
  1. 创建相机选择配置

    • 使用 cameraPicker.PickerProfile 创建相机选择配置对象,设置相机位置为后置摄像头。
  2. 调用相机选择器

    • 使用 cameraPicker.pick 方法调用相机,传入选择配置。
    • 使用 then 处理拍照结果。
    • 打印拍照结果的日志。
  3. 处理拍照结果

    • 检查拍照结果中是否有照片URI。
    • 如果有照片URI,关闭弹窗并将加载状态设置为 false
    • 使用 copyFileToCache 方法将拍照的照片复制到缓存目录。
    • 如果复制成功,调用 this.uploadImg 方法上传照片。
    • 如果复制失败或没有拍照,将加载状态设置为 true
  4. 处理拍照错误

    • 使用 catch 捕获拍照时的错误,并将错误信息打印到日志中。
uploadImg 方法
uploadImg(filePath: string) {
  const formData = new FormData()
  formData.append('file', `internal://cache/${filePath}`)
  uploadImg<PermissionResponse>(formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
    context: getContext(this),
  }).then(res => {
    console.log('cwx-uploadImg-', JSON.stringify(res))
    if (res.data && res.data.url) {
      promptAction.showToast({
        message: '上传成功!',
        duration: 1000,
      });
      this.handlerUploadCallBack?.complete(JSON.stringify({ url: res.data.url }))
    }
  }).finally(() => {
    this.modalShow = false;
    this.loaded = true;
  })
}
  1. 创建表单数据

    • 使用 FormData 创建表单数据对象。
    • 将文件路径添加到表单数据中,路径前缀为 internal://cache/,表示文件在缓存目录中。
  2. 上传文件

    • 使用 uploadImg 方法上传文件,传入表单数据和配置对象。
    • 配置对象中设置请求头为 multipart/form-data,并传入上下文。
  3. 处理上传结果

    • 使用 then 处理上传结果。
    • 打印上传结果的日志。
    • 检查上传结果中是否有URL。
    • 如果有URL,显示上传成功的提示信息,并调用上传完成的回调函数。
    • 使用 finally 在上传完成时关闭弹窗并将加载状态设置为 true

      选择图片效果图

      请添加图片描述


帅比九日
7 声望2 粉丝