HarmonyOS 发布快照与编辑冻结(连载05)【共创稿事节】
座位已经可以调整,个人草稿也各有归属,发布入口却还存在一个时间窗口:用户打开确认页后继续修改姓名,确认页可能悄悄跟着变化。确认对象如果只是编辑状态的引用,屏幕上刚看过的内容与最终提交的内容就可能不同。会议座签需要在进入确认时复制完整发布内容,并明确暂停编辑,直到确认结束或返回编辑。

发布对象只带需要交付的数据
NameplateDesk沿用前篇名单、个人草稿和座位计划。发布快照包含顺序修订,以及按座位排列的身份、姓名和职务。它不保存输入焦点、操作反馈或临时草稿,也不把整个EditorState序列化后交给下游。发布结构越接近实际交付内容,确认页越容易解释。
本篇选择严格规则:存在尚未应用的个人草稿时,暂缓进入发布确认。用户必须应用或整理这些差异,避免确认页一边显示已应用姓名,编辑区另一边还留着期待发布的称谓。这个规则属于当前业务流程,可以调整,但必须在快照创建之前明确执行。
顺序修订记录排列变化,快照则保存实际排列内容。只有修订号而没有内容,后续确认仍得回头读取可变名单;只有内容而没有修订,也不便定位这份确认依据哪个座位计划。两者结合后,确认阶段可以完全围绕同一份数据展开。
逐项复制,隔开编辑引用
新增model/ReleaseSnapshot.ets,导入均来自前篇完整文件。姓名与职务来自saved,座位来自当前order。快照中的每一行都是新对象,后续替换编辑状态不会改动它。这里没有使用共享GuestRecord来节约复制,因为发布数据很小,归属清晰比减少几个对象更有价值。
import { EditorState, isDirty } from './RosterEditor';
import { GuestRecord } from './RosterSeed';
import { SeatPlan, validSeatPlan } from './SeatOrder';
export interface ReleaseRow {
id: string;
seat: number;
name: string;
role: string;
}
export interface ReleaseSnapshot {
orderRevision: number;
rows: ReleaseRow[];
}
export function captureSnapshot(editor: EditorState, seats: SeatPlan): ReleaseSnapshot {
if (!validSeatPlan(editor, seats)) { throw new Error('座位与名单不一致'); }
if (editor.guests.some((guest: GuestRecord): boolean => isDirty(guest))) {
throw new Error('请先处理尚未应用的草稿');
}
const rows: ReleaseRow[] = seats.order.map((id: string, index: number): ReleaseRow => {
const guest: GuestRecord | undefined = editor.guests.find(
(item: GuestRecord): boolean => item.id === id);
if (guest === undefined) { throw new Error('发布身份不存在'); }
const row: ReleaseRow = {
id: id, seat: index + 1, name: guest.saved.name, role: guest.saved.role
};
return row;
});
const snapshot: ReleaseSnapshot = { orderRevision: seats.revision, rows: rows };
return snapshot;
}
字符串字段按值保存,行数组和行对象重新建立,因此快照与名单之间没有嵌套对象共享。若以后增加图片、样式或附件字段,要继续审视复制层级。把含对象的字段直接赋过去,只会复制引用,外层数组换新仍不足以保证内部内容独立。
快照创建时同时校验完整座序和草稿状态。不要先进入确认页,再逐项填充姓名;那会使页面短暂出现不完整内容,也使失败回退更复杂。准备阶段全部成功后,再一次替换确认状态,用户看到的就是完整对象。
确认状态单独管理
新增model/PublishGate.ets。locked表示当前处于确认阶段,snapshot保存该阶段唯一的内容来源。返回编辑会丢弃本次确认快照,下一次进入时重新捕获。确认发布返回交付对象并保留冻结状态,便于界面继续展示本次提交内容,编辑解锁必须通过明确动作完成。
import { EditorState } from './RosterEditor';
import { SeatPlan } from './SeatOrder';
import { ReleaseRow, ReleaseSnapshot, captureSnapshot } from './ReleaseSnapshot';
export interface PublishGate {
locked: boolean;
snapshot: ReleaseSnapshot | null;
message: string;
}
export function createGate(): PublishGate {
const gate: PublishGate = { locked: false, snapshot: null, message: '' };
return gate;
}
export function beginConfirmation(editor: EditorState, seats: SeatPlan): PublishGate {
const frozen: ReleaseSnapshot = captureSnapshot(editor, seats);
const gate: PublishGate = {
locked: true, snapshot: frozen, message: '编辑已冻结'
};
return gate;
}
export function backToEditing(): PublishGate {
const gate: PublishGate = { locked: false, snapshot: null, message: '可以继续编辑' };
return gate;
}
export function confirmedRows(gate: PublishGate): ReleaseSnapshot {
if (!gate.locked || gate.snapshot === null) { throw new Error('请先进入发布确认'); }
const copy: ReleaseSnapshot = {
orderRevision: gate.snapshot.orderRevision,
rows: gate.snapshot.rows.map((row: ReleaseRow): ReleaseRow => ({
id: row.id, seat: row.seat, name: row.name, role: row.role
}))
};
return copy;
}
交付函数再复制一次,是为了让接收方不能通过修改返回对象来改变确认页。当前约定依靠函数封装与调用纪律,后续持久化时还应检查存储格式和版本。冻结是业务生命周期状态,不能把一个锁图标当成已经完成数据保护。
本篇的确认动作只产生可交付的数据,不直接扩展网络上传或打印服务。接入这些服务时,应把同一个快照作为参数,并在成功响应前保留确认内容。失败后提供重试或返回编辑,不能用正在变化的编辑器重新组装请求。
用条件渲染阻止编辑入口继续工作
下面的pages/Index.ets是发布阶段的完整页面,沿用名单与座序服务。初始数据使用两人的已应用内容,顺序为周明、陈思远。进入确认后只渲染快照卡片,返回后再出现上移操作。页面通过业务动作切换状态,提交内容始终来自confirmedRows。
import { EditorState, createEditor } from '../model/RosterEditor';
import { SeatPlan, createSeatPlan, moveGuest } from '../model/SeatOrder';
import { ReleaseRow, ReleaseSnapshot } from '../model/ReleaseSnapshot';
import { PublishGate, createGate, beginConfirmation,
backToEditing, confirmedRows } from '../model/PublishGate';
@Entry
@Component
struct Index {
@State editor: EditorState = createEditor();
@State seats: SeatPlan = moveGuest(createSeatPlan(this.editor), 'guest-csy', 1);
@State gate: PublishGate = beginConfirmation(this.editor, this.seats);
@State notice: string = '';
private prepare(): void {
try { this.gate = beginConfirmation(this.editor, this.seats); }
catch (error) { this.notice = String(error); }
}
private submit(): void {
const payload: ReleaseSnapshot = confirmedRows(this.gate);
this.notice = `已确认${payload.rows.length}个座位的发布内容`;
}
private move(): void {
if (this.gate.locked) { return; }
this.seats = moveGuest(this.seats, 'guest-csy', -1);
}
build() {
Scroll() {
Column({ space: 16 }) {
Text('会议座签').fontSize(26).fontWeight(FontWeight.Bold)
Text(this.gate.locked ? '发布确认' : '编辑座位').fontSize(20)
if (this.gate.snapshot !== null) {
Text(this.gate.message).fontColor('#145A44')
Text(`顺序修订 ${this.gate.snapshot.orderRevision}`)
ForEach(this.gate.snapshot.rows, (row: ReleaseRow) => {
Column({ space: 8 }) {
Text(`座位${row.seat} ${row.name}`).fontSize(22)
Text(row.role).fontSize(16)
}.width('100%').padding(20).backgroundColor('#FFFFFF')
}, (row: ReleaseRow) => row.id)
Button('确认发布').width('100%').onClick(() => { this.submit(); })
Button('返回编辑').width('100%').onClick(() => {
this.gate = backToEditing(); this.notice = '';
})
} else {
Text(`座位顺序:${this.seats.order.join(',')}`).fontSize(16)
Button('陈思远上移').onClick(() => { this.move(); })
Button('进入发布确认').onClick(() => { this.prepare(); })
}
Text(this.notice).fontSize(16)
}.width('100%').padding(20)
}.width('100%').height('100%').backgroundColor('#F7F6EF')
}
}

