HarmonyOS DataPanel 容量对照:环形、线形与统一分母【共创稿事节】

一、分母先定下来,图形才有可比性
容量仪表容易出现一种看似精致、含义却错误的结果:仓库只装了六十箱,图形已经占满一圈。原因往往不是绘图组件失灵,而是开发者把分类合计当成了总容量。三类库存加起来六十箱,只能说明已经用了多少;仓库能够容纳一百箱,才是表达占用程度时需要的分母。
本文固定一个业务样例:原料三十箱、半成品二十箱、成品十箱,总容量一百箱。已用六十箱,空余四十箱。所有数字都来自代码中的样例数组,既不是设备测试结果,也不是系统性能指标。我们用它比较同一组件的环形和线形表达,重点是比例语义与输入边界。
两张图都使用同一个总容量,三个色段分别表示总容量的三成、两成和一成,余下四成保留轨道底色。环形中心显示的六成,是已用合计除以总容量;它不是某个分类在已用库存中的占比。只要分母固定,这几种表示就能够互相核对。
接口也支持根据数据合计决定比例的情况,但那适合表达组成关系。本文需要回答仓库还剩多少位置,因此显式传入正的总容量。一个参数的默认行为是否合理,必须结合它回答的业务问题来判断,不能因为默认图形看起来饱满,就省略必要的分母。
二、两种布局使用同一组输入进行比较
环形适合在中心放一个需要快速识别的总体数字。读者先看到占用百分比,再通过周围颜色了解三个分类。它占用一个接近正方形的区域,单独作为容量摘要时比较清楚。但如果要同时对照多个仓库,每个仓库都画一圈会消耗较多垂直空间,跨行比较也需要反复移动视线。
线形占用较少高度,分类顺序从左到右展开,剩余轨道延伸到固定终点。多条线以相同长度对齐时,能够直接比较容量占用;但如果每条线各自换一个分母,长度相同又可能隐藏总容量差异。因此即使使用线形,也应保留总量文字,不能只靠颜色条让用户推测绝对数量。
本例故意把两种图同时放在一张白色卡片中,用相同颜色、分类顺序和数据源建立对照。它是一个技术比较页面,并不意味着最终产品一定要同时保留两张重复表达的图。完成判断后,可以根据阅读任务选择一种,同时保留必要的分类数值与空余量。
颜色只是辅助编码。原料、半成品和成品的名称与数值始终可见,空余量也有独立文字。这样用户不需要记住某一抹蓝色究竟代表什么,无法分辨相近颜色时仍然能读取信息。后续修改主题配色,分类数组与颜色数组的对应顺序必须一起维护。
从性能角度,不能因为线形看起来简单就宣布它一定更快,也不能把关闭效果的设置当成已经完成帧率优化。本文没有给出设备性能数字。真正需要性能比较时,应固定设备、数据规模、刷新频率和观察指标,再记录结果;当前比较的是空间占用与信息表达。

