请添加图片描述

最近在做一个家具购买记录的App,主要是想把家里买的家具都记录下来,方便查看保修期、购买价格这些信息。今天先从首页仪表盘开始,这个页面算是整个App的门面,需要展示一些关键数据和常用入口。

做这个App的初衷其实很简单,家里陆陆续续买了不少家具,有的是网上买的,有的是实体店买的,时间一长就记不清哪个家具是什么时候买的、保修期到什么时候。之前试过用备忘录记,但是太乱了,所以干脆自己写一个专门的App来管理。

开发环境说明

这个项目是基于 Flutter for OpenHarmony 开发的,可以同时运行在鸿蒙设备和其他平台上。用到的主要依赖包括:

  • flutter_screenutil:屏幕适配,让UI在不同尺寸设备上都能正常显示
  • get:路由管理和状态管理,用起来比较轻量
  • convex_bottom_bar:底部导航栏组件,样式比较好看

这些依赖都是经过鸿蒙适配的版本,可以直接在 OpenHarmony 设备上运行。

整体页面结构

首页仪表盘我用的是 StatelessWidget,因为这个页面主要是展示数据,暂时不需要复杂的状态管理。页面整体采用 SingleChildScrollView 包裹,这样内容多了也能滚动。

class DashboardView extends StatelessWidget {
  const DashboardView({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFFAF8F5),
      appBar: AppBar(
        title: const Text('家具购买记录'),
        backgroundColor: const Color(0xFF8B4513),
        foregroundColor: Colors.white,
        actions: [
          IconButton(icon: const Icon(Icons.search), onPressed: () => Get.toNamed(AppRoutes.search)),
          IconButton(icon: const Icon(Icons.notifications_outlined), onPressed: () => Get.toNamed(AppRoutes.reminderList)),
        ],
      ),

这里背景色用了 0xFFFAF8F5,是一种很淡的米白色,看起来比较温馨,符合家居类App的调性。选这个颜色是因为纯白色看久了眼睛会累,而这种带一点暖色调的白色会舒服很多。

AppBar 的颜色选了 0xFF8B4513,这是一种棕色,和家具的木质感比较搭。其实一开始试过蓝色和绿色,但总觉得和家具主题不太协调,最后还是选了这个棕色系。

右上角放了搜索和通知两个按钮,用 GetX 的路由跳转。搜索功能可以快速找到某个家具,通知按钮点进去是提醒列表,会显示保修即将到期之类的提醒。

页面主体布局

页面主体用 Column 垂直排列各个模块,每个模块之间用 SizedBox 隔开。这里用了 flutter_screenutil 做屏幕适配,.w.h 后缀会根据设计稿自动计算实际尺寸。

      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildStatsCard(),
            SizedBox(height: 20.h),
            _buildQuickActions(),
            SizedBox(height: 20.h),
            _buildRecentPurchases(),
            SizedBox(height: 20.h),
            _buildWarrantyReminders(),
          ],
        ),
      ),

crossAxisAlignment: CrossAxisAlignment.start 让子组件都靠左对齐,这样标题文字不会居中显示。整体 padding 设置为 16,给内容留出呼吸空间,不会显得太拥挤。

为什么用 SingleChildScrollView 而不是 ListView?因为这个页面的内容是固定的几个模块,不是动态列表,用 SingleChildScrollView 更合适。如果内容超出屏幕高度,用户可以滚动查看。

数据统计卡片

首页最显眼的就是顶部的统计卡片,用渐变背景让它更有层次感。这个卡片展示三个核心数据:家具总数、房间数、总花费。

  Widget _buildStatsCard() {
    return Container(
      padding: EdgeInsets.all(20.w),
      decoration: BoxDecoration(
        gradient: const LinearGradient(colors: [Color(0xFF8B4513), Color(0xFFA0522D)]),
        borderRadius: BorderRadius.circular(16.r),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          _buildStatItem('家具总数', '48', Icons.chair),
          _buildStatItem('房间数', '6', Icons.room),
          _buildStatItem('总花费', '¥86,500', Icons.payment),
        ],
      ),
    );
  }

LinearGradient 从深棕色渐变到浅棕色,视觉上更有质感。如果只用单一颜色,看起来会比较平,加上渐变之后立体感就出来了。

borderRadius.r 后缀做圆角适配,16 的圆角大小刚好,不会太圆也不会太方。圆角太大会显得幼稚,太小又没有现代感,16是个比较平衡的数值。

三个统计项用 Row 横向排列,spaceAround 让它们均匀分布。这样不管屏幕多宽,三个数据都能保持合适的间距。

单个统计项组件

