ArkUI + HarmonyOS 图稿校对台:交付验收与异常兜底(连载06/06)【共创稿事节】
交付前最后一次检查,不应该从“界面好不好看”开始,而应该从一个更难回答的问题开始:当作品被移除、版次发生变化、旧分页结果迟到时,图稿校对台还能不能把人带到正确的审阅位置? 如果答案依赖某个页面恰好没有销毁,或者恰好没触发并发加载,这套方案还没真正收尾。
这篇延续此前五个阶段的工程约定。SketchWork 的稳定身份是 id;SketchRevision 以 revisionId 关联 workId;ReviewMark 使用 markId + workId + revisionId 定位一条具体意见,xPermille、yPermille 的合法范围是零至一千。目录的分批加载与批注的懒加载分别由不同的数据源管理,selectedId 是页面唯一的作品选择来源。最后一篇不再引入另一套框架,而是把这些约定做成能维护、能定位异常的验收边界。

从六个边界审查最终工程
第一条边界是身份不能漂移。作品标题可以修改,列表顺序可以改变,但 sk-01 不能因为排序换成 sk-03。版次显示标签 R2 不能替代完整的 revisionId,因为其他作品也可能存在同名版次。每一条批注都要核对作品与版次双重归属。若只检查 markId 是否存在,跨作品同名编号或者脏数据就可能被误用。
第二条是坐标语义统一。保存的是图稿内容区域上的千分比坐标,不是当前窗口的像素。缩放前后只能换算显示坐标,不能把一次换算后的像素结果写回模型。对于负数、无穷大或超过一千的输入,要明确拒绝或收敛;面向历史数据恢复时建议拒绝越界批注,并向界面提供可诊断的状态,避免在画布外绘制交互热点。
第三条是会话快照不等于业务数据。此前的 ReviewSessionStore 负责记住当前作品、版次和批注指针,它本来就可能过期;任何一次恢复都要回到作品库验证,而不能让快照凭空创造对象。第四条是布局不改变业务选择。同一份 selectedId 与 revisionId 驱动窄屏单列和宽屏双栏,窗口调整时不应该重新初始化它们。
第五条是加载代次必须有效。上一阶段为分批目录定义的 generation 令牌必须在筛选条件重置时递增;当旧请求晚于新请求返回,应丢弃旧结果,不能把两种筛选的记录拼接到同一目录。第六条是更新粒度与通知一致。批注状态改变应针对稳定 markId 通知列表数据源;如果只是处理一个意见,却调用完整数据源重置,要说明这种成本是否必要。

这六项构成审阅工作台的核心合同:业务实体是否存在决定可否继续审阅;页面宽度、滚动位置和按钮布局只是呈现层信息。把两者倒过来管理,未来增加更多设备类型时就会成倍增加排障难度。
用纯 ArkTS 函数封住最容易遗漏的恢复路径
以下代码放在 entry/src/main/ets/model/ReviewAcceptance.ets。它沿用第二篇约定的三个业务接口,增加一个只用于验收与恢复的会话引用类型。输入由原来的作品仓库、版次仓库和批注仓库提供,不依赖新系统能力;输出是经过验证的引用而不是把旧缓存重新当成事实。
import { SketchWork, SketchRevision, ReviewMark } from './ReviewTypes';
export interface ReviewPointer {
workId: string;
revisionId: string;
markId: string;
}
export interface ReviewResolution {
pointer: ReviewPointer | null;
warning: string;
}
function validPoint(mark: ReviewMark): boolean {
return Number.isInteger(mark.xPermille) &&
Number.isInteger(mark.yPermille) &&
mark.xPermille >= 0 && mark.xPermille <= 1000 &&
mark.yPermille >= 0 && mark.yPermille <= 1000;
}
export function resolveReviewPointer(
saved: ReviewPointer,
works: SketchWork[],
revisions: SketchRevision[],
marks: ReviewMark[]
): ReviewResolution {
const work = works.find((w: SketchWork) => w.id === saved.workId);
if (work === undefined) {
return { pointer: null, warning: '原作品已不在目录中' };
}
const candidates = revisions.filter((r: SketchRevision) =>
r.workId === work.id);
const current = candidates.find((r: SketchRevision) =>
r.revisionId === saved.revisionId);
const revision = current ?? candidates.reduce<SketchRevision | undefined>(
(latest: SketchRevision | undefined, item: SketchRevision) =>
latest === undefined || item.sequence > latest.sequence ? item : latest,
undefined);
if (revision === undefined) {
return { pointer: null, warning: '原作品没有有效版次' };
}
const mark = marks.find((m: ReviewMark) =>
m.markId === saved.markId && m.workId === work.id &&
m.revisionId === revision.revisionId && validPoint(m));
return {
pointer: { workId: work.id, revisionId: revision.revisionId,
markId: mark?.markId ?? '' },
warning: current === undefined ? '原版次已变化,已回到最新有效版次' :
(saved.markId.length > 0 && mark === undefined ?
'原批注不存在或坐标不合法' : '')
};
}
这段方法有意把三种失败区别开:作品消失时不选任何作品;作品仍在但版次已删除时,按 sequence 选择该作品当前最高有效版次;批注失效时保留合法作品和版次、只清空 markId。恢复过程不修改 ReviewMark,也不把失效批注写入会话存储。生产项目如果有“已归档版次不能恢复”的业务规则,只需要在选择候选版次时增加相应条件,不该偷偷改变 markId 的语义。
上述代码使用的是 ArkTS 的数组查找、过滤、数值检查等语言能力,并没有声称存在名为 resolveReviewPointer 的系统 API;它是应用自行实现的业务函数。对于海量版本索引,调用方应从查询层提供相应作品的有限候选集合,而不是每次把所有历史数据复制进组件。
按故障现象组织回归动作
实际验收可以直接照着现象操作,而不是等页面出现问题才翻日志。先准备三张作品、两次版次修订和数条批注,使 sk-01-r2 上有 m-001。记录它们在模型里的真实身份,之后依次调整窗口、切换图稿、关闭页面再打开。如果恢复到 sk-01 却展示 sk-02 的批注,优先检查过滤条件是否同时含 workId 与 revisionId,而不是先怀疑布局容器。

