第99篇 | HarmonyOS 最小闭环复盘:从拍照到相册的一次可复现路径
第99篇 | HarmonyOS 最小闭环复盘:从拍照到相册的一次可复现路径
前面 98 篇已经把相机、地图、AI、视频、分享、隐私和发布材料拆开讲过。第 99 篇收回来,只看一个最小闭环:用户点击拍照,应用拿到文件路径,生成 GalleryMoment,写入本地记录,再让相册和地图都能消费这条记录。
这篇不是做概念总结,而是把主链路重新压成一条可复现路径。只要读者能沿着本文从状态字段找到函数,从函数找到记录模型,再回到真机页面确认结果,就说明系列文章真的形成了工程闭环。
版本与环境
本文复测口径为 DevEco Studio 6.1 Release、HarmonyOS SDK 6.1.0(23)、Stage 模型 ArkTS 页面。涉及相机、地图、AI 在线能力、华为账号、系统分享或多端同步时,以真机结果为准;预览器只能用来检查页面结构和文案层级,不能替代权限、设备能力和系统弹窗验证。
对应源码位置
entry/src/main/ets/pages/Index.etsentry/src/main/ets/services/GalleryRecordService.etsentry/src/main/ets/services/DualPhotoComposerService.ets
本篇目标
- 确认拍照结果不是只停留在图片文件,而是进入 GalleryMoment 记录。
- 理解 appendGalleryRecord 为什么要同时更新选中态、相册、地图和持久化。
- 把成功态、取消态、保存失败态都放进验收路径。
- 形成 6.14 继续发布时可复用的文章结构。
从页面状态看最小闭环
最小闭环的起点在 Index 页面。页面同时维护相机权限、双摄能力、拍摄模式、预览状态、相册记录和地图状态,这些字段共同决定用户看到的是“准备拍摄”“正在保存”还是“已进入相册”。
写文章时不能只展示一张相机页截图,还要说明这些状态如何连到结果记录。否则读者只能看到界面,无法判断拍照完成后数据去了哪里。

相机页是最小闭环入口,拍照结果要一路进入相册和地图
@State private holdingHandSide: HoldingHandSide = 'right';
@State private holdingHandAwarenessStatusText: string = '握姿感应待命';
@State private mapReady: boolean = false;
@State private mapErrorText: string = '';
@State private showDetailPanel: boolean = false;
@State private cameraPermissionReady: boolean = false;
@State private cameraCapabilityChecked: boolean = false;
@State private dualCameraSupported: boolean = false;
@State private cameraStatusText: string = '拍照准备中';
@State private cameraDeviceCount: number = 0;
@State private cameraConcurrentProfileCount: number = 0;
@State private cameraProbeResultText: string = '拍照能力检测完成';
@State private singleCameraSupported: boolean = false;
@State private selectedCaptureMode: CaptureMode = 'dual';
@State private singleCameraRole: CameraLensRole = 'back';
@State private singlePreviewLive: boolean = false;
@State private backPreviewLive: boolean = false;
@State private frontPreviewLive: boolean = false;
@State private cameraFlashAvailable: boolean = false;
@State private cameraFlashMode: camera.FlashMode = camera.FlashMode.FLASH_MODE_CLOSE;
@State private cameraZoomMin: number = 1;
@State private cameraZoomMax: number = 1;
@State private cameraZoomCurrent: number = 1;
@State private cameraZoomReady: boolean = false;
@State private backLensChoiceKey: string = '';
@State private captureBusy: boolean = false;
@State private captureOutputReady: boolean = true;
@State private capturePairCount: number = 0;
@State private lastCaptureSummary: string = '拍完自动进入相册';
@State private cameraCapturePreviewVisible: boolean = false;
@State private cameraCapturePreviewBackUri: string = '';
@State private cameraCapturePreviewFrontUri: string = '';
@State private cameraCapturePreviewTitle: string = '';
@State private cameraCapturePreviewActionsVisible: boolean = false;
@State private cameraSequentialThumbnailUri: string = '';
@State private cameraSequentialThumbnailLabel: string = '';
@State private selectedCameraEffectCategory: CameraEffectCategoryKey = 'beauty';
@State private cameraEffectOptionRevision: number = 0;
@State private selectedBeautyPreset: BeautyPresetKey = 'natural';
@State private selectedFillLightColor: FillLightColorKey = 'off';
@State private selectedWatermarkStyle: WatermarkStyleKey = 'place';
@State private galleryRecords: Array<GalleryMoment> = [];
@State private galleryLoading: boolean = false;
@State private gallerySelectedId: string = '';
@State private selectedGalleryGroupKey: string = '';
@State private galleryUserNoteDraft: string = '';
markCaptureDelivered 负责收口拍摄结果
拍摄完成后,关键不是回调本身,而是 markCaptureDelivered 如何判断后摄、前摄、单拍和双拍路径是否都已交付。它把不同拍摄模式收束为 GalleryMoment,这也是最小闭环里最值得定位的函数。
真机验收时可以分别跑单拍、双拍和顺序双拍。每一种模式最终都应该生成可读记录,而不是只在日志里显示拍摄成功。

