HarmonyOS应用实战-启示散页-40-题库拖拽排序别直接改实体:用排序快照保护列表和持久化

编辑题库时把第三条答案拖到第一条,看上去只是移动数组元素;但如果用户随后取消编辑、拖动被打断,或者保存时另一处内容已经更新,直接修改持久化实体就会留下半完成顺序。排序本质上是一笔需要确认的编辑事务。

在这里插入图片描述

本文的边界:先区分已有事实与设计建议

已核对的实现是 DeckEditPage 使用 EditableAnswer 副本编辑,并在保存时构造 SaveDeckPayload;工程当前没有拖拽排序功能。本文的 DeckOrderDraft 是为这项功能提出的设计示例。

因此,本文不会把建议类代码当作已经上线的功能。它关注的是把问题放到正确 owner:页面负责表达意图,服务负责业务判断,仓储负责稳定数据,发布或启动期的检查只承担自己的职责。

在这里插入图片描述
在这里插入图片描述

先还原故障链,而不是直接修表面现象

这类问题通常跨越三个阶段:输入或构建产物进入系统、某个 owner 做出判断、结果在下一个入口或下次启动才被看见。只在症状页面补一行状态更新,会让当前路径看似恢复,却把错误留给重进页面、冷启动、另一个窗口或发布阶段。

阶段 应问的问题 常见错误
输入 数据、资源或操作从哪里来 直接相信页面数组或目录内容
判断 谁拥有校验、冲突和回退 组件回调顺手写持久化
提交 什么结果应稳定保存 半完成状态提前落库
反馈 哪些 owner 需要重新读取 共享完整可变对象

用一个小结果模型把判断说清楚

下面的模型是这条链路需要对外解释的最小信息。它不等同于底层 Preferences JSON,也不应混入 @State、导航栈或弹层开关。保持这种隔离,存储结构或页面布局变化时,业务判断仍可独立复查。

interface DeckOrderDraft {
  deckId: string;
  answerIds: string[];
  baseUpdatedAt: number;
  dirty: boolean;
}

模型的字段要能回答两件事:这次动作的结论是什么,以及下一层需要据此做什么。无法解释业务结果的字段留在页面或诊断记录中,不借机进入持久化对象。

排序草稿是编辑事务,不是数组技巧

function moveAnswer(draft: DeckOrderDraft, from: number, to: number): DeckOrderDraft {
  const nextIds = [...draft.answerIds];
  const [moved] = nextIds.splice(from, 1);
  if (!moved) {
    throw new Error('拖拽项不存在');
  }
  nextIds.splice(to, 0, moved);
  return { ...draft, answerIds: nextIds, dirty: true };
}

这里的关键不在语法,而在边界:临界输入先被规范化或校验,输出保持可解释;没有把组件实例、动画状态或整份用户内容带进这条路径。接入现有工程时,应复用已存在的模型和仓储接口,而不是并行复制一套名字相近的结构。

保存前用 id 重新映射答案

async function commitOrder(draft: DeckOrderDraft, service: DeckService): Promise<void> {
  const latest = await service.getById(draft.deckId);
  if (!latest || latest.updatedAt !== draft.baseUpdatedAt) {
    throw new Error('题库已在其他入口更新,请重新加载后排序');
  }
  const answers = draft.answerIds.map((id) => latest.answers.find((item) => item.id === id));
  if (answers.some((item) => item === undefined)) {
    throw new Error('排序快照与当前答案不一致');
  }
  await service.save({ ...latest, answers: answers as Answer[] });
}

这段处理放在服务或发布/启动编排层,而不是按钮回调中。只有在关键操作成功后,页面才更新展示并通知相关 owner 重新读取;一旦失败,旧数据应保持可见,用户得到可以理解的下一步,而不是一个已经变空的页面。

顺序草稿只保存 answerId,避免把展示位置误当成领域身份。拖动过程中答案被删除、另一个入口编辑了文本,都可能让旧 index 指向另一条内容。保存前重新用 id 映射最新答案,才能发现快照已经失效。

取消不需要反向移动数组:丢弃草稿即可。这个特性是排序设计比直接 splice 实体更可靠的原因,也是用户在编辑中返回、切换页面或发生冲突时仍能保持原数据可信的基础。

给失败路径一个与成功路径同等清楚的结果

很多实现只写了成功分支:资源能读就继续、草稿能保存就更新、导出能生成就分享。真正让问题难排的是失败后谁来保留旧状态、谁来给出可理解结果。下面的记录结构不要求原样进入工程,它表达的是本文必须留下的证据字段:操作对象、阶段、结果和下一步,而不是用户题库正文或完整原始输入。

interface Article40OperationRecord {
  topic: '题库拖拽排序别直接改实体:用排序快照保护列表和持久化';
  subject: string;
  phase: 'prepare' | 'commit' | 'recover';
  outcome: 'ok' | 'rejected' | 'fallback';
  reason?: string;
}

