ArkUI + HarmonyOS 衡能看板:数据质量校验与图表组件边界(连载05/06)【共创稿事节】
能耗看板收到七条记录,画面上的七根柱子就一定可信吗?“衡能”第四篇已经可以点选周五,明细显示 6.6 kWh,同时用 requestSeq 避免旧结果回写。可在实际接入数据时,新的难题并不是请求顺序,而是结果本身可能缺一天、重复一天,甚至包含负数或非有限值。这时让柱图直接参与求和,界面可能仍然很漂亮,却会把含义错误的数据包装成可靠结论。
“衡能”沿用 EnergyBar { key, label, kwh }。示例用“近7日”依次为周一 5.2、周二 4.8、周三 6.1、周四 5.4、周五 6.6、周六 5.1、周日 5.4,总量 38.6 kWh;“今天”四个时段为 1.4、1.6、2.2、1.0,合计 6.2 kWh。前三篇把范围切换、720vp 栅格、异步刷新建立起来,第四篇补足 selectedKey。本篇不增加设备采集权限,也不伪造一份“最新电费账单”,而是沿着已经有的数据流补一道校验关口,并把校验、加载状态和绘图表现分到职责清楚的组件里。

一、零值、缺测、错误值不能画成同一根零高度柱
先拿三个具体输入比一比。如果周三确实采到了 0.0 kWh,它是合法记录,即使图表上看起来没有柱高,也必须保留“周三 · 0.0 kWh”的标签和点选入口。如果周三的 d3 根本没回来,那么周三不是零电量,而是“缺测”;直接为缺失日补零,就会改变统计含义。如果返回两条 d5,不能把重复的周五当作两个独立天相加,否则总量会虚增。对于 NaN、Infinity 或负数,也不应该勉强归一化成某个柱高。
这些情况需要在数据进入 UI 之前解决:原始输入 → 结构与业务校验 → 被接受的完整快照 → 图表与明细。图表组件本身不负责猜测缺测是零,也不负责纠正重复键。数据层一旦发现异常,就返回错误状态和可读的原因,保留请求范围,让用户决定重试;页面不能同时显示“38.6 kWh”总量和某个不完整的新柱图。
已经存在的 requestSeq 仍然必要,不过它只能判断“这个结果是不是最后发起的请求”,无法证明结果内容正确。顺序和质量是两个正交的门槛:通过了顺序检查但质量错误,仍应拒绝覆盖展示快照;质量正确但来自旧请求,也不能进页面。反过来说,为了让界面看上去从不空白而直接沿用旧图,必须加“历史数据”标签;本篇继续沿用第三篇“加载即清空”的明确策略,避免用户混淆。
二、为两个示例范围建立最小质量契约
这里的契约并不复杂:近7日有 d1 到 d7 七个唯一键,今天有 h08、h12、h16、h20 四个唯一键;每项有非空标签,电量是大于等于零的有限数字。它适合当前完整固定粒度的示例源。等接入有缺口的真实时序数据,应把缺测作为独立状态表达,并根据设备协议决定是否允许不完整统计,不能生搬本篇的“缺一条就整组拒绝”规则。
// entry/src/main/ets/common/EnergyQuality.ets
export interface EnergyBar {
key: string;
label: string;
kwh: number;
}
export type RangeKey = '今天' | '近7日';
export interface QualityResult {
ok: boolean;
rows: EnergyBar[];
total: number;
reason: string;
}
const KEYS_TODAY: string[] = ['h08', 'h12', 'h16', 'h20'];
const KEYS_WEEK: string[] = ['d1', 'd2', 'd3', 'd4', 'd5', 'd6', 'd7'];
export function inspectEnergy(input: EnergyBar[], range: RangeKey): QualityResult {
const expected: string[] = range === '今天' ? KEYS_TODAY : KEYS_WEEK;
if (!Array.isArray(input) || input.length !== expected.length) {
return { ok: false, rows: [], total: 0, reason: '记录数量与时间范围不符' };
}
const seen: Set<string> = new Set<string>();
for (const bar of input) {
if (!bar || typeof bar.key !== 'string' ||
typeof bar.label !== 'string' || bar.label.length === 0 ||
!expected.includes(bar.key) || seen.has(bar.key) ||
typeof bar.kwh !== 'number' || !Number.isFinite(bar.kwh) || bar.kwh < 0) {
return { ok: false, rows: [], total: 0, reason: '记录键或电量数值异常' };
}
seen.add(bar.key);
}
const ordered: EnergyBar[] = expected.map((key: string): EnergyBar =>
input.find((x: EnergyBar): boolean => x.key === key) as EnergyBar);
const total: number = ordered.reduce(
(sum: number, item: EnergyBar): number => sum + item.kwh, 0);
return { ok: true, rows: ordered, total: total, reason: '' };
}
这里特意按 expected 重排结果,原因是柱子的位置表达时间顺序,不能让网络响应数组的顺序决定周一和周日的位置。但校验没有擅自删除重复项,也没有把缺测填成零。total 只来自通过校验的 ordered,且由这一份数据同时传给顶部总量和图表明细,避免出现多套汇总算法。Number.isFinite() 负责拦截非有限数字;如果数据源本身是以字符串表达电量的 JSON,应该在明确的适配层解析和验证,不能让 parseFloat('6.2abc') 之类的宽松转换直接进入统计。