再做异常注入:把快照中的 workId 改为不存在的编号,预期出现作品不可恢复的提示;保留作品而改坏 revisionId,预期按约定回到合法版次;让 markId 指向已经移除的意见,预期只有批注焦点被清空。修改一条批注状态以后,确认其他批注的 markId、排序和滚动位置没有无故变化。这样一组例子能覆盖“全对象失效”“部分引用失效”和“局部内容变更”三种不同级别的问题。
目录加载则要主动制造先后错位:第一次请求模拟延迟返回,期间切换筛选并触发第二次请求。最后只能出现当前代次的数据。通过日志记录 generation、请求页号和是否被接受三项信息,日志只记录必要元数据,不要把图稿原图或批注正文写入调试输出。这里可以用受控的本地 Promise 测试替代外部网络服务,排除网络波动对业务判断的干扰。
export interface PageReply<T> {
generation: number;
items: T[];
}
export function acceptCurrentPage<T>(
reply: PageReply<T>, currentGeneration: number
): T[] {
if (reply.generation !== currentGeneration) {
return [];
}
return reply.items;
}
acceptCurrentPage 只处理“能否采用返回值”,具体把数据追加到 SketchCatalogSource 仍应由第五篇已有的数据源入口完成。调用方不能把空数组解释成“当前目录没有任何作品”,因为它也可能代表被舍弃的旧请求;建议同时保留 ignored 事件供排查。分页去重仍依据作品 id,不能靠索引比较。
交付清单比漂亮的性能数字更重要
最终验收至少准备四组观察记录。第一组记录窗口大小变化前后的选中作品、版次、批注三元组;第二组记录插入、修改与删除批注后触发了哪一种数据源通知;第三组记录目录请求代次及过期结果被舍弃的原因;第四组记录持久化快照在失效引用场景中的降级策略。每组都要有输入、预期和实际观测值,未进行设备测试的部分留到测试执行阶段填写,不提前给出“通过”或固定耗时。

从实现顺序看,先确保模型索引与引用校验,再处理单列双列布局,之后核对批注列表和图稿目录两套懒加载,最后进行不同窗口与前后台场景的回归。不要用“滚动流畅”替代可复现的指标:如果要评估首屏时延或帧稳定性,应在相同设备、构建模式、数据规模下留存基线与优化后记录,并把图像解码和列表构建的影响分开看。
HarmonyOS 7 对应的 API 26 环境也不意味着本篇所有示例都依赖新接口。示例使用成熟的 ArkUI 组件与普通 ArkTS 业务方法,不需要设备敏感权限,也不需要访问相机、媒体库和分布式数据。正式集成时应根据实际资源加载方式补充所需权限;若仅使用应用内图片资源与本地状态,则不存在为示例强行申请网络、相册等权限的理由。
最终这套图稿校对台完成的是可解释的工程闭环:作品身份稳定,批注有版次归属,窗口变化保留审阅指针,长列表和目录各自控制更新代价,错误引用能降级,迟到数据不会覆盖当前选择。它不是一个完整在线协作平台,也没有把本地会话等同于跨设备同步;这些扩展必须重新设计冲突处理与服务端身份协议。到这里,六篇的核心目标已经可以用明确的验收动作逐项核对,而不是依赖读者对界面效果的主观印象。
相关官方文档可直接查阅 ArkUI LazyForEach与数据源规范、Preferences API 及 HarmonyOS 文档中心。
更多推荐

所有评论(0)