每个统计项是一个小的 Column,从上到下依次是图标、数值、标签。把它抽成独立方法,代码更清晰,也方便复用。

  Widget _buildStatItem(String label, String value, IconData icon) {
    return Column(
      children: [
        Icon(icon, color: Colors.white70, size: 26.sp),
        SizedBox(height: 8.h),
        Text(value, style: TextStyle(color: Colors.white, fontSize: 18.sp, fontWeight: FontWeight.bold)),
        Text(label, style: TextStyle(color: Colors.white70, fontSize: 12.sp)),
      ],
    );
  }

图标用 Colors.white70 稍微透明一点,这样不会太抢眼。如果图标和数值都是纯白色,视觉上会有点乱,分不清主次。

数值用白色加粗显示,是整个卡片的视觉焦点。用户一眼看过去,最先注意到的应该是这些数字。

标签字号小一些,颜色也淡一些,形成主次分明的层次。这种处理方式在很多App里都能看到,是比较成熟的设计模式。

快捷操作区域

快捷操作是用户最常用的几个功能入口,我放了添加家具、购买记录、日历、收藏夹四个。用一个 List 存储配置数据,然后 map 遍历生成按钮。

  Widget _buildQuickActions() {
    final actions = [
      {'icon': Icons.add_circle, 'label': '添加家具', 'route': AppRoutes.addFurniture},
      {'icon': Icons.receipt_long, 'label': '购买记录', 'route': AppRoutes.purchaseRecord},
      {'icon': Icons.calendar_month, 'label': '日历', 'route': AppRoutes.calendar},
      {'icon': Icons.favorite, 'label': '收藏夹', 'route': AppRoutes.favorites},
    ];
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('快捷操作', style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold, color: const Color(0xFF5D4037))),
        SizedBox(height: 12.h),

这种数据驱动的写法好处是,以后要加减按钮只需要改 actions 数组就行,不用动 UI 代码。比如以后想加一个"扫码录入"的功能,直接在数组里加一项就行了。

标题用深棕色 0xFF5D4037,和主题色呼应但又有区分。这个颜色比 AppBar 的棕色稍微深一点,用在文字上刚好。

为什么选这四个功能作为快捷入口?添加家具是最常用的操作,购买记录可以查看历史,日历可以看保修到期时间,收藏夹放一些重要的家具。这四个基本覆盖了日常使用场景。

快捷按钮样式

每个快捷按钮是一个圆角方块加底部文字的组合,点击后跳转到对应页面。

        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: actions.map((a) => GestureDetector(
            onTap: () => Get.toNamed(a['route'] as String),
            child: Column(
              children: [
                Container(
                  padding: EdgeInsets.all(14.w),
                  decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12.r), boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: 4)]),
                  child: Icon(a['icon'] as IconData, color: const Color(0xFF8B4513), size: 28.sp),
                ),
                SizedBox(height: 8.h),
                Text(a['label'] as String, style: TextStyle(fontSize: 12.sp, color: const Color(0xFF5D4037))),
              ],
            ),
          )).toList(),
        ),

按钮背景是白色,加了一点淡淡的阴影,让它有浮起来的感觉。阴影的透明度设为 0.1,模糊半径 4,这样阴影很柔和,不会太突兀。

GestureDetector 包裹整个按钮区域,点击范围更大,用户体验更好。如果只在图标上加点击事件,用户可能会点不准。

图标颜色用主题棕色,保持整体风格统一。图标大小 28,不会太大也不会太小,和下面的文字搭配刚好。

最近购买列表

这个模块展示最近购买的家具,方便用户快速查看。标题栏右边有个"查看全部"按钮,点击跳转到完整的购买记录页面。

  Widget _buildRecentPurchases() {
    final records = [
      {'name': '北欧实木沙发', 'room': '客厅', 'date': '2024-01-15', 'price': '¥12,800'},
      {'name': '智能升降书桌', 'room': '书房', 'date': '2024-01-10', 'price': '¥3,200'},
    ];
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text('最近购买', style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold, color: const Color(0xFF5D4037))),
            TextButton(onPressed: () => Get.toNamed(AppRoutes.purchaseRecord), child: const Text('查看全部')),
          ],
        ),

标题和按钮用 Row 包裹,spaceBetween 让它们分别靠左和靠右。这是很常见的列表头部布局方式。

TextButton 自带点击效果,不用额外处理。而且它的默认样式就是蓝色文字,和整体设计也比较搭。

这里的数据是写死的,实际项目中应该从数据库读取。不过现在先把 UI 做出来,数据层后面再接入。

购买记录卡片