三、数据质量关口放在请求序号之后、UI 快照之前
将 EnergyQuality.ets 放进 common,保留 EnergyDashboard.ets 原有的 DemoEnergySource、activeRange、requestSeq、phase、displayBars、selectedKey。每次请求完成先检查是否为最新,再执行校验;成功时让柱图、总量和明细同时接受同一份快照。失败时不再传递不完整数组,只更新显式错误态。下面是对上一版 load() 的关键更新位置,原有的按钮、GridRow 容器和错误重试入口保持不变。
// EnergyDashboard.ets 的 load() 增量逻辑
import { EnergyBar, RangeKey, QualityResult,
inspectEnergy } from '../common/EnergyQuality';
@State private acceptedTotal: number = 0;
@State private qualityMessage: string = '';
private async load(fail: boolean): Promise<void> {
const seq: number = ++this.requestSeq;
const range: RangeKey = this.activeRange;
this.phase = 'loading';
this.displayBars = [];
this.selectedKey = '';
this.acceptedTotal = 0;
this.qualityMessage = '';
try {
const raw: EnergyBar[] = await this.source.load(range, fail);
if (seq !== this.requestSeq) { return; }
const result: QualityResult = inspectEnergy(raw, range);
if (!result.ok) { throw new Error(result.reason); }
this.displayBars = result.rows;
this.acceptedTotal = result.total;
this.selectedKey = result.rows[0].key;
this.phase = 'ready';
} catch (err) {
if (seq !== this.requestSeq) { return; }
this.phase = 'error';
this.displayBars = [];
this.selectedKey = '';
this.acceptedTotal = 0;
this.qualityMessage = '数据不完整或读取失败,请重试';
}
}
错误态应隐藏总量卡片或将其明确标为“不可用”。虽然代码把内部的 acceptedTotal 复位为零,但不能把这个内部占位数渲染成“当前耗电 0.0 kWh”;零耗电和加载失败是两种不同业务事实。
实际产品可以进一步区分“设备离线”“数据质量不合格”“服务请求失败”,让提示更准确;上面为了保持原页面改动范围有限,统一使用错误态,并通过 QualityResult.reason 在调试或专门诊断界面展示详细原因。请勿把内部异常堆栈、请求头、用户地址直接显示在最终 UI。还有一个特别重要的顺序:如果 seq 已经过期,连错误提示也不能更新。否则旧请求的失败可能让新范围的成功页面又出现一条红色提示。
把组件拆分后,数据的权威来源仍然只有父组件。EnergyBarsPanel 接收 displayBars、selectedKey 和选中回调;EnergyDetailPanel 只接收从同一数组查询的 selectedBar;顶部统计卡片接收 acceptedTotal。这三者都不私自调用数据源,不把 total 再次累加一遍。720vp 断点的 GridRow 负责排列,不改变数据语义;从手机切到平板,仍然显示七项 38.6 kWh,而不是因为子组件销毁重建再次请求一套不一致的数据。

