184.[HarmonyOS NEXT 实战案例十:Grid] 仪表板网格布局基础篇

2025-06-30 23:02:41
104次阅读
0个评论

[HarmonyOS NEXT 实战案例十:Grid] 仪表板网格布局基础篇

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

image.png

image.png

1. 引言

在企业级应用开发中,仪表板(Dashboard)是展示关键业务数据和指标的重要界面,它能够帮助用户快速了解业务状况并做出决策。HarmonyOS NEXT 提供的 Grid 组件具有强大的布局能力,非常适合构建复杂的仪表板界面。本教程将详细讲解如何使用 Grid 组件实现一个功能完善、布局合理的企业级仪表板界面。

2. 仪表板概述

2.1 仪表板的特点

企业级仪表板通常具有以下特点:

  1. 数据可视化:通过图表、数字卡片等方式直观展示数据
  2. 信息层次分明:将信息按重要性和关联性进行分组
  3. 交互性强:支持筛选、刷新、查看详情等操作
  4. 响应式布局:适应不同屏幕尺寸和方向
  5. 实时更新:能够定期或实时更新数据

2.2 实现功能

本教程将实现的仪表板功能包括:

  1. 数据概览:展示关键业务指标(用户数、收入、订单数、转化率)
  2. 数据图表:销售趋势图、用户分布图
  3. 数据列表:热销商品排行、最近活动记录
  4. 告警系统:系统告警通知和查看
  5. 交互功能:时间范围选择、数据刷新、告警查看

3. 数据模型设计

3.1 数据结构定义

仪表板的数据模型设计是实现的基础。我们定义了以下数据结构:

// 仪表板数据结构
export interface DashboardData {
    overview: Overview;         // 概览数据
    charts: Charts;            // 图表数据
    alerts: Alert[];           // 告警数据
}

// 概览数据
export interface Overview {
    totalUsers: number;        // 总用户数
    totalRevenue: number;      // 总收入
    totalOrders: number;       // 总订单数
    conversionRate: number;    // 转化率
    userGrowth: number;        // 用户增长率
    revenueGrowth: number;     // 收入增长率
    orderGrowth: number;       // 订单增长率
    conversionGrowth: number;  // 转化率增长
}

// 图表数据
export interface Charts {
    salesTrend: SalesTrend[];           // 销售趋势
    userDistribution: UserDistribution[]; // 用户分布
    topProducts: TopProduct[];          // 热销商品
    recentActivities: RecentActivity[]; // 最近活动
}

// 销售趋势数据
export interface SalesTrend {
    month: string;  // 月份
    value: number;  // 销售额
}

// 用户分布数据
export interface UserDistribution {
    category: string;  // 用户类别
    value: number;     // 占比值
    color: string;     // 显示颜色
}

// 热销商品数据
export interface TopProduct {
    name: string;    // 商品名称
    sales: number;   // 销量
    revenue: number; // 收入
}

// 最近活动数据
export interface RecentActivity {
    time: string;   // 时间
    user: string;   // 用户
    action: string; // 行为
    type: 'purchase' | 'register' | 'cart' | 'view' | 'payment'; // 类型
}

// 告警数据
export interface Alert {
    id: number;     // ID
    type: 'warning' | 'info' | 'error' | 'success'; // 类型
    message: string; // 消息
    time: string;    // 时间
    isRead: boolean; // 是否已读
}

3.2 状态管理设计

在组件中,我们使用 @State 装饰器来管理状态:

@Component
export struct DashboardGrid {
    // 仪表板数据
    @State dashboardData: DashboardData = { /* 初始数据 */ }
    
    // 其他状态
    @State selectedTimeRange: string = '今日'
    @State refreshing: boolean = false
    @State showAlerts: boolean = false
    
    // 时间范围选项
    private timeRanges: string[] = ['今日', '本周', '本月', '本季度', '本年']
    
    // 组件方法和构建函数...
}

4. 辅助功能实现

4.1 数据格式化

为了更好地展示数据,我们实现了几个数据格式化方法:

// 格式化数字
formatNumber(num: number): string {
    if (num >= 100000000) {
        return `${(num / 100000000).toFixed(1)}亿`
    } else if (num >= 10000) {
        return `${(num / 10000).toFixed(1)}万`
    } else if (num >= 1000) {
        return `${(num / 1000).toFixed(1)}k`
    }
    return num.toString()
}

// 格式化百分比
formatPercentage(num: number): string {
    const sign = num >= 0 ? '+' : ''
    return `${sign}${num.toFixed(1)}%`
}

4.2 样式辅助方法

我们还实现了一些辅助方法来动态设置样式:

// 获取增长趋势颜色
getGrowthColor(growth: number): string {
    return growth >= 0 ? '#34C759' : '#FF3B30'
}