function describeArticle40Failure(subject: string, reason: string): Article40OperationRecord {
  return { topic: '题库拖拽排序别直接改实体:用排序快照保护列表和持久化', subject, phase: 'recover', outcome: 'fallback', reason };
}

这段边界避免了两个极端:其一,失败后只把页面清空,导致用户不知道是否已经写入;其二,为了排障直接记录问题、答案或整份配置。本文所涉及的每个操作都应能在不泄露内容的前提下说明“失败在哪里、旧状态是否保留、下一步该做什么”。

落地步骤:按顺序消除不确定性

  1. 进入编辑页时复制答案 id 和 updatedAt,不直接拿实体数组。
  2. 拖动只返回新的草稿,取消时直接丢弃草稿。
  3. 保存前重新读取题库,核对草稿基线版本。
  4. 一次性提交完整顺序,成功后再让列表刷新。

实施时先完成第一步的事实核对,再添加设计层。特别是本文涉及当前工程尚未提供的能力时,代码片段是落地方案,不是对现状的描述;不要为让页面尽快可点而绕过既有 Service 或 Repository。

验证不只看一次正常操作

覆盖“拖动后取消”“连续拖动再保存”“拖动期间另一入口更新”“删除一个答案后继续保存”四条路径。任何未确认操作都不能改变重新进入编辑页时的顺序。

建议把验证结果按“静态结构、服务路径、真机运行”分开记录:源码或清单只能证明配置与调用关系;运行路径才证明生命周期、资源读取、持久化和页面接线;发布平台的提交结果则需要在平台实际操作后再确认。

const article40Acceptance = {
  topic: '题库拖拽排序别直接改实体:用排序快照保护列表和持久化',
  staticEvidence: 'owner、目录或依赖方向已复查',
  serviceEvidence: '异常输入、成功提交与回退结果可区分',
  runtimeEvidence: '重进页面与冷启动后的结果一致',
  releaseEvidence: '截图、日志和导出内容不包含用户正文'
};

这份记录的作用不是替代真机或发布平台操作,而是防止“源码看起来合理”被误报为“用户路径已经证明”。特别是涉及资源、发布截图和隐私的主题,静态路径正确与实际产物正确之间仍隔着一次真实构建和设备复查。

常见问题与定位顺序

现象 首先确认 处理
取消后顺序仍变 是否就地 splice 了持久化数组 页面只持有草稿副本
删除后排序错位 是否把 index 当作身份 草稿保存 answerId,不保存 index
保存覆盖了别处修改 是否保存前比较 updatedAt 返回冲突并要求重新加载

排查时先从本文的 owner 和结果模型找起,再回到页面调用点。只搜索某个按钮或文案,大概率只能找到症状,不会找到导致重进、重启或并发后出错的事实来源。

取舍:保持轻量,但不把边界省掉

这不是要求轻量应用引入庞大框架。真正需要的是一个可审查的判断点、稳定的数据边界和可复查的验证路径。只影响当前动画、展开和按钮禁用的状态可以留在页面;会影响本地数据、多个入口、恢复或发布材料的规则,则必须离开页面临时状态。

判断 合适位置
只影响当前组件的展示节奏 页面 @State
会改变题库、收藏、历史或配置 Service + Repository
需要唤起其他页面重新读取 轻量刷新信号
需要解释包体、截图或发布风险 发布账本或受控场景

合并前再问三个问题

  1. 这段逻辑如果从另一个页面、快捷入口或恢复路径触发,是否仍会走同一个判断点?
  2. 动作失败时,旧数据、当前选择或发布材料是否会保持可解释状态?
  3. 下一位维护者能否从模型、仓储或账本定位这次变化,而无需阅读某个组件回调?

三个问题中只要有一个答不上来,就不应把逻辑继续塞进页面。此时更合适的动作是补齐 owner、把中间状态从持久化对象中拿出来,或者先建立可以复现异常的最小样本。这样做增加的代码不多,却能避免后续版本把一次临时修补扩散成长期数据债务。

对于“题库拖拽排序别直接改实体:用排序快照保护列表和持久化”这一主题,还要把变更前后的事实保留下来:变更前谁拥有数据或资源,变更后哪个入口读取它,失败时是否仍能回到可信状态。这样,后续版本即使替换页面、调整模块或更换发布流程,也不会失去判断依据。文中的模型和记录格式可以按项目命名调整,但“事实、判断、提交、回退”四个环节不应省略。

小结

拖拽反馈属于页面;排序结果属于可确认的草稿;最终持久化仍归服务。把这三件事拆开,列表在取消、冲突和恢复时才不会失真。
否仍能回到可信状态。这样,后续版本即使替换页面、调整模块或更换发布流程,也不会失去判断依据。文中的模型和记录格式可以按项目命名调整,但“事实、判断、提交、回退”四个环节不应省略。

小结

拖拽反馈属于页面;排序结果属于可确认的草稿;最终持久化仍归服务。把这三件事拆开,列表在取消、冲突和恢复时才不会失真。

Logo

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

更多推荐