请添加图片描述

做完家具列表和详情页之后,接下来要实现添加家具的功能。这个页面是整个App的核心功能之一,用户需要在这里录入新购买的家具信息,包括基本信息、购买信息、保修信息和规格信息等。

说实话,表单页面是我觉得最考验耐心的页面类型。字段多、验证逻辑复杂、用户体验要求高。但做好了,用户用起来会很顺手,所以还是值得花时间打磨的。

页面整体设计思路

添加家具页面采用分组表单的设计,把相关的字段放在一起,用卡片区分不同的信息类别。这样用户填写的时候思路更清晰,不会被一大堆输入框搞晕。

页面使用 StatefulWidget,因为有下拉选择框需要管理状态。整体布局用 SingleChildScrollView 包裹,表单内容多的时候可以滚动。

页面基础结构

先看页面的基础结构,包括状态变量和 build 方法的整体框架:

class AddFurniturePage extends StatefulWidget {
  const AddFurniturePage({super.key});
  
  State<AddFurniturePage> createState() => _AddFurniturePageState();
}

这里用 StatefulWidget 是因为页面里有两个下拉选择框,选中的值需要用状态来管理。如果用 StatelessWidget,下拉框选择后没法更新显示。

接下来是状态类的定义:

class _AddFurniturePageState extends State<AddFurniturePage> {
  String _room = '客厅';
  String _category = '沙发';
  final _rooms = ['客厅', '卧室', '书房', '餐厅', '厨房', '卫生间', '阳台'];
  final _categories = ['沙发', '床', '桌子', '椅子', '柜子', '灯具', '家电', '其他'];

_room_category 是两个下拉框的当前选中值,默认分别是"客厅"和"沙发"。_rooms_categories 是下拉选项列表,用 final 修饰因为它们不会变。

为什么要预设这些选项而不是让用户自己输入?主要是为了数据的一致性。如果用户随便输入,可能会出现"客厅"、“客廳”、"living room"这种同一个意思但写法不同的情况,后面做统计分析就麻烦了。

build 方法实现

build 方法里构建整个页面的 UI 结构:

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFFAF8F5),
      appBar: AppBar(
        title: const Text('添加家具'), 
        backgroundColor: const Color(0xFF8B4513), 
        foregroundColor: Colors.white
      ),

背景色用米白色 0xFFFAF8F5,和整个 App 保持一致。AppBar 用棕色主题色,标题文字白色。这个配色方案贯穿整个 App,保持视觉统一。

页面主体用 SingleChildScrollView 包裹,内容多的时候可以滚动:

      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          children: [
            _buildSection('基本信息', [
              _buildField('家具名称', '请输入家具名称', Icons.chair),
              _buildDropdown('所属房间', _rooms, _room, (v) => setState(() => _room = v!)),
              _buildDropdown('家具分类', _categories, _category, (v) => setState(() => _category = v!)),
              _buildField('品牌', '请输入品牌名称', Icons.business),
            ]),

表单分成四个区块:基本信息、购买信息、保修信息、规格信息。每个区块用 _buildSection 方法构建,传入标题和字段列表。

这种写法的好处是结构清晰,每个区块的字段一目了然。如果以后要加减字段,直接在数组里改就行了。

购买信息和保修信息区块

继续看后面几个区块的代码:

            SizedBox(height: 16.h),
            _buildSection('购买信息', [
              _buildField('购买价格', '请输入价格', Icons.payment),
              _buildDateField('购买日期'),
              _buildField('购买商家', '请输入商家名称', Icons.store),
            ]),
            SizedBox(height: 16.h),
            _buildSection('保修信息', [
              _buildDateField('保修到期日期'),
              _buildField('保修电话', '请输入保修联系电话', Icons.phone),
            ]),

区块之间用 SizedBox(height: 16.h) 隔开,16 是个比较舒适的间距,不会太挤也不会太散。

购买信息包括价格、日期、商家三个字段。保修信息包括到期日期和联系电话。这些都是买家具时比较重要的信息,以后查保修的时候会用到。

规格信息和保存按钮

最后是规格信息区块和保存按钮:

            SizedBox(height: 16.h),
            _buildSection('规格信息', [
              _buildField('尺寸', '长×宽×高 (cm)', Icons.straighten),
              _buildField('颜色', '请输入颜色', Icons.palette),
              _buildField('材质', '请输入材质', Icons.texture),
            ]),
            SizedBox(height: 24.h),
            _buildSaveButton(),
          ],
        ),
      ),
    );
  }

