在这里插入图片描述

前言

垃圾分类有很多让人困惑的地方,比如外卖餐盒到底算什么垃圾?大骨头和小骨头分类一样吗?常见问题页面就是解答这些疑惑的,用问答的形式,简单直接。本文将详细介绍如何在Flutter for OpenHarmony环境下实现一个完整的常见问题页面。在日常生活中,很多人对垃圾分类存在各种各样的疑问,这个页面的目的就是帮助用户快速找到答案,解决他们在垃圾分类过程中遇到的困惑。

技术要点概览

本页面涉及的核心技术点包括以下几个方面:

  • ListView.builder:高效的列表渲染,适合展示大量问答数据
  • ExpansionTile:可展开和收起的问答组件,交互体验良好
  • Card组件:卡片式布局设计,视觉层次分明
  • 条件渲染:问题和答案标识的样式设计,区分清晰

数据准备

问答数据用Map存储,q是问题,a是答案:

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

  
  Widget build(BuildContext context) {
    final faqs = [
      {'q': '外卖餐盒属于什么垃圾?', 'a': '清洗干净的外卖餐盒属于可回收物,有油污的属于其他垃圾。'},
      {'q': '小骨头和大骨头分类一样吗?', 'a': '不一样。小骨头(鸡骨、鱼骨)属于厨余垃圾,大骨头(猪骨、牛骨)属于其他垃圾。'},
      {'q': '用过的纸巾可以回收吗?', 'a': '不可以。用过的纸巾已被污染,属于其他垃圾。'},
      {'q': '过期化妆品怎么处理?', 'a': '过期化妆品属于有害垃圾,请投放到有害垃圾桶。'},

这些问题都是日常生活中经常遇到的,选题很有针对性。

继续看后面的问题:

      {'q': '快递包装怎么分类?', 'a': '纸箱属于可回收物,塑料袋和泡沫属于其他垃圾,胶带属于其他垃圾。'},
      {'q': '宠物粪便属于什么垃圾?', 'a': '宠物粪便不属于生活垃圾分类范畴,建议冲入马桶或包好后投入其他垃圾。'},
      {'q': '椰子壳属于什么垃圾?', 'a': '椰子壳属于其他垃圾,因为它很难分解。'},
      {'q': '旧手机属于什么垃圾?', 'a': '旧手机属于可回收物,但建议通过正规渠道回收处理。'},
    ];

问题选择的考量:这些问题有个共同特点——答案不是那么直观。比如椰子壳看起来像是厨余垃圾,但实际上因为难以分解,属于其他垃圾。

使用Model类管理数据

class FaqItem {
  final String question;
  final String answer;
  final String? category;
  final int? viewCount;
  
  FaqItem({
    required this.question,
    required this.answer,
    this.category,
    this.viewCount,
  });
}

页面结构

ListView.builder渲染问题列表:

    return Scaffold(
      appBar: AppBar(title: const Text('常见问题')),
      body: ListView.builder(
        padding: EdgeInsets.all(16.w),
        itemCount: faqs.length,
        itemBuilder: (context, index) {
          final faq = faqs[index];
          return Card(
            margin: EdgeInsets.only(bottom: 12.h),
            child: ExpansionTile(

这里用了ExpansionTile组件,点击问题可以展开看答案,再点一下收起。这种交互方式很适合问答场景。

问题部分的UI

问题前面放了个Q标识:

              leading: Container(
                width: 32.w,
                height: 32.w,
                decoration: BoxDecoration(
                  color: AppTheme.primaryColor.withOpacity(0.1),
                  shape: BoxShape.circle,
                ),
                child: Center(
                  child: Text(
                    'Q',
                    style: TextStyle(
                      color: AppTheme.primaryColor,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
              title: Text(faq['q']!, style: TextStyle(fontSize: 15.sp)),

Q用圆形背景包裹,颜色是主题色的浅色版本,既醒目又不会太抢眼。

答案部分的UI

答案放在ExpansionTilechildren里,展开后才显示:

              children: [
                Padding(
                  padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h),
                  child: Row(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Container(
                        width: 32.w,
                        height: 32.w,
                        decoration: BoxDecoration(
                          color: AppTheme.secondaryColor.withOpacity(0.1),
                          shape: BoxShape.circle,
                        ),
                        child: Center(
                          child: Text(
                            'A',
                            style: TextStyle(
                              color: AppTheme.secondaryColor,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ),
                      ),

答案前面放了个A标识,用的是次要主题色,和Q形成呼应但又有区分。

                      SizedBox(width: 12.w),
                      Expanded(
                        child: Text(
                          faq['a']!,
                          style: TextStyle(fontSize: 14.sp, height: 1.5),
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

答案文字用Expanded包裹,保证长文本能正确换行。行高1.5让多行文字读起来更舒服。

ExpansionTile的优势

ExpansionTile而不是直接把问答都显示出来,有几个好处:

1. 节省空间

8个问答如果全部展开,页面会很长。折叠起来后,用户可以快速浏览所有问题。

2. 减少信息过载

一次性展示太多信息会让用户感到压力。折叠式设计让用户可以按需获取信息。

3. 交互感更强

点击展开、收起的交互让页面更有"活力",不是死板的静态页面。

搜索功能

问题多了之后,可以加个搜索框:

class FaqPageWithSearch extends StatelessWidget {
  final searchController = TextEditingController();
  final filteredFaqs = <Map<String, String>>[].obs;
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('常见问题')),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(16.w),
            child: TextField(
              controller: searchController,
              decoration: InputDecoration(
                hintText: '搜索问题',
                prefixIcon: Icon(Icons.search),
                filled: true,
                fillColor: Colors.grey.shade100,
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(12.r),
                  borderSide: BorderSide.none,
                ),
              ),
              onChanged: _filterFaqs,
            ),
          ),
          Expanded(
            child: Obx(() => ListView.builder(
              padding: EdgeInsets.symmetric(horizontal: 16.w),
              itemCount: filteredFaqs.length,
              itemBuilder: (context, index) => _buildFaqItem(filteredFaqs[index]),
            )),
          ),
        ],
      ),
    );
  }
  
  void _filterFaqs(String keyword) {
    if (keyword.isEmpty) {
      filteredFaqs.value = faqs;
    } else {
      filteredFaqs.value = faqs.where((faq) {
        return faq['q']!.contains(keyword) || faq['a']!.contains(keyword);
      }).toList();
    }
  }
}

分类展示

把问题按垃圾类型分组:

final groupedFaqs = {
  '可回收物': [
    {'q': '外卖餐盒属于什么垃圾?', 'a': '清洗干净的外卖餐盒属于可回收物...'},
    {'q': '旧手机属于什么垃圾?', 'a': '旧手机属于可回收物...'},
  ],
  '厨余垃圾': [
    {'q': '小骨头和大骨头分类一样吗?', 'a': '不一样...'},
  ],
  '有害垃圾': [
    {'q': '过期化妆品怎么处理?', 'a': '过期化妆品属于有害垃圾...'},
  ],
  '其他垃圾': [
    {'q': '用过的纸巾可以回收吗?', 'a': '不可以...'},
  ],
};

Widget _buildGroupedFaqs() {
  return ListView(
    children: groupedFaqs.entries.map((entry) {
      return ExpansionTile(
        title: Text(entry.key, style: TextStyle(fontWeight: FontWeight.bold)),
        children: entry.value.map((faq) => _buildFaqItem(faq)).toList(),
      );
    }).toList(),
  );
}

用户提问功能

让用户可以提交自己的问题:

Widget _buildAskButton() {
  return FloatingActionButton.extended(
    onPressed: () => _showAskDialog(),
    icon: Icon(Icons.add),
    label: Text('提问'),
  );
}

void _showAskDialog() {
  final questionController = TextEditingController();
  
  Get.dialog(
    AlertDialog(
      title: Text('提交问题'),
      content: TextField(
        controller: questionController,
        maxLines: 3,
        decoration: InputDecoration(
          hintText: '请输入您的问题...',
          border: OutlineInputBorder(),
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Get.back(),
          child: Text('取消'),
        ),
        ElevatedButton(
          onPressed: () {
            if (questionController.text.isNotEmpty) {
              _submitQuestion(questionController.text);
              Get.back();
              Get.snackbar('成功', '问题已提交,我们会尽快回复');
            }
          },
          child: Text('提交'),
        ),
      ],
    ),
  );
}

热门问题排序

根据用户点击量排序:

class FaqController extends GetxController {
  final faqs = <FaqItem>[].obs;
  
  void incrementViewCount(String questionId) {
    final index = faqs.indexWhere((f) => f.id == questionId);
    if (index != -1) {
      faqs[index] = faqs[index].copyWith(
        viewCount: (faqs[index].viewCount ?? 0) + 1,
      );
      _sortByViewCount();
    }
  }
  
  void _sortByViewCount() {
    faqs.sort((a, b) => (b.viewCount ?? 0).compareTo(a.viewCount ?? 0));
  }
}

反馈功能

让用户反馈答案是否有帮助:

Widget _buildFeedback(FaqItem faq) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.end,
    children: [
      Text('这个回答有帮助吗?', style: TextStyle(fontSize: 12.sp, color: Colors.grey)),
      IconButton(
        icon: Icon(Icons.thumb_up_outlined, size: 18.sp),
        onPressed: () => _markHelpful(faq.id, true),
      ),
      IconButton(
        icon: Icon(Icons.thumb_down_outlined, size: 18.sp),
        onPressed: () => _markHelpful(faq.id, false),
      ),
    ],
  );
}

性能优化

1. 使用const构造函数

const Text('常见问题')
const Icon(Icons.search)

2. 列表项使用Key

return Card(
  key: ValueKey(faq['q']),
  // ...
);

总结

这个页面的核心价值是解决用户的实际困惑。问题选得好、答案写得清楚,比花哨的UI更重要。本文介绍的实现方案包括:

  1. ExpansionTile:可展开/收起的问答组件
  2. Q/A标识:清晰的问答视觉区分
  3. 搜索功能:快速找到相关问题
  4. 分类展示:按垃圾类型分组

通过合理的页面设计,可以帮助用户快速找到垃圾分类的答案。


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

Logo

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

更多推荐