// 获取告警图标
getAlertIcon(type: string): Resource {
    switch (type) {
        case 'error':
            return $r('app.media.01')
        case 'warning':
            return $r('app.media.02')
        case 'success':
            return $r('app.media.03')
        default:
            return $r('app.media.04')
    }
}

// 获取告警颜色
getAlertColor(type: string): string {
    switch (type) {
        case 'error':
            return '#FF3B30'
        case 'warning':
            return '#FF9500'
        case 'success':
            return '#34C759'
        default:
            return '#007AFF'
    }
}

4.3 数据刷新

实现数据刷新功能:

// 刷新数据
refreshData() {
    this.refreshing = true
    // 模拟数据刷新
    setTimeout(() => {
        this.refreshing = false
        console.log('数据刷新完成')
    }, 2000)
}

5. 页面结构设计

5.1 整体布局

仪表板的整体布局采用 Stack 和 Column 组合:

build() {
    Stack() {
        Column() {
            // 顶部标题栏
            // ...
            
            // 仪表板网格
            // ...
        }
        .width('100%')
        .height('100%')

        // 告警对话框
        if (this.showAlerts) {
            // ...
        }
    }
    .width('100%')
    .height('100%')
}

5.2 顶部标题栏

顶部标题栏包含标题、时间范围选择、告警按钮和刷新按钮:

// 顶部标题栏
Row() {
    Column() {
        Text('数据仪表板')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')

        Text('实时监控业务数据')
            .fontSize(14)
            .fontColor('#666666')
            .margin({ top: 4 })
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)

    Row() {
        // 时间范围选择
        Button(this.selectedTimeRange)
            .fontSize(14)
            .fontColor('#007AFF')
            .backgroundColor('rgba(0, 122, 255, 0.1)')
            .borderRadius(16)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .margin({ right: 8 })

        // 告警按钮
        Button() {
            Stack({ alignContent: Alignment.TopEnd }) {
                Image($r('app.media.big15'))
                    .width(24)
                    .height(24)
                    .fillColor('#333333')

                if (this.dashboardData.alerts.filter(alert => !alert.isRead).length > 0) {
                    Circle({ width: 8, height: 8 })
                        .fill('#FF3B30')
                        .margin({ top: -2, right: -2 })
                }
            }
        }
        .width(40)
        .height(40)
        .borderRadius(20)
        .backgroundColor('#F0F0F0')
        .margin({ right: 8 })
        .onClick(() => {
            this.showAlerts = true
        })

        // 刷新按钮
        Button() {
            Image($r('app.media.big20'))
                .width(24)
                .height(24)
                .fillColor('#333333')
                .rotate({ angle: this.refreshing ? 360 : 0 })
                .animation({
                    duration: 1000,
                    iterations: this.refreshing ? -1 : 1,
                    curve: Curve.Linear
                })
        }
        .width(40)
        .height(40)
        .borderRadius(20)
        .backgroundColor('#F0F0F0')
        .onClick(() => {
            this.refreshData()
        })
    }
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 16 })
.backgroundColor('#FFFFFF')

6. 仪表板网格实现

6.1 Grid 组件配置

仪表板的核心是使用 Grid 组件来布局各个数据卡片:

// 仪表板网格
Grid() {
    // 概览数据卡片
    // ...
    
    // 销售趋势图
    // ...
    
    // 用户分布图
    // ...
    
    // 热销商品
    // ...
    
    // 最近活动
    // ...
}
.columnsTemplate('1fr 1fr')  // 两列等宽布局
.rowsGap(12)                 // 行间距
.columnsGap(12)              // 列间距
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, bottom: 16 })
.backgroundColor('#F8F8F8')

6.2 GridItem 配置

使用 GridItem 组件来定位每个卡片的位置:

// 概览数据卡片
GridItem() {
    this.OverviewCard('总用户数', this.dashboardData.overview.totalUsers,
        this.dashboardData.overview.userGrowth, $r('app.media.big19'))
}
.columnStart(0)  // 从第0列开始
.columnEnd(0)     // 到第0列结束(占据第1列)

GridItem() {
    this.OverviewCard('总收入', this.dashboardData.overview.totalRevenue,
        this.dashboardData.overview.revenueGrowth, $r('app.media.big11'))
}
.columnStart(1)  // 从第1列开始
.columnEnd(1)     // 到第1列结束(占据第2列)

// 跨列的图表
GridItem() {
    this.ChartCard('销售趋势', () => {
        this.SalesTrendChart()
    })
}
.columnStart(0)  // 从第0列开始
.columnEnd(1)     // 到第1列结束(占据2列)

6.3 卡片组件实现

6.3.1 概览卡片