四、把四组异常样例变成长期回归用例
完整的周数据应该通过校验并生成 38.6 kWh;刻意删除 d3 的数组应在数量检查处失败;复制一条 d5 再删掉 d6,虽然条数仍是七,但必须在重复键检查处失败;将七项全部设为合法的 0.0,则应该通过校验、总量为 0.0 kWh,图表显示“七项零值”而非“无数据”。这四组只是用于逻辑验证的构造数据,不能写成真实住户的用电测量。

还应验证先切“今天”再切“近7日”,让旧请求最后返回:只有新的 requestSeq 能提交质量合格的结果。再用空数组、NaN、Infinity、负数和空标签分别验证错误态。选择项的稳定规则也要保留:仅在被接受的 displayBars 中寻找 selectedKey,校验失败时不继续展示上次的 d5 明细。ForEach 依旧使用 bar.key;不要使用数组下标,因为校验后的规范排序只是布局实现,不代表索引获得了稳定身份。
如果要做可重复的单元检查,可以把 inspectEnergy() 当作纯函数直接喂数组,断言 ok、total 和 reason,完全不依赖页面。比如输入合法的七条记录,断言 ok === true、total === 38.6(浮点求和应采用合适的容差比较);输入缺失 d3 的六条,则断言 ok === false 并检查错误原因。当数据规模扩大、来自真实设备时,测试更应该围绕业务单位、采集时间区间和采集点身份,而不是围绕柱子的像素高度。
对于生产数据,还应当约定服务端和前端“总量”的共同口径:按小时累计读数与按天汇总读数不可随便相加,千瓦 kW 是功率,千瓦时 kWh 才是能量;使用不同的采样区间会造成看似相同却不能直接比较的数字。本篇示例全部以离散时段的 kWh 为单位,才可以进行求和。应用开发者不能用界面格式化去消除上游单位错误,正式接入智能电表时应增加设备 ID、时间区间和采集质量标识,并在数据适配层完成更严格的业务校验。
当这些逻辑用例都通过后,才轮到视觉问题:零值是否仍有可点选的标签、大字体下柱形区是否被挤压、平板宽屏时明细与图表是否保持对齐。这里没有给出任何“优化前后 FPS”数字;质量关口是一段确定性数据处理逻辑,性能好坏需要在实际设备上另外测量。错误态可以显示明确的重试入口,但不应凭空补出一份看似完整的柱图。
这次改造看起来只是多了一个 inspectEnergy(),实质是为界面建立了数据进入展示前的最终边界。请求序号保证结果属于当前操作,质量校验保证结果符合当前数据协议,稳定键保证点选归属于同一份快照。下一篇可在此基础上收束多设备页面、可访问性和部署检查,而不必再去修复“明明七根柱子,却不是七天”的基础错误。
兼容性方面,本文使用的 GridRow / GridCol 与 ForEach 均为 ArkUI 已公开的基础组件,示例以现有 Stage 工程和通用 ArkTS 标准库为基础;不依赖“只有新系统才开放”的能力,也不需要为静态示例数据额外申请设备读取权限。提交真实数据前要核对采集设备能力、单位、网络与隐私约束。参考华为开发者文档:GridRow / GridCol 栅格、ForEach 稳定键规则、ArkUI 状态管理。
更多推荐


所有评论(0)