规格信息包括尺寸、颜色、材质。这些信息对于家具来说挺重要的,比如买窗帘的时候需要知道窗户尺寸,买沙发套需要知道沙发尺寸。

保存按钮放在最下面,和最后一个区块之间留了 24 的间距,比区块之间的间距大一些,视觉上有个收尾的感觉。

区块容器组件

_buildSection 方法用来构建每个表单区块的容器:

  Widget _buildSection(String title, List<Widget> children) {
    return Container(
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: Colors.white, 
        borderRadius: BorderRadius.circular(16.r)
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start, 
        children: [
          Text(title, style: TextStyle(
            fontSize: 16.sp, 
            fontWeight: FontWeight.bold, 
            color: const Color(0xFF5D4037)
          )),
          SizedBox(height: 16.h),
          ...children,
        ]
      ),
    );
  }

每个区块是一个白色圆角卡片,内边距 16。标题用深棕色 0xFF5D4037,字号 16,加粗显示。标题和内容之间留 16 的间距。

...children 是 Dart 的展开运算符,把传入的 Widget 列表展开放到 Column 里。这样调用的时候可以直接传数组,不用手动展开。

文本输入框组件

_buildField 方法构建普通的文本输入框:

  Widget _buildField(String label, String hint, IconData icon) {
    return Padding(
      padding: EdgeInsets.only(bottom: 12.h),
      child: TextFormField(
        decoration: InputDecoration(
          labelText: label, 
          hintText: hint,
          prefixIcon: Icon(icon, color: const Color(0xFF8B4513)),
          border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.r)),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(10.r), 
            borderSide: const BorderSide(color: Color(0xFF8B4513))
          ),
        ),
      ),
    );
  }

每个输入框底部留 12 的间距,这样多个输入框叠在一起不会太挤。

prefixIcon 是输入框左边的图标,用主题棕色。图标能帮助用户快速识别这个字段是干什么的,比如看到椅子图标就知道是家具名称,看到钱的图标就知道是价格。

focusedBorder 设置输入框获得焦点时的边框颜色,用主题棕色,和整体风格统一。默认的蓝色焦点边框和棕色主题不太搭。

下拉选择框组件

_buildDropdown 方法构建下拉选择框:

  Widget _buildDropdown(String label, List<String> items, String value, Function(String?) onChanged) {
    return Padding(
      padding: EdgeInsets.only(bottom: 12.h),
      child: DropdownButtonFormField<String>(
        value: value,
        decoration: InputDecoration(
          labelText: label, 
          border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.r))
        ),
        items: items.map((i) => DropdownMenuItem(value: i, child: Text(i))).toList(),
        onChanged: onChanged,
      ),
    );
  }

DropdownButtonFormField 是 Flutter 提供的表单下拉框组件,和 TextFormField 风格一致,放在一起比较协调。

items 参数通过 map 把字符串列表转换成 DropdownMenuItem 列表。onChanged 回调在选择改变时触发,外部传入的是 setState 调用,用来更新状态。

为什么用 DropdownButtonFormField 而不是普通的 DropdownButton?因为前者自带表单样式,有 label、边框这些,和其他输入框放在一起更统一。

日期选择器组件

