HarmonyOS ArkUI:宽屏工序工作台的主从布局与数据同源(连载05)【共创稿事节】
DarkroomFlow的五段暗房流程已经有顺序确认、倒计时暂停、重复请求拦截,以及计划与实际耗时双轨汇总。到了这一阶段,手机页上按顺序展示五张工序卡片还比较自然;但把它放到宽屏工作台时,继续把所有内容拉成一条长列表,会让操作员在上方选择工序,又向下滚动查看时间偏差,确认按钮和当前对象容易失去视觉关联。
上一节的账本需要原样保留:显影计划08:00、实际08:40,停止计划00:30、实际00:45,二者已经完成;定影计划05:00,目前处于就绪状态;水洗计划10:00、干燥计划20:00仍待前序完成。当前流程修订号为9,已完成2/5,已完成计划08:30、实际09:25、累计偏差+00:55,剩余计划35:00,预测总耗时44:25。本节的目标不是修改这些数字,而是在不同宽度下保持一份状态来源。

先决定哪个区域拥有操作权
宽屏版本把页面分成两部分:左侧是工序顺序列表,负责选择工序与观察整体进度;右侧是当前工序详情、双轨耗时卡与主操作区。960vp的内容区采用左右布局,420vp时则上下排列。阈值以容器可用宽度720vp划分,原因是两栏同时需要保证计时数字、完整工序名称以及按钮触控范围,而不是简单把整块手机UI放大。
主从布局的难点并不是Row和Column本身,而是两个区域会同时显示“当前工序”。如果左侧选中定影,右侧仍保留上一次的停止详情,用户容易把启动动作落到错误的对象上。这里约定页面父级持有selectedStepId,左右视图都从这一字段读取当前选择。完成确认仍需通过既有版本检查和顺序校验;点选工序仅更换详情焦点,绝不会改变流程状态。
需要分清“选中”与“就绪”:左栏可以选中任意工序查看历史和计划,但只有状态为ready的定影可以启动。显影和停止处于done,水洗和干燥处于waiting。宽屏上的详情面板不能因为某条记录被选中,就自动把它升级为可执行状态。此规则与之前的幂等令牌、修订号检查配合,避免宽屏增加快捷按钮后造成流程跳步。
让投影函数负责表格与详情
最稳妥的方案是保留上一节使用的TimingLine和summarize数据契约,然后在其上构造仅供展示的工作台投影。当前记录从固定的五步列表推导,不靠控件排列顺序反查。如果业务列表后来增加筛选条件,计划、实际与当前状态仍能按照稳定的id取回。
export interface FlowLine {
id: string;
name: string;
plannedSeconds: number;
actualSeconds: number | null;
phase: 'done' | 'ready' | 'waiting';
}
export const FLOW_LINES: FlowLine[] = [
{ id: 'develop', name: '显影', plannedSeconds: 480,
actualSeconds: 520, phase: 'done' },
{ id: 'stop', name: '停止', plannedSeconds: 30,
actualSeconds: 45, phase: 'done' },
{ id: 'fix', name: '定影', plannedSeconds: 300,
actualSeconds: null, phase: 'ready' },
{ id: 'wash', name: '水洗', plannedSeconds: 600,
actualSeconds: null, phase: 'waiting' },
{ id: 'dry', name: '干燥', plannedSeconds: 1200,
actualSeconds: null, phase: 'waiting' }
];
export function formatDuration(seconds: number): string {
const m: number = Math.floor(seconds / 60);
const s: number = seconds % 60;
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
计划秒数保持整数,是为了避免在“08:40”之类的显示字符串上直接做运算。实际秒数为空意味着这道工序尚未完成结算,不能按0秒直接累计。五个稳定的id也同样用于选中状态、按钮权限和事件记录:即使以后将标签改成更贴合工作习惯的名称,业务关联也不需要依赖本地化文字。
每一次更新账本都应该来自一个明确的业务事件。比如定影完成确认通过后,系统才会生成实际耗时并推进下一步;单纯扩大工作区、收起右侧面板或切换屏幕方向没有资格改写账本。把UI投影与业务事件切开,可以让跨布局切换保持可预期性,也更方便排查错误来源。
用同一份账本生成总览指标
工作台的摘要包含完整计划、已完成计划、已完成实际、剩余计划、累计偏差和预测总时长。已完成部分只按phase=done且存在合法实际秒数的条目求和。当前样本中510秒计划对565秒实际,偏差55秒;后面三道工序剩余2100秒计划,预测总时长2665秒。左侧列表只负责展示每条记录,右侧汇总函数负责这些全局数字,避免在多处写一套独立计算公式。
export interface FlowSummary {
completed: number;
totalPlan: number;
donePlan: number;
doneActual: number;
remainingPlan: number;
delta: number;
forecast: number;
}
export function calculateSummary(lines: FlowLine[]): FlowSummary {
let totalPlan: number = 0;
let donePlan: number = 0;
let doneActual: number = 0;
let completed: number = 0;
for (const line of lines) {
totalPlan += line.plannedSeconds;
if (line.phase === 'done' && line.actualSeconds !== null) {
completed += 1;
donePlan += line.plannedSeconds;
doneActual += line.actualSeconds;
}
}
const remainingPlan: number = totalPlan - donePlan;
return {
completed, totalPlan, donePlan, doneActual, remainingPlan,
delta: doneActual - donePlan,
forecast: doneActual + remainingPlan
};
}
export function canStart(lines: FlowLine[], targetId: string): boolean {
const target: FlowLine | undefined = lines.find(
(line: FlowLine) => line.id === targetId
);
return target !== undefined && target.phase === 'ready';
}
这段纯逻辑还让测试样本更容易覆盖异常:遇到完成状态和实际字段不一致,应先进入数据修复路径,而非在界面里勉强显示结果;实际秒数不得为负,计划秒数应大于零;同一工序id出现两次也应该被识别出来。合法性检查可以放到输入层,不让脏数据穿透到展示层后再由各个Text组件分别兜底。
视觉上也需要区分“完成耗时”和“预测耗时”。09:25是已确认的两步实际累计;44:25是在其余三步继续采用计划值的前提下推导出来的预测值。UI将二者排列在不同的指标卡片,避免颜色或大小暗示它们是同一种统计口径。最终五步全部完成时,预测才可以改为完整实际总结。
自适应布局与选择状态分离
HarmonyOS 7对应的API 26提供ContainerReader,适合按当前内容容器宽度切换主从布局。这里在父组件存放尺寸、选中工序以及账本。左右构建函数读取同一@State selectedStepId,使用onClick只更改这一焦点。因为工序详情与权限都通过稳定id派生,所以即使分栏改变导致子组件树重新创建,也不会把选中定影变成选中停止。
import { ContainerReader, Size } from '@kit.ArkUI';
@Entry
@Component
struct WorkbenchPage {
@State private contentSize: Size = { width: 0, height: 0 };
@State private selectedStepId: string = 'fix';
@State private revision: number = 9;
@State private currentPhase: 'ready' | 'running' = 'ready';
@State private acceptedTokens: string[] = [];
private startSelected(): void {
const token: string = 'start-fix-01';
if (this.selectedStepId !== 'fix' || this.currentPhase !== 'ready' ||
this.revision !== 9 || this.acceptedTokens.includes(token)) {
return;
}
this.acceptedTokens = [...this.acceptedTokens, token];
this.currentPhase = 'running';
this.revision = 10;
}
@Builder
private stepsPane() {
Column({ space: 10 }) {
ForEach(FLOW_LINES, (line: FlowLine) => {
Row() {
Text(`${line.name} ${formatDuration(line.plannedSeconds)}`)
Text(line.phase === 'done' ? '已完成' :
line.phase === 'ready' ? '就绪' : '等待')
}
.padding(12).width('100%')
.backgroundColor(this.selectedStepId === line.id ? '#D9EDFF' : '#FFFFFF')
.onClick(() => { this.selectedStepId = line.id; })
}, (line: FlowLine) => line.id)
}
}
@Builder
private detailPane() {
Column({ space: 12 }) {
Text(`当前选择 ${this.selectedStepId}`)
Text(`修订号 ${this.revision}`)
Text('已完成2/5 · 实际09:25 · 偏差+00:55')
Button(this.currentPhase === 'ready' ? '开始定影' : '定影计时中')
.enabled(this.selectedStepId === 'fix' && this.currentPhase === 'ready' &&
canStart(FLOW_LINES, 'fix'))
.onClick(() => this.startSelected())
}.padding(16).backgroundColor('#EFF6FF')
}
build() {
ContainerReader({ size: this.contentSize!! }) {
if (this.contentSize.width >= 720) {
Row({ space: 16 }) {
Column() { this.stepsPane() }.layoutWeight(2)
Column() { this.detailPane() }.layoutWeight(3)
}
} else {
Column({ space: 16 }) {
this.stepsPane()
this.detailPane()
}
}
}
.breakpointConfig({ width: [720] })
.width('100%').height('100%')
}
}
按钮的交互包含当前工序、修订号和令牌的检查;接受一次启动后,将页面级阶段变为运行中并推进修订号,重复点击因阶段变化而失去操作入口。这里的页面状态转移还应与既有流程控制服务的确认入口保持一致,持久层按相同令牌和预期修订号处理,避免宽屏新增快捷操作后绕开第三节建立的重复保护。正在计时的定影仍属于尚未完成的工序,不应因为启动动作立刻写入实际耗时。

从420vp到960vp的状态校对
选择定影后,在420vp内容区先展示工序列表,再展示定影的详情和“开始定影”按钮。将容器扩展至960vp,页面变为左列表右详情,定影仍处于选中状态,修订号仍为9,摘要卡仍应写着已完成2/5、09:25和+00:55。再次缩回420vp,原来的统计口径不能重新计算出别的结果,更不能因为尺寸变化触发“开始”事件。
如果操作员在右栏停留时查看已完成的停止工序,左侧可以高亮停止,但右侧启动按钮要随之禁用,因为停止已是done。如果用户再选择定影,按钮重新可用。这说明“当前选中”与“当前可执行”完全是两类状态。可以为键盘和触控提供同样的选择路径,语义与顺序保持一致,避免同一工序在不同设备上行为分叉。
此外,分栏宽度如果被其它页面元素占用,内容区可能比窗口宽度小得多。容器断点针对实际可用空间做判断,配合滚动或自适应文本换行,比强行设置固定960vp窗口尺寸更可靠。对于主面板较窄的情况,时间汇总卡可以转为纵向排列,但数字仍然从summarize的同一份结果读取。

让主从结构维持清楚的职责
这一节的工程变化集中在展示层:左侧“查找与选择”,右侧“读取详情与提交入口”,父级“维护焦点”,流程服务“确认合法事件”。从一列变成两列没有产生第二套工序账本,也没有让倒计时与汇总成为两个互相竞争的数据来源。这样一来,维护人员可以针对实际耗时计算单独核对,也可以针对布局在阈值附近的行为单独处理。
下一步加入历史记录恢复时,依旧能使用当前的稳定工序id、修订号以及原有的完成确认语义。用户从手机查看、再打开平板工作台,不应看到不同的总耗时。布局适配的价值,就体现在屏幕形态改变时,工程数据和操作约束始终保持同一套定义。
相关技术参考:华为开发者:ContainerReader容器断点、华为开发者:ArkUI布局概述、华为开发者:@Watch状态通知。
更多推荐



所有评论(0)