每条购买记录是一个白色卡片,左边是图标,中间是名称和信息,右边是价格。

        ...records.map((r) => Container(
          margin: EdgeInsets.only(bottom: 10.h),
          padding: EdgeInsets.all(14.w),
          decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12.r)),
          child: Row(
            children: [
              Container(
                padding: EdgeInsets.all(10.w),
                decoration: BoxDecoration(color: const Color(0xFF8B4513).withOpacity(0.1), borderRadius: BorderRadius.circular(10.r)),
                child: Icon(Icons.chair, color: const Color(0xFF8B4513), size: 24.sp),
              ),
              SizedBox(width: 12.w),
              Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
                Text(r['name']!, style: TextStyle(fontWeight: FontWeight.w600, fontSize: 15.sp)),
                Text('${r['room']} · ${r['date']}', style: TextStyle(color: Colors.grey[600], fontSize: 12.sp)),
              ])),
              Text(r['price']!, style: TextStyle(color: const Color(0xFF8B4513), fontWeight: FontWeight.bold)),
            ],
          ),
        )).toList(),

图标外面套了一个浅棕色背景的容器,和纯白卡片形成对比。这个浅棕色是主题色加 0.1 的透明度,颜色很淡但能起到区分作用。

中间部分用 Expanded 包裹,这样文字太长会自动省略,不会把价格挤出去。这个细节很重要,不然遇到名字很长的家具,布局就会乱掉。

价格用主题色加粗显示,一眼就能看到。毕竟对于购买记录来说,价格是很重要的信息。

卡片之间用 margin: EdgeInsets.only(bottom: 10.h) 隔开,只设置底部间距,这样最后一个卡片下面不会有多余的空白。

保修提醒模块

最后是保修提醒,用橙色系来表示警告状态,提醒用户注意即将到期的保修。

  Widget _buildWarrantyReminders() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('保修提醒', style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold, color: const Color(0xFF5D4037))),
        SizedBox(height: 12.h),
        Container(
          padding: EdgeInsets.all(14.w),
          decoration: BoxDecoration(color: Colors.orange[50], borderRadius: BorderRadius.circular(12.r), border: Border.all(color: Colors.orange[200]!)),
          child: Row(
            children: [
              Icon(Icons.alarm, color: Colors.orange[700], size: 24.sp),
              SizedBox(width: 12.w),
              Expanded(child: Text('智能冰箱保修即将到期', style: TextStyle(fontWeight: FontWeight.w600))),
              Text('15天后', style: TextStyle(color: Colors.orange[800], fontSize: 12.sp)),
            ],
          ),
        ),
      ],
    );
  }

背景用 Colors.orange[50] 很淡的橙色,边框用 Colors.orange[200] 稍深一点,形成柔和的警告效果。这种配色不会太刺眼,但又能引起用户注意。

闹钟图标用 Colors.orange[700],右边的天数用 Colors.orange[800],整体色调统一但又有层次变化。橙色系在这里表示"注意"而不是"危险",如果用红色会显得太严重。

为什么要单独做一个保修提醒模块?因为很多人买了家具之后就忘了保修这回事,等出问题了才发现保修已经过期了。有了这个提醒,用户可以提前知道哪些家具快过保了,该修的赶紧修。

导入依赖说明

文件开头需要导入必要的依赖:

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../app/routes/app_routes.dart';

flutter_screenutil 提供屏幕适配功能,get 提供路由跳转功能,app_routes.dart 是我们自己定义的路由常量文件。

把路由定义成常量的好处是,如果以后要改路由路径,只需要改一个地方就行了,不用到处找。

配色方案总结

整个首页用到的颜色不多,主要是这几个:

  • 主题棕色 0xFF8B4513:AppBar、图标、价格文字
  • 深棕色 0xFF5D4037:标题文字
  • 米白背景 0xFFFAF8F5:页面背景
  • 纯白色:卡片背景
  • 橙色系:保修提醒

颜色不要用太多,不然会显得很乱。选定一个主题色,然后围绕它搭配就行了。

响应式适配要点

flutter_screenutil 做适配时,有几个要注意的地方:

  • 宽度相关的用 .w,比如 padding、margin、width
  • 高度相关的用 .h,比如 SizedBox 的 height
  • 字号用 .sp,会根据系统字体设置缩放
  • 圆角用 .r,保持比例协调

在 main.dart 里要初始化 ScreenUtil,设置设计稿尺寸。我用的是 375x812,这是 iPhone X 的尺寸,也是很多设计师常用的尺寸。

小结

首页仪表盘的实现其实不复杂,主要是把各个模块拆分清楚,每个模块负责自己的展示逻辑。配色上保持统一的棕色系,符合家具App的定位。用 flutter_screenutil 做适配,在不同尺寸的设备上都能正常显示。

代码组织上,把每个模块抽成独立的方法,这样主 build 方法就很清晰,一眼就能看出页面结构。以后要修改某个模块,直接找到对应的方法就行了。

下一篇会讲家具列表页面的实现,涉及到列表渲染和筛选功能,敬请期待。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