实际编辑页的姓名输入、职务输入和座位按钮也应在同一冻结条件下隐藏或停用,业务回调仍需检查状态。条件渲染处理可见入口,回调检查处理已经排队的动作。若多个组件各自保存一个锁标记,就容易出现某处仍能修改数据的缝隙,因此确认状态应由共同的业务层持有。
排查确认内容漂移
定位时沿三条数据线观察:编辑器当前saved、座位计划order、确认快照rows。进入确认后改变前两项的测试副本,快照内容应保持原值。再修改confirmedRows返回的数组,确认页也应保持原样。这些检查能区分捕获时漏复制与交付时泄露引用两类问题。
另一个关键分支是尚有草稿。为陈思远填写主持称谓但暂缓应用,再进入确认,应停留在编辑流程并显示处理草稿的提示。应用之后重新准备,快照才包含新称谓。不能只看按钮是否变灰,还要核对实际捕获的数据来自哪一层。

返回编辑会开启新的确认周期
返回之后用户可以换座位或修改个人内容,原快照已经被清空。再次进入确认会依据最新的已应用资料创建新对象,顺序修订也随之更新。不要沿用上一次数组后逐项补丁修改,否则确认周期之间又会出现隐蔽共享。
对于较宽窗口,编辑区与确认区可以同时摆放,但冻结期间的编辑区仍应采用一致规则。页面布局不改变快照生命周期,窗口宽度也不能触发重新捕获。排版可以随尺寸重新计算,确认的姓名、职务和顺序则保持稳定。
发布前还要明确版面检查的位置
前篇字号选择依赖实际可用宽度,发布快照则保存业务内容,两者并非同一类数据。若发布目标具有固定座签尺寸,应在捕获前按该目标尺寸完成排版检查;若多个输出尺寸共用内容,则分别检查各目标的布局结果,不能把当前手机预览能放下直接当作所有输出尺寸都合适。
这也解释了为什么快照里没有保存手机窗口宽度。窗口宽度属于当前查看环境,姓名和座位属于本次发布对象。后续增加版面配置时,可以把明确的目标尺寸、字体方案和内容一起组成发布合同,但不要把某次界面测量得到的临时值混进人员资料。
本篇发布页聚焦捕获与确认阶段,个人编辑服务与前篇文件继续保留。完整工程将发布页接入原编辑页时,应共用同一份editor和seats,由编辑页发起beginConfirmation,再把结果交给确认视图。不要重新createEditor来替代用户正在编辑的名单;本页初始化只用于呈现确定的两人确认样本。
异步接入后仍需守住同一份内容
如果提交过程耗时,应增加提交中状态,并让重复确认复用同一次提交对象。用户返回编辑后发起的新确认属于新周期,旧请求回调需要核对所属周期再更新页面。否则旧结果可能覆盖新确认的提示,即便快照本身独立,交互反馈仍会错位。
错误处理也应保留足够上下文。捕获失败直接留在编辑阶段,提交失败保留确认快照并给出重试入口,主动返回则清空当前快照。三种分支对应不同生命周期,不宜都归结为一个清空状态的通用错误处理,否则用户难以判断刚才确认的内容是否还存在。
快照把确认对象固定下来,冻结把编辑动作暂停下来,两者一起定义了清晰的发布起点。后续加入撤回和多窗口核对时,可以继续围绕这份明确内容判断差异,而不必猜测用户当时确认过哪一版座签。
更多推荐

所有评论(0)