HarmonyOS APP实战-基于Image Kit的图像处理APP - 第11篇编辑记录本地存储1. 开篇上一篇我们完成了水印与文字叠加功能通过Canvas绘制在PixelMap上支持用户输入文本、选择颜色和位置拖动最终生成带水印的新图片。核心代码封装为WatermarkOverlay组件嵌入图片编辑页面与缩放、旋转、滤镜等功能并列。用户点击保存后水印会永久写入图片。图片处理APP的功能已近完整但每次编辑后的操作缩放、裁剪、滤镜、水印等都未记录用户无法追踪历史操作。本篇将使用本地持久化存储记录每次编辑的源图路径、操作类型和时间戳并在独立的历史页面中展示列表支持单条删除。这样用户既能回顾编辑历程也能清理无用记录为后续批量导出或分享提供依据。2. 核心实现2.1 基础配置引入数据存储模块在pages目录下新增EditHistoryPage.ets同时在utils中创建PreferenceManager.ts封装ohos.data.preferences的常用操作。需要先在module.json5中声明权限Preferences无需额外权限但涉及文件路径读取仍需ohos.permission.READ_MEDIA此权限已在系列第2篇中配置此处复用。// utils/PreferenceManager.tsimportpreferencesfromohos.data.preferences;import{BusinessError}fromohos.base;// 本地存储文件名constSTORE_NAMEedit_history_store;// 存储记录的 keyconstHISTORY_KEYedit_history_list;// 单条历史记录的数据结构exportinterfaceHistoryRecord{uri:string;// 图片路径临时文件或相册URIoperationType:string;// 操作类型scale, crop, rotate, filter, brightness, watermark 等timestamp:number;// 时间戳毫秒thumbnail?:string;// 缩略图base64可选减小存储压力}exportclassPreferenceManager{privatepref:preferences.Preferences|nullnull;// 初始化存储实例asyncinit(context:Context):Promisevoid{try{this.prefawaitpreferences.getPreferences(context,STORE_NAME);console.info(PreferenceManager init success);}catch(err){leteerrasBusinessError;console.error(PreferenceManager init failed, code:${e.code}, message:${e.message});}}// 获取所有历史记录asyncgetHistoryRecords():PromiseHistoryRecord[]{if(!this.pref)return[];try{constjsonStrawaitthis.pref.get(HISTORY_KEY,[])asstring;if(!jsonStr)return[];returnJSON.parse(jsonStr)asHistoryRecord[];}catch(err){console.error(Failed to get history records:,err);return[];}}// 添加一条记录asyncaddRecord(record:HistoryRecord):Promisevoid{if(!this.pref)return;constrecordsawaitthis.getHistoryRecords();records.push(record);// 追加到尾部最新记录在尾部// 只保留最近100条避免数据无限膨胀consttrimmedrecords.length100?records.slice(-100):records;awaitthis.pref.put(HISTORY_KEY,JSON.stringify(trimmed));awaitthis.pref.flush();}// 删除某条记录通过uritimestamp唯一标识asyncdeleteRecord(uri:string,timestamp:number):Promisevoid{if(!this.pref)return;constrecordsawaitthis.getHistoryRecords();constfilteredrecords.filter(item!(item.uriuriitem.timestamptimestamp));awaitthis.pref.put(HISTORY_KEY,JSON.stringify(filtered));awaitthis.pref.flush();}// 清空所有记录asyncclearAllRecords():Promisevoid{if(!this.pref)return;awaitthis.pref.put(HISTORY_KEY,[]);awaitthis.pref.flush();}}// 全局单例exportconsthistoryManagernewPreferenceManager();关键点说明preferences.getPreferences(context, name)创建/打开一个轻量级存储实例返回promise。preferences.Preferences.put(key, value)写入键值对value支持string/number/boolean/ArrayBuffer等此处使用JSON序列化后的字符串。flush()将内存数据同步写入持久化文件必须调用才能保证断电不丢。记录用数组存储写入时做截断保留最近100条避免因频繁操作导致存储过大。HistoryRecord的uri字段记录图片路径便于后续点击跳转回编辑页重新打开时识别图片。2.2 核心逻辑在编辑完成后自动记录历史在图片处理流程中每当用户完成一次编辑如缩放到指定值、应用滤镜、保存水印图片等都需要调用historyManager.addRecord。以第10篇的水印保存为例在保存完成回调中添加记录。// pages/ImageEditPage.ets片段演示集成位置import{historyManager,HistoryRecord}from../utils/PreferenceManager;// 假设第10篇中保存水印图片的函数asyncfunctionsaveWatermarkImage(finalPixelMap:PixelMap){// ... 编码并保存到相册的原有逻辑// 保存成功后记录历史constcurrentTimeDate.now();constrecord:HistoryRecord{uri:savedUri,// 实际保存后的图片URI从fileUri或相册返回operationType:watermark,timestamp:currentTime,thumbnail:undefined// 可扩展此处略};awaithistoryManager.addRecord(record);}同理在其他编辑操作缩放、裁剪、旋转、滤镜、亮度调整的保存回调中也需插入类似代码。为降低耦合可以在一个公共的handleSave()方法中集中处理记录逻辑。// 统一保存方法示例asyncfunctionhandleSave(finalPixelMap:PixelMap,operation:string){// 编码保存...constsavedUriawaitsavePixelMapToFile(finalPixelMap);// 记录历史constrecord:HistoryRecord{uri:savedUri,operationType:operation,timestamp:Date.now()};awaithistoryManager.addRecord(record);// 提示保存成功promptAction.showToast({message:已保存并记录${operation}操作});}关键点说明operationType使用字符串标识例如’scale’, ‘crop’, ‘rotate’, ‘filter’, ‘brightness’, ‘watermark’可在常量文件中定义枚举。注意每次保存的uri是完整的文件路径如file://...历史页面点击后可用来重新加载。记录时间使用Date.now()毫秒数展示时格式化显示。2.3 完整页面EditHistoryPage新建pages/EditHistoryPage.ets展示历史记录列表支持单条删除和清空。// pages/EditHistoryPage.etsimport{historyManager,HistoryRecord}from../utils/PreferenceManager;importrouterfromohos.router;EntryComponentstruct EditHistoryPage{Stateprivaterecords:HistoryRecord[][];Stateprivateloading:booleantrue;aboutToAppear(){this.loadRecords();}asyncloadRecords(){this.loadingtrue;constdataawaithistoryManager.getHistoryRecords();// 按时间戳降序排列最新在前面data.sort((a,b)b.timestamp-a.timestamp);this.recordsdata;this.loadingfalse;}// 删除单条asyncdeleteItem(record:HistoryRecord){AlertDialog.show({title:删除确认,message:确定删除${this.formatOperation(record.operationType)}记录,primaryButton:{value:取消,action:(){}},secondaryButton:{value:删除,action:async(){awaithistoryManager.deleteRecord(record.uri,record.timestamp);this.loadRecords();// 刷新列表}}});}// 清空全部asyncclearAll(){AlertDialog.show({title:清空确认,message:将删除所有历史记录不可恢复,primaryButton:{value:取消,action:(){}},secondaryButton:{value:清空,action:async(){awaithistoryManager.clearAllRecords();this.loadRecords();}}});}// 将操作类型转换为中文显示formatOperation(type:string):string{constmap:Recordstring,string{scale:缩放,crop:裁剪,rotate:旋转,flip:翻转,filter:滤镜,brightness:亮度对比度,watermark:水印};returnmap[type]||type;}// 格式化时间formatTime(ts:number):string{constdatenewDate(ts);constpad(n:number):stringn10?0n:String(n);return${date.getFullYear()}-${pad(date.getMonth()1)}-${pad(date.getDate())}${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())};}build(){Column(){// 顶部标题栏Row(){Text(编辑历史).fontSize(20).fontWeight(FontWeight.Bold).lineHeight(40).margin({left:16})Blank()Button(清空记录).type(ButtonType.Normal).fontColor(#ff0000).fontSize(14).backgroundColor(Color.Transparent).onClick(()this.clearAll())}.width(100%).height(56).backgroundColor(#f5f5f5)// 列表内容if(this.loading){LoadingProgress().width(48).height(48).margin({top:100})}elseif(this.records.length0){Text(暂无编辑记录).fontSize(16).fontColor(#999).margin({top:100})}else{List({space:8}){ForEach(this.records,(item:HistoryRecord,index:number){ListItem(){Row(){// 图标可选Image($r(app.media.app_icon))// 占位图标.width(40).height(40).borderRadius(4).margin({right:12})Column(){Text(this.formatOperation(item.operationType)).fontSize(16).fontWeight(FontWeight.Medium)Text(item.uri.length30?item.uri.substring(0,30)...:item.uri).fontSize(12).fontColor(#888).maxLines(1)Text(this.formatTime(item.timestamp)).fontSize(11).fontColor(#aaa)}.alignItems(HorizontalAlign.Start).layoutWeight(1)Button(删除).type(ButtonType.Normal).fontColor(#ff3333).fontSize(13).backgroundColor(Color.Transparent).onClick(()this.deleteItem(item))}.width(100%).padding(12).backgroundColor(Color.White).borderRadius(8)}})}.width(100%).padding(12).layoutWeight(1)}}.width(100%).height(100%).backgroundColor(#f0f0f0)}}关键点说明使用State装饰器使列表响应式更新每次增删后调用loadRecords()重新加载。排序采用降序最新记录显示在最上面。删除操作前弹出确认对话框避免误删。formatOperation将内部类型标识映射为中文便于用户理解。formatTime将毫秒时间戳格式化为yyyy-MM-dd HH:mm:ss。列表项的URI太长时截断显示完整URI可通过点击记录跳转到编辑页时加载功能可后续扩展。2.4 集成到页面路由在主页面添加一个按钮跳转到历史页// 例如在主页面的菜单中Button(编辑历史).onClick((){router.pushUrl({url:pages/EditHistoryPage});})同时在EntryAbility的onCreate或onForeground中初始化historyManager// EntryAbility.etsimport{historyManager}from../utils/PreferenceManager;onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{// ...historyManager.init(this.context);}完成上述步骤后启动APP进行任意编辑操作并保存再进入编辑历史页面即可看到记录。3. 运行验证打开APP选择一张图片进入编辑页。进行一次操作例如应用一个灰度滤镜然后点击保存提示保存成功。进入编辑历史页面通过主菜单或编辑页返回后点击“历史”按钮。页面显示最新一条记录包含“滤镜”操作、图片路径部分信息和保存时间。对同一张图片或另一张图片进行水印叠加保存后再进入历史页列表新增一条水印记录并按时间降序排列。点击某条记录后的“删除”按钮弹出确认框确认后该条消失。点击“清空记录”所有记录被删除列表变为空提示。预期表现所有历史记录持久化存储关闭APP再打开依然存在。删除操作彻底移除指定项。4. 小结与预告本篇为APP增加了编辑历史本地存储能力核心产出包括PreferenceManager单例类封装了基于ohos.data.preferences的增删查操作。EditHistoryPage完整页面展示按时间排序的历史列表支持单条删除和清空。历史记录自动捕获每次编辑保存动作与第10篇的水印功能无缝集成。这一功能使用户能回溯编辑过程也为后续跨设备同步或分享编辑参数提供了数据基础。下一篇《性能优化与资源释放》将分析图片处理中的内存泄漏与性能瓶颈重点优化大图加载采样率控制、及时释放PixelMap资源、避免重复解码等确保APP在高频编辑场景下依然流畅稳定。