
HarmonyOS 统计页实现活动日志、周期桶与 ArkUI 柱状图统计页经常被写成“假图表”页面上有柱状图但数据和用户操作没有关系。这个项目的统计页没有走图表库而是用服务层活动日志生成周期数据再用 ArkUI 组件绘制轻量柱状图。数据来源activityLog每次关键操作都会写活动记录export type ActivityType create | update | archive | restore | delete | backup | restoreBackup | theme | style | reminder;记录结构如下export interface ActivityRecordModel { id: string; type: ActivityType; title: string; createdAt: string; dayKey: string; weight: number; }服务层写入活动时会控制最大长度this.state.activityLog.unshift(record); if (this.state.activityLog.length 60) { this.state.activityLog this.state.activityLog.slice(0, 60); }这对轻量本地应用很重要。统计页不需要无限日志只需要最近趋势。统计周期day / week / month / year页面状态里保存当前周期State selectedPeriod: string week; State summaryCards: ShowcaseCardModel[] appDataService.getStatisticsSummaryCards(week); State statPoints: StatPointModel[] appDataService.getStatPoints(week);切换 ChipTabs 后刷新ChipTabs({ items: appStatisticsTabs, selectedId: this.selectedPeriod, onChange: (id: string) { this.selectedPeriod id; this.refreshData(); } })这个设计让统计页保持纯展示周期怎么分桶、数据怎么累加都在服务层完成。周期桶先划时间再映射活动服务层通过createTrendBuckets()生成桶private createTrendBuckets(period: StatsPeriod): TrendBucket[] { const now: Date new Date(); const buckets: TrendBucket[] []; if (period week) { for (let index: number 6; index 0; index - 1) { const start: Date new Date(now.getFullYear(), now.getMonth(), now.getDate() - index, 0, 0, 0, 0); const end: Date new Date(start.getTime()); end.setDate(end.getDate() 1); buckets.push({ label: formatMonthDay(start), start: start, end: end }); } return buckets; } }不同周期的桶宽不同day按 3 小时粒度。week最近 7 天。month按约 4 天一组。year按两个月一组。这比简单取最近 7 条记录更真实。空数据时不要画假柱子柱状图高度由服务层计算function createChartHeight(value: number, maxValue: number): number { if (maxValue 0) { return 0; } return Math.max(8, Math.round((value / maxValue) * 128)); }这里有一个关键判断maxValue 0时直接返回 0。否则空数据也会出现一排最低柱看起来像有数据。页面绘制柱状图统计页用 ArkUI 原生组件绘制Row() { ForEach(this.statPoints, (item: StatPointModel) { Column({ space: 8 }) { Text(${item.value}) Column() .width(AppSizes.chartBarWidth) .height(item.height) .backgroundColor($r(app.color.brand_primary)) .borderRadius(AppSizes.pillRadius) Text(item.label) } .layoutWeight(1) .justifyContent(FlexAlign.End) .alignItems(HorizontalAlign.Center) }, (item: StatPointModel) item.id) }这不是复杂图表但对“操作趋势”已经够用。好处是不依赖第三方图表库。样式完全跟随项目主题。小屏上更容易控制文字和柱子不溢出。顶部摘要卡从当前数据计算统计页 banner 也不是静态文案private bannerCard(): ShowcaseCardModel { const totalValue: number this.statPoints.reduce((sum, item) sum item.value, 0); const lastPoint this.statPoints.length 0 ? this.statPoints[this.statPoints.length - 1] : undefined; return { id: stat-summary, title: 日常使用, subtitle: this.periodSubtitle(), value: ${totalValue}, footer: lastPoint ? 最新区间${lastPoint.label} : 暂无趋势数据, badge: 统计, tone: brand }; }切换周期后摘要卡、柱状图、核心指标一起刷新。统计页和服务层的边界页面负责保存当前选择周期。渲染 ChipTabs、摘要卡、趋势图、核心指标。在切换周期时调用refreshData()。服务层负责记录活动日志。按周期生成时间桶。把活动映射到桶。计算柱状图高度。输出 summary cards。这种边界可以避免页面里塞大量日期计算。验证建议统计页重点测新建卡片后统计页活动数增加。归档、恢复、备份后对应活动被记录。day / week / month / year 切换后标题和柱状图同步变化。空数据时柱状图不显示假柱子。最近区间 footer 和最后一个桶 label 一致。小屏下柱状图数字和 label 不挤压。基础链路小结这个项目的统计页没有追求复杂图表而是把“活动日志 - 时间桶 - 柱状图”这条链路做清楚。对大多数工具类应用来说这种轻量实现更容易维护也更适合 ArkUI 页面。统计页最重要的是数据真实、空状态诚实、周期切换清晰。做到这三点比单纯堆一个华丽图表更有价值。统计页的价值是“轻量可解释”不是堆图表统计页如果照搬 Web 后台的图表思路移动端和快应用兼容都会吃亏。Project028 的StatisticsPage.ets选择轻量趋势、核心指标和活动记录是比较稳的路线数据来自本地服务图形用 ArkUI 基础布局表达不引入重图表库。统计页使用PageHeader、ChipTabs、ShowcaseCard和本地趋势点把页面拆成“周期筛选、摘要、趋势、指标”四块。重点不是图形多复杂而是每一块都能解释当前应用状态卡片数量、收藏数量、备份/恢复次数、最近活动。ChipTabs({ items: appStatisticsTabs, selectedId: this.selectedPeriod, onChange: (id: string) { this.selectedPeriod id; this.refreshData(); } }) ShowcaseCard({ item: this.bannerCard() })移动端统计最容易出问题的是文本和图形挤压。Project028 的统计点、标签和值大量使用maxLines(1)和textOverflow({ overflow: TextOverflow.Ellipsis })同时用layoutWeight(1)控制横向分配。这些细节看似 UI 层实际决定了项目拆解质量真正的项目教程要说明为什么不直接放大字号、不直接硬编码宽度。Text(this.latestStatPointLabel()) .fontSize(AppSizes.smallTextSize) .fontColor($r(app.color.app_text_secondary)) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis })统计页的关键判断是统计页的数据不必一开始就追求“实时大盘”但必须可追溯。每一次创建、更新、删除、备份、恢复都应进入活动记录趋势页只是这些活动的可视化结果。这样后续接云同步或远端统计时仍有稳定的本地语义。落地检查清单是否说明统计数据来自服务层而不是页面临时拼接。是否解释轻量图形为什么更适合移动端。是否覆盖空数据时的默认展示。是否写到文本截断和布局稳定性。是否覆盖真实路径StatisticsPage.ets、AppDataService.ets、ShowcaseCard.ets。