HarmonyOS摄影选片连载06:批注意见分组与筛选追踪【共创稿事节】
摄影棚选片进入批注阶段后,屏幕上画出的矩形和标记点只是意见的落点,真正推动交付的还有每条意见的内容、类别与处理状态。前面的工作流已经从系统相册获取P001~P003,复制工作图到应用目录,并为P001的4000×3000原图建立了一个矩形框:左上角(600,450),右下角(1800,1200);三个标记点分别位于(1020,800)、(1680,620)和(1820,1180)。缩放至1.6倍并平移(-40,-20)后,坐标仍以原图像素保存,撤销重做则以完整的批注文档快照维护。
但当摄影师在点1写下“花瓣边缘需要细化”,在点2写“高光层次再保留一点”,又给矩形区域添加“构图边距过紧”时,仅靠图上的蓝色圆点已无法快速找到待处理意见。摄影师切换到另一张照片、调整检索条件或恢复页面以后,意见还必须跟随原来的标记身份,而非跟随列表行号。第六篇将批注几何、意见文本、筛选条件拆成三个对象,让处理状态与坐标互不干扰。

意见与标记建立稳定的关联关系
每条批注意见都需要自己的noteId,同时引用所属资产assetId和目标targetId。例如,P001的点1保留point-1作为标记身份,意见note-101关联它;点2的note-102写入色彩类意见;矩形的note-103指向region-main。即使点2被移动到新原图坐标(1700,650),它仍然是同一个点,而不是创建新意见。只有用户明确删除标记或者解除绑定时,才需要询问相关意见如何处理。
工程在model/Annotation.ets与model/AnnotationHistory.ets之外增加model/ReviewNote.ets、service/NoteStore.ets和pages/NoteBoardPage.ets。批注文档的修订号与意见自身的修改序号各自递增:前者描述几何内容,后者描述意见文本及状态。用户调整图像的缩放和平移只是视图变化,不应给意见增加新的修订号。
export type NoteGroup = 'COMPOSITION' | 'COLOR' | 'DETAIL';
export type NoteState = 'OPEN' | 'IN_PROGRESS' | 'RESOLVED';
export interface ReviewNote {
noteId: string;
assetId: string;
targetId: string;
group: NoteGroup;
state: NoteState;
text: string;
noteRevision: number;
documentRevision: number;
updatedAt: number;
}
export interface NoteBoard {
assetId: string;
notes: ReviewNote[];
selectedNoteId: string;
}
界面上的“构图”“色彩”“细节”都是意见分类,不是系统相册属性。标签由业务层控制,暂时不允许用户随意拼写新的组名,避免“高光”“亮部”“高亮细节”在列表里被拆成三个含义相近的维度。可在后续加入受控词表,但记录的稳定ID与归属关系不需要因此变化。
处理状态由明确的编辑意图驱动
新建意见进入OPEN,有人开始处理后转为IN_PROGRESS,复核结果被接受后转为RESOLVED。若需要返工,从已解决重新打开应记下原因,而不是静默把状态改回最初值。即使目前只有一位摄影师使用该工具,也应把状态变化写成独立函数,以便将来加入接手人或操作日志时不必重新检查所有按钮。
更新函数至少校验目标意见存在、资产身份一致、修改序号匹配和文本非空。匹配成功后返回新对象,不直接修改@State数组里的原对象;页面通过替换数组接收变化,更容易让ArkUI组件触发界面刷新。对于已经处理完成的意见,文字改动可以单独作为一项内容修订,而不是顺带重置处理状态。
export interface NoteUpdate {
noteId: string;
expectedRevision: number;
text: string;
nextState: NoteState;
}
export type NoteUpdateResult =
{ kind: 'APPLIED'; note: ReviewNote } |
{ kind: 'CONFLICT' } |
{ kind: 'MISSING' };
export function updateNote(
source: ReviewNote[], command: NoteUpdate, changedAt: number
): NoteUpdateResult {
const current = source.find((item: ReviewNote) => item.noteId === command.noteId);
if (!current) { return { kind: 'MISSING' }; }
if (current.noteRevision !== command.expectedRevision) {
return { kind: 'CONFLICT' };
}
if (command.text.trim().length === 0) {
throw new Error('批注意见不能为空');
}
return { kind: 'APPLIED', note: {
...current, text: command.text.trim(), state: command.nextState,
noteRevision: current.noteRevision + 1, updatedAt: changedAt
} };
}
旧修订号与当前修订号不一致时,界面需要保留用户正在编辑的文字并提示重新查看意见,而不能拿旧文本覆盖新记录。批注图形本身已经有撤销重做栈;意见状态不应被任意一次几何撤销连带改写。两个历史轨道可以使用共同的资产ID展示在同一个页面,但提交逻辑与冲突判断保持独立。
筛选追踪依靠ID,不依靠数组位置
列表可以同时按意见分类、状态和关键词过滤。处理照片P001时,摄影师选中note-102并输入“高光”,此时只出现色彩类的待处理意见;将分类改成“全部”后,选中的仍应是note-102。若转而搜索“构图”使它暂时不可见,详情区显示“该意见未包含在当前筛选”,而非自动跳到列表第一条。这个规则与此前港口箱位的稳定ID思想相似,但两个系列各自采用独立模型和工程场景。
export interface NoteFilter {
group: NoteGroup | 'ALL';
state: NoteState | 'ALL';
keyword: string;
}
export function selectNotes(
source: ReviewNote[], assetId: string, filter: NoteFilter
): ReviewNote[] {
const key = filter.keyword.trim().toLowerCase();
return source.filter((note: ReviewNote) => {
if (note.assetId !== assetId) { return false; }
if (filter.group !== 'ALL' && note.group !== filter.group) { return false; }
if (filter.state !== 'ALL' && note.state !== filter.state) { return false; }
return key.length === 0 || note.text.toLowerCase().includes(key) ||
note.targetId.toLowerCase().includes(key);
}).sort((a: ReviewNote, b: ReviewNote) =>
b.updatedAt - a.updatedAt || a.noteId.localeCompare(b.noteId));
}
检索仅针对本次会话已授权的资产意见,不扫描整个设备相册。关键词在进入过滤函数之前作空格归一化,中文备注保留原有大小写和标点用于显示,搜索时才转换比较形式。对于大量意见,建议根据资产身份分段加载,避免每次输入都重新生成全部缩略图;列表行也应使用noteId作为ForEach的键,确保输入框、选中颜色和详情弹层不会随排序变动错位。