// 概览卡片组件
@Builder
OverviewCard(title: string, value: number, growth: number, icon: Resource, isPercentage: boolean = false) {
    Column() {
        Row() {
            Image(icon)
                .width(24)
                .height(24)
                .fillColor('#007AFF')

            Blank()

            Column() {
                Text(this.formatPercentage(growth))
                    .fontSize(12)
                    .fontColor(this.getGrowthColor(growth))
                    .textAlign(TextAlign.End)

                Image(growth >= 0 ? $r('app.media.big1') : $r('app.media.big2'))
                    .width(12)
                    .height(12)
                    .fillColor(this.getGrowthColor(growth))
            }
            .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .margin({ bottom: 12 })

        Text(isPercentage ? `${value}%` : this.formatNumber(value))
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
            .width('100%')
            .textAlign(TextAlign.Start)
            .margin({ bottom: 4 })

        Text(title)
            .fontSize(14)
            .fontColor('#666666')
            .width('100%')
            .textAlign(TextAlign.Start)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({
        radius: 6,
        color: 'rgba(0, 0, 0, 0.1)',
        offsetX: 0,
        offsetY: 2
    })
}

6.3.2 图表卡片

// 图表卡片组件
@Builder
ChartCard(title: string, content: () => void) {
    Column() {
        Text(title)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
            .width('100%')
            .textAlign(TextAlign.Start)
            .margin({ bottom: 16 })

        content()
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({
        radius: 6,
        color: 'rgba(0, 0, 0, 0.1)',
        offsetX: 0,
        offsetY: 2
    })
}

7. 图表与数据可视化

7.1 销售趋势图

使用简化的柱状图展示销售趋势:

// 销售趋势图
@Builder
SalesTrendChart() {
    Column() {
        // 简化的柱状图
        Row() {
            ForEach(this.dashboardData.charts.salesTrend, (item:SalesTrend, index) => {
                Column() {
                    // 柱子
                    Column()
                        .width(20)
                        .height((item.value / 350000) * 100)
                        .backgroundColor('#007AFF')
                        .borderRadius({ topLeft: 4, topRight: 4 })
                        .margin({ bottom: 8 })

                    // 月份标签
                    Text(item.month)
                        .fontSize(10)
                        .fontColor('#666666')
                        .textAlign(TextAlign.Center)

                    // 数值
                    Text(this.formatNumber(item.value))
                        .fontSize(8)
                        .fontColor('#999999')
                        .textAlign(TextAlign.Center)
                        .margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.Center)
                .layoutWeight(1)
            })
        }
        .width('100%')
        .height(120)
        .alignItems(VerticalAlign.Bottom)
    }
}

7.2 用户分布图

使用简化的饼图展示用户分布:

// 用户分布饼图
@Builder
UserDistributionChart() {
    Row() {
        // 简化的饼图(使用圆形进度条模拟)
        Stack() {
            Circle({ width: 80, height: 80 })
                .fill('#F0F0F0')

            Circle({ width: 60, height: 60 })
                .fill('#FFFFFF')

            Text('用户\n分布')
                .fontSize(10)
                .fontColor('#666666')
                .textAlign(TextAlign.Center)
        }
        .margin({ right: 16 })

        // 图例
        Column() {
            ForEach(this.dashboardData.charts.userDistribution, (item:UserDistribution) => {
                Row() {
                    Circle({ width: 8, height: 8 })
                        .fill(item.color)
                        .margin({ right: 8 })

                    Text(item.category)
                        .fontSize(12)
                        .fontColor('#666666')
                        .layoutWeight(1)

                    Text(`${item.value}%`)
                        .fontSize(12)
                        .fontWeight(FontWeight.Bold)
                        .fontColor('#333333')
                }
                .width('100%')
                .margin({ bottom: 8 })
            })
        }
        .layoutWeight(1)
    }
    .width('100%')
}

7.3 数据列表

7.3.1 热销商品列表

// 热销商品列表
@Builder
TopProductsList() {
    Column() {
        ForEach(this.dashboardData.charts.topProducts.slice(0, 3), (product:TopProduct, index) => {
            Row() {
                Text(`${index + 1}`)
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(index === 0 ? '#FFD700' : index === 1 ? '#C0C0C0' : '#CD7F32')
                    .width(20)

                Column() {
                    Text(product.name)
                        .fontSize(12)
                        .fontColor('#333333')
                        .maxLines(1)
                        .textOverflow({ overflow: TextOverflow.Ellipsis })
                        .width('100%')
                        .textAlign(TextAlign.Start)

                    Text(`销量: ${product.sales}`)
                        .fontSize(10)
                        .fontColor('#666666')
                        .margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.Start)
                .margin({ left: 12 })
                .layoutWeight(1)

                Text(this.formatNumber(product.revenue))
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#FF3B30')
            }
            .width('100%')
            .padding({ top: 8, bottom: 8 })
            .border({
                width: { bottom: index < 2 ? 1 : 0 },
                color: '#F0F0F0'
            })
        })
    }
}

7.3.2 最近活动列表

// 最近活动列表
@Builder
RecentActivitiesList() {
    Column() {
        ForEach(this.dashboardData.charts.recentActivities.slice(0, 4), (activity:RecentActivity, index) => {
            Row() {
                Image(this.getActivityIcon(activity.type))
                    .width(16)
                    .height(16)
                    .fillColor('#007AFF')
                    .margin({ right: 8 })

                Column() {
                    Text(`${activity.user} ${activity.action}`)
                        .fontSize(12)
                        .fontColor('#333333')
                        .maxLines(1)
                        .textOverflow({ overflow: TextOverflow.Ellipsis })
                        .width('100%')
                        .textAlign(TextAlign.Start)

                    Text(activity.time)
                        .fontSize(10)
                        .fontColor('#999999')
                        .margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
            }
            .width('100%')
            .padding({ top: 8, bottom: 8 })
            .border({
                width: { bottom: index < 3 ? 1 : 0 },
                color: '#F0F0F0'
            })
        })
    }
}

8. 告警系统实现

8.1 告警对话框

// 告警对话框
@Builder
AlertsDialog() {
    Column() {
        Row() {
            Text('系统告警')
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
                .layoutWeight(1)

            Button() {
                Image($r('app.media.search_icon'))
                    .width(20)
                    .height(20)
                    .fillColor('#666666')
            }
            .width(32)
            .height(32)
            .borderRadius(16)
            .backgroundColor('#F0F0F0')
            .onClick(() => {
                this.showAlerts = false
            })
        }
        .width('100%')
        .margin({ bottom: 16 })

        Column() {
            ForEach(this.dashboardData.alerts, (alert:Alert) => {
                Row() {
                    Image(this.getAlertIcon(alert.type))
                        .width(20)
                        .height(20)
                        .fillColor(this.getAlertColor(alert.type))
                        .margin({ right: 12 })

                    Column() {
                        Text(alert.message)
                            .fontSize(14)
                            .fontColor('#333333')
                            .width('100%')
                            .textAlign(TextAlign.Start)

                        Text(alert.time)
                            .fontSize(12)
                            .fontColor('#999999')
                            .margin({ top: 4 })
                    }
                    .alignItems(HorizontalAlign.Start)
                    .layoutWeight(1)

                    if (!alert.isRead) {
                        Circle({ width: 8, height: 8 })
                            .fill('#FF3B30')
                    }
                }
                .width('100%')
                .padding(12)
                .backgroundColor(alert.isRead ? '#FFFFFF' : '#F8F8F8')
                .borderRadius(8)
                .margin({ bottom: 8 })
                .onClick(() => {
                    alert.isRead = true
                })
            })
        }
        .width('100%')
        .height(300)
    }
    .width('90%')
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
}

8.2 告警显示

在主界面中显示告警对话框:

// 告警对话框
if (this.showAlerts) {
    Column() {
        this.AlertsDialog()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0, 0, 0, 0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
        this.showAlerts = false
    })
}

9. Grid 组件在仪表板中的优势

优势 描述
灵活的布局 Grid 组件支持灵活的行列定义,可以轻松创建复杂的网格布局
精确的位置控制 通过 columnStart、columnEnd 等属性可以精确控制每个卡片的位置
响应式布局 可以使用 fr 单位创建响应式布局,适应不同屏幕尺寸
间距控制 通过 rowsGap 和 columnsGap 可以统一控制网格间距
嵌套能力 可以在 GridItem 中嵌套其他布局组件,构建复杂界面

10. 仪表板卡片设计技巧

技巧 描述
视觉层次 使用阴影、边框和背景色创建视觉层次,突出重要信息
颜色编码 使用颜色编码表示不同状态(如增长为绿色,下降为红色)
数据格式化 对大数据进行格式化(如 1.2万、3.5k),提高可读性
图标使用 使用图标增强视觉效果,提高信息识别度
交互反馈 为按钮和卡片添加点击效果,提供良好的交互体验

11. 总结

本教程详细讲解了如何使用 HarmonyOS NEXT 的 Grid 组件实现企业级仪表板界面。通过合理的数据模型设计、模块化的组件构建和灵活的网格布局,我们打造了一个功能完善、布局合理的仪表板界面。

Grid 组件的强大布局能力使得我们能够轻松实现复杂的仪表板布局,而模块化的设计思想则使得代码更加清晰和可维护。在实际开发中,开发者可以根据具体需求,灵活运用 Grid 组件,创建出更加丰富多样的仪表板界面。

在下一篇教程中,我们将深入探讨仪表板网格布局的进阶技巧,包括动态调整网格结构、高级交互效果和性能优化等内容,敬请期待!

收藏00

登录 后评论。没有帐号? 注册 一个。