《海浪角》这幅八乘八的十字绣图谱已经从单格落针走到了镜像预览、用线预算、缩放命中、宽屏分栏、撤销重做和符号图例。现在画面上有五针,却距离六十四格全部完成还有一段距离。编辑器需要的是一个可信的检查点:用户可以保存当前针位、线色、焦点和显示选项,下次打开时回到相同的工作位置,而不是把五针的半成品误写成已完工图谱。

目前五条针位仍是湖蓝B位于第二行第二、三列以及第五行第五列,珊瑚橙C位于第三行第四列,乳白A位于第四行第四列。已落针五格,完成率为7.8125%;按每针含余量0.392米估算,总用线1.960米,其中B需要1.176米。湖蓝使用同染色批次B-02,库存1.200米,预算剩余0.024米。符号模式打开,选中格保持第五行第五列。这些数字应当由针位与库存契约重新计算,不能把界面上看到的统计文字原样存档。

图谱检查点与归档校验

为什么归档的是检查点,不是完成状态

归档有两类不同的含义。一类是作品确实完成,进入只读陈列;另一类是编辑中的安全检查点,用于后续继续工作。当前图谱只有五针,属于后一类。若业务层仅根据“已经存盘”就将状态写成“已完成”,列表排序和进度统计会同时出错。这里把archiveStatus定义为checkpoint,页面显示“已保存检查点”和“稿件尚未完成”,不提供误导性的成品完成标签。

检查点要冻结可以独立解释的原始数据:八乘八的画布尺寸、五条落针、稳定选中键、符号显示状态、已选染色批次以及格式版本。落针数组是事实来源,计数、用线、进度都属于可以重新推导的结果。若额外保存缓存汇总,只能作为回读核对字段,校验失败时应该拒绝加载,而不是以缓存覆盖针位。

我会在存档对象里保留业务版本与文件格式版本这两个不同概念。格式版本决定读取器如何解析结构;检查点版本用于用户区分本次保存和未来的下一次保存。未来如果加入多幅图谱,应该按图谱稳定键分别管理,而不能始终覆盖同一个全局值。当前文章固定在这一幅《海浪角》上,使用一个明确的检查点键即可。

export type ColorId = 'A' | 'B' | 'C';
export interface StitchCell {
  row: number;
  col: number;
  colorId: ColorId;
}
export interface StitchCheckpoint {
  formatVersion: number;
  checkpointVersion: number;
  patternName: string;
  width: number;
  height: number;
  selectedKey: string;
  showSymbols: boolean;
  blueLotId: string;
  status: 'checkpoint';
  stitches: StitchCell[];
}
export const CURRENT_CHECKPOINT: StitchCheckpoint = {
  formatVersion: 1, checkpointVersion: 1,
  patternName: '海浪角', width: 8, height: 8,
  selectedKey: '5-5', showSymbols: true,
  blueLotId: 'B-02', status: 'checkpoint',
  stitches: [
    { row: 2, col: 2, colorId: 'B' },
    { row: 2, col: 3, colorId: 'B' },
    { row: 3, col: 4, colorId: 'C' },
    { row: 4, col: 4, colorId: 'A' },
    { row: 5, col: 5, colorId: 'B' }
  ]
};

这份结构没有把线色转换成视觉色号。乳白、湖蓝和珊瑚橙的显示配置沿用前一篇的图例契约,A为空心圆、B为实心菱形、C为实心三角;储存的仍是A、B、C这三个业务编码。恢复时先从存档恢复针位和选中键,再让页面投影出格子和图例,避免切换色彩主题时导致历史文件全部变动。

读取之前把五类错误挡在数据层

数据解析成功不等于内容能够使用。一份JSON可以拥有完整字段,却在第九行放了一针、重复填写第二行第三列,或者把线色写成不存在的D。把这些异常留给GridItem渲染会出现不可预测的统计,甚至让库存计算在两个页面得到不同结论。因此读取层的职责是验证坐标范围、唯一位置、颜色字典、选中键及预算范围;通过之后才产生供页面使用的对象。

第五行第五列也必须经过同样的范围检查。用户上次缩放的比例、平移量是临时视图状态,不应和选中针位混在一起。检查点只记录业务键5-5,手机与平板重新进入时,由各自容器计算适合的显示宽度,选中对象始终指向相同格子。这样无需把某个设备上的像素坐标跨设备带走。

export interface CheckResult {
  ok: boolean;
  reason: string;
  countA: number;
  countB: number;
  countC: number;
}
export function inspectCheckpoint(data: StitchCheckpoint): CheckResult {
  const fail = (reason: string): CheckResult => ({
    ok: false, reason, countA: 0, countB: 0, countC: 0
  });
  if (data.formatVersion !== 1 || data.status !== 'checkpoint' ||
      data.width !== 8 || data.height !== 8 ||
      data.patternName !== '海浪角' || data.blueLotId !== 'B-02') {
    return fail('基础契约不匹配');
  }
  if (data.stitches.length > 64 || !/^([1-8])-([1-8])$/.test(data.selectedKey)) {
    return fail('选择位置或数量有误');
  }
  const occupied: Set<string> = new Set<string>();
  let a = 0; let b = 0; let c = 0;
  for (const item of data.stitches) {
    const key: string = `${item.row}-${item.col}`;
    if (!Number.isInteger(item.row) || !Number.isInteger(item.col) ||
        item.row < 1 || item.row > 8 || item.col < 1 || item.col > 8 ||
        occupied.has(key)) return fail('无效或重复针位');
    occupied.add(key);
    if (item.colorId === 'A') a++;
    else if (item.colorId === 'B') b++;
    else if (item.colorId === 'C') c++;
    else return fail('未知线色');
  }
  if (b * 0.392 > 1.2 + 0.000001) return fail('B-02预算超出库存');
  return { ok: true, reason: '通过', countA: a, countB: b, countC: c };
}