页面既显示批注位置,也显示处理进度
点列表中的意见时,详情需要根据targetId找到对应几何对象,再使用上一篇的sourceToView()将原图坐标投影到当前视图。点2在放大到1.6倍时位置依然来自(1680,620)或最新已提交的(1700,650),而不是来自意见列表的行高。若目标标记已经被删除,意见仍保留在列表中并显示“目标标记已移除”,便于业务人员决定迁移意见还是归档;不能因为绘制层找不到它就顺手删除文字。
@Entry
@Component
struct NoteBoardPage {
@State private keyword: string = '';
@State private notes: ReviewNote[] = [];
@State private selectedNoteId: string = '';
@State private activeGroup: NoteGroup | 'ALL' = 'ALL';
private assetId: string = 'P001';
private getVisible(): ReviewNote[] {
return selectNotes(this.notes, this.assetId, {
group: this.activeGroup, state: 'ALL', keyword: this.keyword
});
}
build() {
Column({ space: 12 }) {
Text('摄影棚批注意见').fontSize(22).fontWeight(FontWeight.Bold)
Text('P001 · 花卉静物 · 批注面板')
TextInput({ placeholder: '搜索意见或标记ID', text: this.keyword })
.onChange((value: string) => { this.keyword = value; })
Row({ space: 8 }) {
Button('全部').onClick(() => { this.activeGroup = 'ALL'; })
Button('色彩').onClick(() => { this.activeGroup = 'COLOR'; })
Button('细节').onClick(() => { this.activeGroup = 'DETAIL'; })
}
List() {
ForEach(this.getVisible(), (note: ReviewNote) => {
ListItem() {
Column({ space: 4 }) {
Text(`${note.targetId} · ${note.group}`)
Text(note.text).fontSize(14)
Text(note.state).fontColor(Color.Gray)
}.padding(12).width('100%')
}.onClick(() => { this.selectedNoteId = note.noteId; })
}, (note: ReviewNote) => note.noteId)
}.layoutWeight(1)
Text(`当前意见:${this.selectedNoteId || '未选择'}`)
}.padding(16).height('100%')
}
}
这里的列表是意见的阅读视图,几何编辑仍由之前的批注页负责。实际页面可把色彩、构图、细节三个分类按钮放入横向滚动区域,也可以让平板显示“左侧意见列表、右侧图片定位”的布局。切换窗口尺寸时保持selectedNoteId,但不把“当前第几行”作为持久状态。搜索框内容应留在页面状态中,输入回调只更新查询值;如果后续增加远端索引,再单独控制请求代次以免过期响应污染当前查询。
小型意见板的落盘与恢复
意见数量较少时,可按资产ID将结构化数组存入Preferences,同时保存数据格式版本。持久层只写意见内容和状态,不存放花卉原图,也不把当前缩放倍数写到每条意见中。Preferences适合单进程小型数据;写入后执行flush(),并在读取时检查JSON数组、资产ID和修订号。对异常格式应将当前资产标记为“意见待恢复”,给出重新加载或导入备份的入口,不能用空数组覆盖原来的存储键。
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
export async function storeNotes(
context: common.UIAbilityContext, assetId: string, notes: ReviewNote[]
): Promise<void> {
const store = await preferences.getPreferences(context, { name: 'photo_review_notes' });
const owned = notes.filter((note: ReviewNote) => note.assetId === assetId);
const payload = JSON.stringify({ schema: 1, assetId: assetId, items: owned });
await store.put(`notes_${assetId}`, payload);
await store.flush();
}
export async function loadNotes(
context: common.UIAbilityContext, assetId: string
): Promise<string> {
const store = await preferences.getPreferences(context, { name: 'photo_review_notes' });
return String(await store.get(`notes_${assetId}`, ''));
}
调试路径可以从固定的三条意见开始。选中色彩类note-102,修改关键词后确认焦点仍指向同一条;把点2移动到(1700,650),检查意见关联仍是point-2;改变缩放和平移,检查列表不会生成新意见;把note-101从待处理转为处理中,再用旧修订号提交,观察冲突分支是否保留旧内容;让存储数据格式不正确,检查恢复提示是否避免覆盖。HiLog记录资产ID、筛选命中条数、意见修订号和保存阶段即可,原图URI及完整摄影意见不必写进日志。

到这一阶段,摄影选片工具已经把“图像像素坐标”“批注几何快照”“批注意见”和“检索视图”分成四种稳定的数据。处理人员可以按照构图、色彩或细节查找待办,编辑过程不会因缩放而丢位置,撤销几何修改也不会破坏处理意见。接下来的交付清单和版本导出能够沿用这一组关联身份,不必再凭照片缩略图或列表序号推断意见属于哪里。
参考资料:ArkUI列表创建、TextInput修改数据后的光标位置、Preferences存储接口、ArkTS语言概述。
更多推荐



所有评论(0)