在Harmony应用中实现图片选择+上传。
主要实现逻辑
1.图片选择,利用photoAccessHelper.PhotoViewPicker()的select方法选择手机中的图片
select(option: PhotoSelectOptions, callback: AsyncCallback<PhotoSelectResult>): void
PhotoSelectResult中的photoUris: Array<string>
就是相册中图片的路径。
2.利用SDK API提供的request.uploadFile
对选择的文件进行上传。该方法的参数UploadConfig中字段requestFiles类型为Array<File>
,File的官方说明截图如下
关键两点
关键点一: 上面截图的uri字段,它只能是沙箱路径中的cache目录(这个路径我上篇文章中也提到),官方的说明获取应用文件路径。
所以在1和2步之间,我们还需要将相册中图片复制到沙箱路径中的cache目录。比如我的应用统一管理的是cache/appfiles/(appfiles是我自己创建的目录),给uri的值就是internal://cache/appfiles/文件名称
。
关键点二: 上传完毕后服务端返回的数据信息(图片在服务端的网络地址等)
/**
* UploadTask.on('headerReceive')
*/
function uploadFile(context: BaseContext, config: UploadConfig, callback: AsyncCallback<UploadTask>): void;
uploadTask.on('headerReceive', (header) => {
//上传完毕后,服务端返回的信息数据 对数据解析
//header["body"]这个是服务端返回给我们的全部数据
//{"headers":{},"body":"{"code":1929,"msg":"错误","ts":1727864164712,"data":null}"}
//{"headers":{},"body":"{"code":200,"msg":"success","ts":1727867981758, "data":{"fileUrl":"*******.jpg"}}"}
if (header["body"]) {
let result = JSON.parse(header["body"])
}
}
});
完整代码如下:(代码中包含了我自己封装的Log、沙箱路径等工具类、APPBar、弹窗等ArkUI类,就不贴了,需要的请评论区,主要流程是通的,结尾给出实际效果)
工具类UploadFileNew.ets
import { common } from '@kit.AbilityKit'
import { BusinessError, request } from '@kit.BasicServicesKit';
import Logger from '../../common/Logger';
import fs from '@ohos.file.fs';
import { addUrlWithParams } from '../../net/NetRequest';
import { URL_FILE_UPLOAD } from '../../net/ApiUrl';
import AppConfig from '../../common/AppConfig';
let TAG = "UploadSelectFile_uploadImag"
export class UploadFileNew {
public static uploadImag(context: common.UIAbilityContext, imagePathArray: Array<string>,
callback: UploadCallback) {
//给request.uploadFile UploadConfig files参数
let requestFiles:Array<request.File> = UploadFileNew.createRequestFiles(context,imagePathArray)
//服务端定义的上传http url地址
let uploadConfigUrl = addUrlWithParams(URL_FILE_UPLOAD)
Logger.debug(TAG, "uploadImag uploadConfigUrl===" + uploadConfigUrl);
let uploadConfig = {
url: uploadConfigUrl,
header: {
"Content-Type": "multipart/form-data"
},
method: "POST",
files: requestFiles,
data: [],
} as request.UploadConfig;
try {
//开始上传
request.uploadFile(context, uploadConfig,
(err: BusinessError, uploadTask: request.UploadTask) => {
if (err) {
Logger.debug(TAG, `uploadImage Failed to request the upload. Code: ${err.code}, message: ${err.message}`);
callback.onError()
return
}
if (callback.onStart) {
callback.onStart()
}
uploadTask.on('progress', (uploadedSize: number, totalSize: number) => {
callback.inProgress(uploadedSize, totalSize)
})
let upFailCallback = (taskStates: Array<request.TaskState>) => {
for (let i = 0; i < taskStates.length; i++) {
Logger.debug(TAG, "upOnFail taskState:" + JSON.stringify(taskStates[i],undefined,undefined));
}
};
uploadTask.on('fail', upFailCallback);
let uploadResponseFileUrl = new Array<string>()
let upCompleteCallback = (taskStates: Array<request.TaskState>) => {
Logger.debug(TAG, "upOnComplete taskState:" + JSON.stringify(taskStates,undefined,undefined));
callback.onSuccess(uploadResponseFileUrl)
uploadTask.off('progress');
uploadTask.off('fail');
uploadTask.off('complete');
uploadTask.off('headerReceive');
};
uploadTask.on('complete', upCompleteCallback);
uploadTask.on('headerReceive', (header) => {
//这里很重要重要重要要要要
//上传完毕后,服务端返回的信息数据 对数据解析
//header["body"]这个是服务端返回给我们的全部数据
Logger.debug(TAG, "headerReceive header body ==" + JSON.stringify(header["body"],undefined,undefined));
if (header["body"]) {
let result = JSON.parse(header["body"]) as UploadResponseBody<UploadResponseBodyData>
if (result.code === 200 && result.data) {
uploadResponseFileUrl.push(result.data.fileUrl)
}
}
});
})
} catch (error) {
Logger.debug(TAG, `request.uploadFile catch error Code: ${error.code}, message: ${error.message}`);
callback.onError()
}
}
/**
* 将相册目录中的图片地址复制到cache目录下
* 同时封装request.uploadFile UploadConfig需要的files参数用的数据
* files的必传参数示例
* { filename: "test", name: "file", uri: "internal://cache/test.jpg", type: "jpg" }
* @param context
* @param imagePathArray
* @returns
*/
static createRequestFiles(context: common.UIAbilityContext,imagePathArray: string[]): request.File[] {
// /data/storage/el2/base/haps/entry/cache/appfiles
// getContext().cacheDir+"/appfiles"
let appCacheDir = AppConfig.getInstance(context).APP_CACHE_DIR
let requestFiles:Array<request.File> = new Array<request.File>()
imagePathArray.forEach((imagePath:string, index) => {
let file = fs.openSync(imagePath, fs.OpenMode.READ_ONLY);
let fileAllName = file.name
let fileType = "jpg"
let fileTypeIndex = fileAllName.lastIndexOf(".")
let fileName = fileAllName
if (fileTypeIndex>=0) {
fileName = fileAllName.slice(0,fileTypeIndex)
fileType = file.name.slice(fileTypeIndex+1)
if (!fileType||fileType.length<=0) {
fileType = "jpg"
}
}
let uploadFilePath = appCacheDir+ `/${fileAllName}`
Logger.debug(TAG, "uploadImag destCacheDirImageUri:" + uploadFilePath);
// 复制文件到沙箱目录下
fs.copyFileSync(file.fd, uploadFilePath)
fs.close(file)
let cacheUri = uploadFilePath.slice(uploadFilePath.indexOf("cache"))
//[{ filename: "test", name: "file", uri: "internal://cache/test.jpg", type: "jpg" }],
let cacheDirImageFile = {
filename: fileName,
name:"file",
uri: `internal://${cacheUri}`,
type: fileType
} as request.File
requestFiles.push(cacheDirImageFile)
})
return requestFiles
}
}
/**
* 上传文件的回调数据的封装
*/
interface UploadCallback {
onError: () => void;
onStart?: () => void;
inProgress: (progress: number, total: number) => void;
onSuccess: (files: Array<string>) => void
}
//{"headers":{},"body":"{\"code\":1929,\"msg\":\"请先登录\",\"ts\":1727864164712,\"data\":null}"}
//{"headers":{},"body":"{\"code\":200,\"msg\":\"success\",\"ts\":1727867981758, \"data\":{\"fileUrl\":\"ef8f9bc1-c1f3-4717-a739-17e11f2bd63b.jpg\"}}"}
/**
* 上传文件的返回数据的封装
*/
export interface UploadResponseBody<T> {
code: number
msg: string
ts: number
data?: T
}
export interface UploadResponseBodyData {
fileUrl: string
}
调用侧代码UploadSelectFilePage.ets
import { ConfirmDialogInfo, ConfirmDialogView } from '../../common/dialog/ConfirmDialogView';
import Logger from '../../common/Logger';
import { AppBar } from '../AppBar';
import { HandleWorkOrderImageArrayView } from '../handle/HandleWorkOrderImageArrayView';
import { common } from '@kit.AbilityKit';
import { UploadFileNew } from './UploadFileNew';
import { toast } from '../../common/Toast';
@Entry
@Component
struct UploadSelectFilePage {
TAG = "UploadSelectFile_Page"
context = getContext(this) as common.UIAbilityContext
@State imagePathArray: Array<string> = new Array<string>()
@Watch("onConfirmDialogVisible") @State showConfirmDialog: boolean = false;
@State confirmDialogInfo: ConfirmDialogInfo = {} as ConfirmDialogInfo
build() {
Column() {
AppBar({ title: "选择图片",rightTxt:"上传",rightTxtColor:$r("app.color.color_white"),
onRightTxtClick: () => {
this.showConfirmDialog = true
this.showHintDialogView()
}})
//封装了一个简单的图片选择器UI
HandleWorkOrderImageArrayView({ imagePathArray: $imagePathArray })
.margin({ top: 22, left: 12, right: 12 })
}
.height('100%')
.width('100%')
}
/**
* 上传前的确认弹窗
*/
showHintDialogView() {
this.confirmDialogInfo = {
text: "确定上传图片吗?",
showLeftButton: Visibility.Visible,
confirm: () => {
this.startUploadImages()
},
cancel: () => {
},
}
}
/**
* 开始上传
*/
startUploadImages() {
UploadFileNew.uploadImag(this.context,this.imagePathArray,{
onStart:()=>{
Logger.debug(this.TAG, "onStart");
},
onError: () => {
Logger.debug(this.TAG, "上传失败");
},
inProgress: (progress, total) => {
Logger.debug(this.TAG, `上传进度==========:${progress / total * 100 }%`);
},
onSuccess: (files) => {
Logger.debug(this.TAG, `上传成功,图片路径:\n ${JSON.stringify(files)}`);
toast("上传成功")
}
})
}
onConfirmDialogVisible() {
if (this.confirmDialogController != undefined) {
Logger.debug(this.TAG, "onConfirmDialogVisible this.showConfirmDialog=" + this.showConfirmDialog)
if (this.showConfirmDialog) {
this.confirmDialogController.open()
}
}
}
confirmDialogController: CustomDialogController = new CustomDialogController(
{
builder: ConfirmDialogView({
title: this.confirmDialogInfo.title,
text: this.confirmDialogInfo.text,
visible: this.showConfirmDialog,
confirm: this.confirmDialogInfo.confirm,
confirmType: this.confirmDialogInfo.confirmType
}),
customStyle: true,
autoCancel: false,
alignment: DialogAlignment.Center,
onWillDismiss: (dismissDialogAction) => {
Logger.debug(this.TAG, "confirmDialogController onWillDismiss=" + JSON.stringify(dismissDialogAction.reason))
if (dismissDialogAction.reason == DismissReason.PRESS_BACK) {
dismissDialogAction.dismiss()
}
if (dismissDialogAction.reason == DismissReason.TOUCH_OUTSIDE) {
dismissDialogAction.dismiss()
}
}
}
)
}
封装了一个简单的图片选择器工具类
import Logger from '../../common/Logger'
import { BusinessError } from '@kit.BasicServicesKit'
import { photoAccessHelper } from '@kit.MediaLibraryKit'
import { Router } from '@kit.ArkUI'
let TAG = 'HandleWorkOrderImageArrayView'
@Component
export struct HandleWorkOrderImageArrayView {
@Link imagePathArray: Array<string>
gridScroller = new Scroller()
router: Router = this.getUIContext().getRouter();
build() {
Column() {
Grid(this.gridScroller) {
ForEach(this.imagePathArray, (item: string, index) => {
GridItem() {
this.handleWorkOrderImageItemView(item,index)
}
.width(90)
.height(90)
.borderWidth(1)
.borderColor($r("app.color.color_f5f5f5"))
.borderRadius(2)
})
GridItem() {
Image($r('app.media.ic_add_image'))
.objectFit(ImageFit.Contain)
.width(90)
.height(90).onClick(() => {
this.selectImage()
})
}
.width(90)
.height(90)
.visibility(this.imagePathArray.length == 9 ? Visibility.None : Visibility.Visible)
}
.columnsTemplate('1fr 1fr 1fr')
.friction(0.3)
.columnsGap(6)
.rowsGap(6)
.scrollBar(BarState.Off)
.width('100%')
.layoutDirection(GridDirection.Column)
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
})
}.alignItems(HorizontalAlign.Start)
.width("100%")
}
@Builder
private handleWorkOrderImageItemView(imagePath: string,index:number) {
RelativeContainer() {
Image(imagePath)
.width(90)
.height(90)
.objectFit(ImageFit.Contain)
.onClick(() => {
this.router.pushUrl({url:"pages/detail/LookPicturesPage",params:{array:this.imagePathArray,index:index,showSaveBtn:false}})
})
Image($r('app.media.ic_delete_menu'))
.width(14)
.height(14).alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
right: { anchor: '__container__', align: HorizontalAlign.End }
}).onClick(() => {
this.deleteImage(index)
})
}
.width(90)
.height(90)
}
selectImage() {
try {
let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 9 - this.imagePathArray.length;
let photoPicker = new photoAccessHelper.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions,
(err: BusinessError, photoSelectResult: photoAccessHelper.PhotoSelectResult) => {
if (err) {
Logger.debug(TAG, `PhotoViewPicker.select failed with err: ${err.code}, ${err.message}`);
return;
}
Logger.debug(TAG,
'PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(photoSelectResult));
if (photoSelectResult && photoSelectResult.photoUris && photoSelectResult.photoUris.length > 0) {
let imageArray = this.imagePathArray
photoSelectResult.photoUris.forEach((photoUri) => {
imageArray.push(photoUri)
})
this.imagePathArray = imageArray
}
});
} catch (error) {
let err: BusinessError = error as BusinessError;
Logger.debug(TAG, `PhotoViewPicker failed with err: ${err.code}, ${err.message}`);
}
}
private deleteImage(imageIndex: number) {
Logger.debug(TAG, `deleteImage imageIndex=${imageIndex},length=${this.imagePathArray.length}`);
this.imagePathArray.splice(imageIndex,1)
Logger.debug(TAG, `deleteImage splice length=${this.imagePathArray.length}`);
}
}
效果演示
截图
日志