三、在进入组件前拒绝不成立的容量
创建 CapacityRing Stage 模型 Empty Ability 工程,使用 HarmonyOS API 14 SDK,替换默认 pages/Index.ets 即可。模板入口继续加载该页面,不需要额外导入模块或申请权限。代码中的校验函数先处理业务合法性,页面只有拿到完整快照后才更新仪表。
总容量必须是正的安全整数,并限定在本例合理的数量范围内;三个分类必须都是非负安全整数;分类合计不得超过总容量。这里不依赖组件对负值或特殊最大值的默认处理,因为组件接受某个输入并不意味着这个输入符合仓储业务。错误应在展示前得到解释,而不是悄悄修成另一组数。
class CapacitySnapshot {
constructor(public values: number[], public total: number,
public used: number, public free: number) {}
}
function checkCapacity(values: number[], total: number): CapacitySnapshot {
if (!Number.isSafeInteger(total) || total <= 0 || total > 1000000) {
throw new Error('总容量必须是有效的正整数');
}
if (values.length !== 3 ||
values.some((v: number) => !Number.isSafeInteger(v) || v < 0)) {
throw new Error('三个分类都必须是非负整数');
}
const used = values.reduce((sum: number, v: number) => sum + v, 0);
if (!Number.isSafeInteger(used) || used > total) {
throw new Error('分类合计不能超过总容量');
}
return new CapacitySnapshot(values.slice(), total, used, total - used);
}
@Entry
@Component
struct Index {
private colors: string[] = ['#3478B8', '#D98B36', '#36866D'];
@State private values: number[] = [30, 20, 10];
@State private total: number = 100;
@State private used: number = 60;
@State private free: number = 40;
@State private valid: boolean = true;
@State private status: string = '常规样例校验通过';
private choose(values: number[], label: string): void {
try {
const result = checkCapacity(values, 100);
this.values = result.values;
this.total = result.total;
this.used = result.used;
this.free = result.free;
this.valid = true;
this.status = `${label}样例校验通过`;
} catch (e) {
this.valid = false;
this.status = (e as Error).message;
}
}
build() {
Scroll() {
Column({ space: 16 }) {
Text('容量对照').fontSize(28).fontWeight(FontWeight.Bold)
Text('同一份数据,两种图形').fontSize(16).fontColor('#66788C')
if (this.valid) {
Column({ space: 20 }) {
Stack() {
DataPanel({ values: this.values, max: this.total,
type: DataPanelType.Circle })
.width(216).height(216).strokeWidth(22)
.closeEffect(true).valueColors(this.colors)
.trackBackgroundColor('#E1E8EE')
Column({ space: 6 }) {
Text(`${Math.round(this.used / this.total * 100)}%`)
.fontSize(40).fontColor('#245D89')
Text(`已用 ${this.used} 箱`).fontSize(16)
}
}
DataPanel({ values: this.values, max: this.total,
type: DataPanelType.Line })
.width('100%').height(20).closeEffect(true)
.valueColors(this.colors).trackBackgroundColor('#E1E8EE')
Row() {
Text(`总容量 ${this.total} 箱`).fontSize(16)
Blank()
Text(`空余 ${this.free} 箱`).fontSize(16)
}.width('100%')
Column({ space: 10 }) {
Text(`原料 ${this.values[0]} 箱`).fontSize(18).fontColor(this.colors[0])
Text(`半成品 ${this.values[1]} 箱`).fontSize(18).fontColor(this.colors[1])
Text(`成品 ${this.values[2]} 箱`).fontSize(18).fontColor(this.colors[2])
}
}.padding(20).width('100%').backgroundColor(Color.White).borderRadius(20)
} else {
Text('输入未通过校验').fontSize(22).fontColor('#A54835')
}
Text(this.status).fontSize(16).fontColor('#245D89')
Row({ space: 10 }) {
Button('常规').backgroundColor('#245D89').layoutWeight(1)
.onClick(() => { this.choose([30, 20, 10], '常规'); })
Button('满额').backgroundColor('#245D89').layoutWeight(1)
.onClick(() => { this.choose([50, 30, 20], '满额'); })
Button('超额').backgroundColor('#245D89').layoutWeight(1)
.onClick(() => { this.choose([70, 20, 20], '超额'); })
}.width('100%')
Text('轨道空白表示未占用容量').fontSize(15).fontColor('#66788C')
}.padding(24).width('100%')
}.width('100%').height('100%').backgroundColor('#F2F6FA')
}
}
快照把分类、总量、已用和空余放在一起返回,成功后再同步更新状态。失败时隐藏仪表,显示输入错误,避免继续展示上一组成功数据让用户误以为超额样例已经被接受。保留旧图也可以成为一种产品选择,但必须有明确的过期状态与提示,当前页面采用更直接的拒绝方式。
数值数组在校验通过后复制一份,避免调用方随后修改原数组而影响已经显示的快照。三个分类的顺序固定,颜色的顺序也固定;值为零时仍然保留对应位置,不能随手删除零项却继续沿用原来的颜色索引,否则后面的分类会被错误着色。
两种图形分别创建组件实例,使用各自固定的类型。接口十四的类型属性不支持动态修改,所以如果以后提供环形与线形切换,可以用条件分支创建对应组件,而不是假设改变一个状态变量就一定能让已有实例切换类型。这个限制应在设计交互时提前考虑。
四、三组样例分别检验比例、边界和拒绝路径
点击常规样例,应看到已用六十箱、空余四十箱,中心显示百分之六十。环形约六成轨道被分类色段覆盖,线形也是六成长度。两种图不是按三个分类的合计各自填满,因此空余位置会在它们各自的轨道中保留下来。
点击满额样例,数据变成五十、三十、二十,合计一百。中心应显示百分之百,空余为零,轨道不再保留代表空余的区段。满额是合法边界,并不是错误。把大于等于总量都拒绝,会使一个实际已经装满的仓库无法显示正常状态。
点击超额样例,三个分类为七十、二十、二十,合计一百一十。校验应拒绝它,页面显示分类合计超过总容量,且两张仪表都不再出现。不能把它压缩回百分之百继续绘制,因为那会隐藏十箱超出的事实,也会让三个分类的视觉比例失去原来的意义。