业务检查与精确内容比对是两件事。上面的函数适用于校验同类型的未来检查点,因此不会强迫所有存档永远恰好五针;本篇的验收样本则必须得到A一针、B三针、C一针以及选中5-5。若文件的针位内容被替换成另一套同样合法的图案,普通范围检查发现不了这种改变。需要更强的字节完整性时,应在保存层引入规范化序列化和可靠摘要机制,把摘要与具体版本绑定;仅靠计数相等并不足以证明内容完全相同。

库存判断也要说明口径。这里的0.392米是单针预算,包含预留量,不是已经从线轴上测量的真实消耗;B-02剩余0.024米是预算余额。A和C各需0.392米,整幅五针累计预算是1.960米。恢复后如果发现第三针改了颜色,应该从五条针位重新汇总,而不是沿用截图上旧的1.176米数字。

写入顺序决定恢复时能看到哪一版

归档操作不宜由页面同时调用多个保存入口。我的做法是让单一服务串行管理写入:先校验检查点,在独立键里保存完整JSON并刷新到持久层,然后更新当前检查点指针。打开页面时先读取指针,再读取对应内容并重新校验。若写入内容后、更新指针前发生中断,旧指针仍指向上一份有效记录;若读取目标内容失败,则提示用户保留当前编辑状态,不拿空数组覆盖屏幕。

HarmonyOS的用户首选项preferences支持按键存储字符串及异步put、flush。这里保存的业务数据体量很小,用JSON字符串即可;它适用于单应用进程内的轻量检查点,不应把多进程并发写入放在同一份配置里。为了降低用户连续点击导致的竞态,服务调用方还要在提交期间关闭再次保存按钮,或者建立串行写入队列。

import { preferences } from '@kit.ArkData';
export async function saveCheckpoint(context: Context,
  record: StitchCheckpoint): Promise<void> {
  const check: CheckResult = inspectCheckpoint(record);
  if (!check.ok) throw new Error(check.reason);
  const store = await preferences.getPreferences(context, { name: 'stitch_archive' });
  const key: string = `checkpoint_${record.checkpointVersion}`;
  await store.put(key, JSON.stringify(record));
  await store.flush();
  await store.put('current_checkpoint', key);
  await store.flush();
}
export async function loadCheckpoint(context: Context): Promise<StitchCheckpoint | null> {
  const store = await preferences.getPreferences(context, { name: 'stitch_archive' });
  const key = await store.get('current_checkpoint', '');
  if (typeof key !== 'string' || key.length === 0) return null;
  const content = await store.get(key, '');
  if (typeof content !== 'string' || content.length === 0) return null;
  const record = JSON.parse(content) as StitchCheckpoint;
  return inspectCheckpoint(record).ok ? record : null;
}

保存动作完成后,UI可以显示“检查点01·已保存”,按钮名称保持“读取检查点”。保存失败必须让编辑器继续保留原先内存里的五针;读取失败则提供重试入口,不能创建一个新空稿并立即写回同一键。若需要同时支持多个历史检查点,可把所有键列为业务索引,再明确指定当前指针,同时限制单份记录体积和存储总量。

归档读取与针位校验的开发环境

恢复之后如何判断画面真的来自同一份数据

页面恢复不只是填充五个格子。还要恢复选中位置第五行第五列、符号模式开启、湖蓝批次B-02以及衍生的线色统计。Grid中六十四个条目采用行列稳定键,单列手机与双栏平板根据容器宽度重新排版,业务对象本身没有被重新创建成另一套事实来源。符号图例从A、B、C查表生成,选中边框单独绘制,这与第七篇的色觉辅助规则保持一致。

我更倾向于为恢复过程准备一组确定样本:正常五针、重复坐标、非法颜色、超界行列、B批次预算不足,以及损坏JSON。正常记录应得到5/64、7.8125%、A一针、B三针、C一针;异常记录各自返回清楚的原因,不能一律转成空图。这样的检查不需要假设某次页面打开一定成功,而是让状态转换的期望可以被逐项审阅。

图谱还没有完工,但检查点已经具备清晰的审阅价值:读者可以知道保存的是什么、没有保存哪些临时视图数据、哪项错误会阻止恢复。界面上“已保存”和“尚未完成”同时出现,是为了让存储状态与作品状态各归其位。

手机界面的检查点与用线状态

ArkUI网格结构与稳定键可参见华为开发者创建网格Grid/GridItem,首选项持久化接口参考华为开发者Preferences官方接口。到这里,StitchAtlas的八篇连载从一个坐标格子延伸成了能编辑、能撤销、能辨认、能计算用线并能恢复检查点的完整业务链。下一次继续落针时,仍应从经过检查的五针状态出发,而非从截图或统计缓存反向构造图谱。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