_buildDateField 方法构建日期选择输入框:

  Widget _buildDateField(String label) {
    return Padding(
      padding: EdgeInsets.only(bottom: 12.h),
      child: TextFormField(
        readOnly: true,
        decoration: InputDecoration(
          labelText: label,
          prefixIcon: const Icon(Icons.calendar_today, color: Color(0xFF8B4513)),
          border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.r)),
        ),
        onTap: () async {
          await showDatePicker(
            context: context, 
            initialDate: DateTime.now(), 
            firstDate: DateTime(2000), 
            lastDate: DateTime(2030)
          );
        },
      ),
    );
  }

日期输入框设置 readOnly: true,用户不能直接输入文字,只能通过点击弹出日期选择器来选择日期。这样可以保证日期格式的一致性。

showDatePicker 是 Flutter 内置的日期选择器,initialDate 设为当前日期,firstDatelastDate 限制可选范围。这里设置从 2000 年到 2030 年,基本覆盖了家具购买的合理时间范围。

日期图标用日历图标 Icons.calendar_today,用户一看就知道这是选日期的。

保存按钮组件

_buildSaveButton 方法构建底部的保存按钮:

  Widget _buildSaveButton() {
    return SizedBox(
      width: double.infinity, 
      height: 50.h,
      child: ElevatedButton(
        onPressed: () { 
          Get.back(); 
          Get.snackbar('成功', '家具添加成功', 
            backgroundColor: Colors.green, 
            colorText: Colors.white
          ); 
        },
        style: ElevatedButton.styleFrom(
          backgroundColor: const Color(0xFF8B4513), 
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r))
        ),
        child: Text('保存', style: TextStyle(fontSize: 16.sp, color: Colors.white)),
      ),
    );
  }

按钮宽度设为 double.infinity 撑满整行,高度 50。用主题棕色作为背景色,圆角 12。

点击保存后,先用 Get.back() 返回上一页,然后用 Get.snackbar 显示成功提示。Snackbar 用绿色背景表示成功,这是比较通用的颜色约定。

实际项目中,这里应该先做表单验证,验证通过后再保存数据到数据库。现在先把 UI 做出来,数据层后面再接入。

表单验证的考虑

虽然现在没有实现表单验证,但设计的时候已经考虑到了。TextFormField 支持 validator 参数,可以传入验证函数:

TextFormField(
  validator: (value) {
    if (value == null || value.isEmpty) {
      return '请输入家具名称';
    }
    return null;
  },
)

验证失败时返回错误信息,验证通过返回 null。配合 Form 组件和 GlobalKey<FormState>,可以在保存时统一验证所有字段。

这个功能后面接入数据层的时候再加,现在先专注于 UI 实现。

屏幕适配说明

整个页面大量使用了 flutter_screenutil 的适配方法:

  • .w 用于宽度相关的值,比如 padding、margin
  • .h 用于高度相关的值,比如 SizedBox 的 height
  • .sp 用于字号
  • .r 用于圆角

这样在不同尺寸的设备上,UI 都能保持合适的比例。比如在平板上,按钮不会显得太小,在小屏手机上也不会太大。

用户体验优化点

这个页面在用户体验上做了几个优化:

第一,表单分组。把相关的字段放在一起,用户填写时思路更清晰。比如购买信息放一组,保修信息放一组。

第二,输入框带图标。图标能帮助用户快速识别字段用途,减少阅读 label 的时间。

第三,下拉选择代替手动输入。对于房间、分类这种有限选项的字段,用下拉框可以减少输入错误,也能保证数据一致性。

第四,日期选择器。日期用选择器而不是手动输入,避免格式错误,用户操作也更方便。

小结

添加家具页面是一个典型的表单页面,核心是把字段组织好,让用户填写起来顺畅。通过分组、图标、下拉框、日期选择器这些设计,可以大大提升用户体验。

代码组织上,把每种输入组件抽成独立方法,主 build 方法就很清晰。以后要修改某个组件的样式,直接找到对应方法改就行了。

下一篇会讲编辑家具页面的实现,和添加页面类似但有一些不同的处理,比如需要回显已有数据、支持删除操作等。


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

Logo

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

更多推荐