另外可以把一个分类改成零,检查它的名称、颜色关联和总体空余量是否仍然成立。零表示这个分类当前没有库存,不表示该字段缺失;如果数据源根本没有提供某一类,应在接入时判断是否允许按零补齐,而不是让绘图函数悄悄替业务决定。
对于小数库存,应先决定最小计量单位。当前样例以整箱计量,代码明确拒绝小数;若以后按重量计量,可以用统一的最小单位保存整数,或者定义可接受的精度和舍入规则。不要一边用小数计算图形,一边把文字逐项舍入后再重新求和,这会产生两套总量。
中心的整数百分比只是显示格式,分类长度仍使用原始数量和总容量计算。尤其在出现多个较小分类时,逐个四舍五入的百分比之和未必等于整体舍入结果。文字可以说明精度,但不应该为了凑整偷偷修改原始数据。让计算来源保持统一,比让每个标签看起来都是整齐整数更重要。
五、用展示任务决定最后保留哪一种
如果页面只需要一个仓库的醒目摘要,环形中心的大数字有明确作用。如果页面需要同屏比较多个仓库,线形更容易形成共享长度基准。两种布局的优缺点来自阅读任务,而不是组件名称自带的等级;同一套数据校验和快照结构可以继续复用。
对于容量可能变化的业务,分母还需要带上它自己的更新时间。库存数据更新而总容量仍是旧值时,即使每个字段单独合法,组合出来的比例也可能没有业务意义。数据接入层应明确哪些字段来自同一时刻,必要时一起更新,页面不应把不同批次的数字随机拼成一张仪表。
名称与单位也应一起保留。相同的六十这个数字,可能是箱数、重量或体积;如果切换单位只改变标题而没有转换数据,总容量与分类就失去共同量纲。当前代码把所有值统一为箱,使每一个加减和比例都能用同一套单位解释。
空余轨道不能被另一个额外分类重复加入。当前三个值只表示已经使用的容量,空余由总量减去合计得到。若再把空余作为第四个值传入,图形会占满整个总量,这可以用于另一个明确标注的组成图,却不再是本文展示的未占用轨道语义。
分类数量也是组件与业务共同的边界。当前只使用三个分类,图例能够完整展示;如果库存分类持续增加,不应该无限向同一仪表塞入色段。需要先确认组件允许的数量,再决定聚合、筛选或换用更适合明细阅读的形式。颜色越来越多,并不会自动让信息越来越完整。
比较页面还应避免把两幅图的宽度随意改变。线形的长度基准来自它的可用区域,环形的比例基准来自整圈;调整布局以后,数值语义应保持不变。验收时既核对数值,也观察分类与空余的相对关系,才能发现布局修改带来的误导。
最后检查错误路径是否与正常路径同样清楚:合法数据更新全部数值,超额数据得到拒绝,再切回常规样例能够恢复。容量图真正需要守住的,是同一分母、统一单位和可解释的边界。只要这三点没有改变,视觉形式就能服务于阅读,而不会反过来掩盖数据问题。

参考资料:
更多推荐


所有评论(0)