markCaptureDelivered 把 PhotoOutput 结果转成后续可消费的记录
private async markCaptureDelivered(role: 'back' | 'front'): Promise<void> {
this.logCaptureTrace(
'mark-capture-delivered-enter',
`role=${role} backPath=${this.pendingBackCapturePath} frontPath=${this.pendingFrontCapturePath}`
);
if (this.pendingCaptureMode === 'sequence') {
if (role === 'back') {
this.backCaptureDelivered = true;
if (!this.frontCaptureDelivered) {
this.captureBusy = false;
this.pendingSingleCaptureRole = 'front';
this.cameraSequentialThumbnailUri = this.toPhotoImageUri(this.pendingBackCapturePath, '');
this.cameraSequentialThumbnailLabel = '主图已拍';
this.hideCameraCapturePreview();
this.cameraStatusText = '请确认副图画面后继续拍照';
this.lastCaptureSummary = '';
void this.prepareSequentialFrontCapture();
return;
}
} else {
this.frontCaptureDelivered = true;
if (!this.backCaptureDelivered) {
this.captureBusy = false;
this.pendingSingleCaptureRole = 'back';
this.cameraSequentialThumbnailUri = this.toPhotoImageUri(this.pendingFrontCapturePath, '');
this.cameraSequentialThumbnailLabel = '副图已拍';
this.hideCameraCapturePreview();
this.cameraStatusText = '请确认主图画面后继续拍照';
this.lastCaptureSummary = '';
void this.prepareSequentialBackCapture();
return;
}
}
if (this.backCaptureDelivered && this.frontCaptureDelivered) {
const selectedMemory = this.getSelectedMapMemory();
const captureId = this.pendingCaptureId.length > 0 ? this.pendingCaptureId : `${Date.now()}`;
const createdAt = parseInt(captureId, 10);
const capturePlace = this.pendingCapturePlace.length > 0 ? this.pendingCapturePlace : selectedMemory.place;
const captureTitle = this.pendingCaptureTitle.length > 0 ? this.pendingCaptureTitle : selectedMemory.title;
appendGalleryRecord 让记录真正进入应用
appendGalleryRecord 的职责比“数组 unshift 一条记录”更大。它会把记录补成本地图解状态,更新当前选中项,同步相册焦点,再触发持久化。这个函数跑通以后,用户才会在相册、地图和详情里看到一致的结果。
如果文章只讲拍照 API,却不讲 appendGalleryRecord,读者会缺少从系统回调到业务数据的关键桥。

appendGalleryRecord 同步记录、选中态和持久化
private async appendGalleryRecord(record: GalleryMoment): Promise<void> {
this.logCaptureTrace(
'append-gallery-record-start',
`recordId=${record.id} pairIndex=${record.pairIndex} backPath=${record.backPath} frontPath=${record.frontPath}`
);
const readyRecord = record.aiStatus === 'ready' ? record : GalleryRecordService.applyLocalInsight(record);
const nextRecords = [readyRecord, ...this.galleryRecords.filter((item: GalleryMoment) => item.id !== readyRecord.id)];
this.galleryRecords = nextRecords;
this.syncRecordSelections(nextRecords);
this.gallerySelectedId = readyRecord.id;
this.selectedGalleryGroupKey = this.buildGalleryRecordGroupKey(readyRecord);
this.galleryUserNoteDraft = this.getRecordUserNote(readyRecord);
this.showCameraCapturePreview(readyRecord);
this.syncSelectedMapMemory(true);
this.capturePairCount = nextRecords.length;
this.galleryNoticeText = this.hasGalleryFocus()
? this.getGalleryScopeDescription()
: ''
await this.syncMapMarkers();
this.updateAwarenessRecommendation(false);
await this.persistGalleryRecords(nextRecords);
this.gallerySelectedId = readyRecord.id;
this.selectedGalleryGroupKey = this.buildGalleryRecordGroupKey(readyRecord);
持久化是闭环的最后一道验收
记录写入内存只是中间态,重启应用后还能看到才算落盘。GalleryRecordService 负责把记录保存到 Preferences,并在读取时做归一化,旧字段、空字段和 fileUri 都需要在这里收口。
所以本篇的验收不止是“拍完出现一张图”,还要重启应用、返回相册、打开详情,确认记录仍然可读。

GalleryRecordService 把相册记录保存到本地并做恢复归一化
longitude: number;
backPath: string;
frontPath: string;
watermarkStyle?: GalleryWatermarkStyle;
watermarkText?: string;
}
export class GalleryRecordService {
private static readonly STORE_NAME: string = 'super_image_gallery';
private static readonly STORE_KEY: string = 'gallery_records';
private static readonly DEFAULT_USER_NOTE: string = '';
private static readonly DEFAULT_AI_POEM: string = '';
private static readonly DEFAULT_AI_CAPTION: string = '这份照片会保留拍摄地点、时间和画面氛围,你可以继续补充备注。';
private static readonly DEFAULT_VIDEO_PROMPT: string = '选择多张照片后,可以整理成一条回忆短片。';
static async loadRecords(context: common.UIAbilityContext): Promise<Array<GalleryMoment>> {
try {
const store = await preferences.getPreferences(context, GalleryRecordService.STORE_NAME);
const rawValue = store.getSync(GalleryRecordService.STORE_KEY, '[]') as string;
return GalleryRecordService.parseRecords(rawValue);
} catch (error) {
console.error(`Failed to load gallery records: ${JSON.stringify(error)}`);
return [];
}
}
static async saveRecords(context: common.UIAbilityContext, records: Array<GalleryMoment>): Promise<void> {
try {
const store = await preferences.getPreferences(context, GalleryRecordService.STORE_NAME);
store.putSync(GalleryRecordService.STORE_KEY, JSON.stringify(records));
await store.flush();
} catch (error) {
console.error(`Failed to save gallery records: ${JSON.stringify(error)}`);
}
}
真机验收步骤
| 验收点 | 操作 | 预期结果 |
|---|---|---|
| 拍照成功 | 在真机完成一次单拍或双拍 | 相册新增记录,地图或详情能读到同一条记录 |
| 取消拍摄 | 中途取消或拒绝权限 | 页面恢复可点击,不新增空记录 |
| 重启恢复 | 关闭应用后重新进入相册 | 记录仍存在,图片 Uri 可读 |
| 失败提示 | 模拟文件路径不可读或保存失败 | 显示可读文案并允许重试 |
复现边界
这篇只验证最小拍照闭环,不承诺所有设备都支持前后双摄并发。并发能力仍以 getCameraConcurrentInfos 的真机返回为准。
如果设备不支持双摄并发,最小闭环可以降级为单拍或顺序双拍,只要最终记录能保存、恢复、查看,就仍然是有效闭环。
社区同步摘要
本文适合同步到 HarmonyOS 开发者社区,摘要可以聚焦“从 PhotoOutput 到 GalleryMoment 的最小闭环”,并附上 markCaptureDelivered、appendGalleryRecord、GalleryRecordService 三个源码入口。
今日练习
- 在 Index.ets 中搜索 markCaptureDelivered,并画出单拍和双拍两条分支。
- 拍一张照片后重启应用,确认记录是否还能打开。
- 故意拒绝相机权限,记录页面如何恢复。
更多推荐



所有评论(0)