Flutter for OpenHarmony 剧本杀组队App实战:意见反馈功能实现

引言

意见反馈是收集用户建议和问题的重要渠道。本篇将实现意见反馈页面,支持选择反馈类型、输入内容和添加图片。通过完善的反馈机制,开发者可以及时了解用户的需求和问题,不断改进应用的功能和体验。意见反馈功能是应用与用户沟通的桥梁,通过收集用户的真实反馈,可以发现应用中存在的问题,了解用户的真实需求,从而指导产品的优化方向。

功能设计

意见反馈页面包含以下核心功能:

  • 反馈类型选择:支持功能建议、Bug反馈、体验问题、其他等多种类型。不同的反馈类型帮助开发者快速分类和处理反馈
  • 反馈内容输入框:提供充足的输入空间,支持多行文本输入。用户可以详细描述问题或建议
  • 图片添加(可选):允许用户上传截图或相关图片作为附件。图片可以更直观地说明问题
  • 提交按钮:提交反馈前进行表单验证,确保数据完整性。验证确保反馈的质量

数据模型定义

dart 复制代码
import 'package:flutter/material.dart';
import 'package:get/get.dart';

enum FeedbackType { suggestion, bug, experience, other }

class FeedbackModel {
  final String id;
  final String type;
  final String content;
  final List<String> images;
  final DateTime createTime;
  final String status;
  
  FeedbackModel({
    required this.id,
    required this.type,
    required this.content,
    required this.images,
    required this.createTime,
    required this.status,
  });
}

定义了FeedbackType枚举来表示反馈的类型。FeedbackModel数据模型包含反馈的ID、类型、内容、图片列表、创建时间和处理状态。id是反馈的唯一标识,用于后端数据关联和查询。type字段存储反馈的类型,用于分类处理。content字段存储反馈的详细内容。images字段是一个字符串列表,存储附加图片的路径或URL。createTime记录反馈的创建时间。status字段表示反馈的处理状态,如"待处理"、"处理中"、"已处理"等。

dart 复制代码
class FeedbackController extends GetxController {
  final feedbackType = '功能建议'.obs;
  final feedbackContent = ''.obs;
  final selectedImages = <String>[].obs;
  final isSubmitting = false.obs;
  final feedbackList = <FeedbackModel>[].obs;
  
  final List<String> feedbackTypes = ['功能建议', 'Bug反馈', '体验问题', '其他'];
  
  @override
  void onInit() {
    super.onInit();
    loadFeedbackHistory();
  }

FeedbackController继承GetxController,使用GetX框架的响应式编程模式。feedbackType是一个响应式变量,存储当前选中的反馈类型,默认值为"功能建议"。feedbackContent存储用户输入的反馈内容。selectedImages是一个响应式列表,存储用户选择的图片路径。isSubmitting表示是否正在提交反馈,用于控制提交按钮的状态。feedbackList存储所有的反馈记录。feedbackTypes是一个常量列表,包含所有可用的反馈类型。onInit方法在控制器初始化时调用,用于加载反馈历史记录。

dart 复制代码
  void loadFeedbackHistory() {
    feedbackList.value = [
      FeedbackModel(
        id: '1',
        type: '功能建议',
        content: '希望能添加更多的剧本类型',
        images: [],
        createTime: DateTime.now().subtract(const Duration(days: 3)),
        status: '已处理',
      ),
      FeedbackModel(
        id: '2',
        type: 'Bug反馈',
        content: '组队时偶尔会出现加载失败',
        images: [],
        createTime: DateTime.now().subtract(const Duration(days: 1)),
        status: '处理中',
      ),
    ];
  }

loadFeedbackHistory方法加载反馈历史记录。在实际项目中,这里应该调用API接口从后端获取真实的反馈数据。方法创建了两条测试数据,一条是功能建议,已处理;另一条是Bug反馈,处理中。这些测试数据用于演示反馈列表的显示效果。

dart 复制代码
  Future<void> submitFeedback() async {
    if (feedbackContent.value.trim().isEmpty) {
      Get.snackbar('提示', '请输入反馈内容');
      return;
    }
    
    isSubmitting.value = true;
    await Future.delayed(const Duration(seconds: 1));
    
    final newFeedback = FeedbackModel(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      type: feedbackType.value,
      content: feedbackContent.value,
      images: selectedImages.toList(),
      createTime: DateTime.now(),
      status: '待处理',
    );
    
    feedbackList.insert(0, newFeedback);
    feedbackContent.value = '';
    selectedImages.clear();
    isSubmitting.value = false;
    
    Get.back();
    Get.snackbar('成功', '感谢您的反馈!我们会尽快处理');
  }

submitFeedback方法处理反馈提交逻辑。首先检查反馈内容是否为空,如果为空则显示提示并返回。然后设置isSubmitting为true,显示加载状态。使用Future.delayed模拟网络请求延迟。创建一个新的FeedbackModel对象,包含当前的反馈类型、内容和图片。使用DateTime.now().millisecondsSinceEpoch生成唯一的ID。将新反馈插入到feedbackList的头部,使最新的反馈显示在最前面。清空反馈内容和图片列表,为下一次反馈做准备。设置isSubmitting为false,恢复提交按钮的状态。最后关闭当前页面并显示成功提示。

dart 复制代码
  void addImage(String imagePath) {
    if (selectedImages.length < 3) {
      selectedImages.add(imagePath);
    } else {
      Get.snackbar('提示', '最多只能添加3张图片');
    }
  }
  
  void removeImage(int index) {
    selectedImages.removeAt(index);
  }
}

addImage方法用于添加图片。首先检查已选择的图片数量是否少于3张,如果是则添加新图片,否则显示提示信息。这个限制防止用户添加过多的图片,节省存储空间和网络带宽。removeImage方法用于删除指定索引的图片。

反馈页面UI实现

dart 复制代码
class FeedbackPage extends StatelessWidget {
  FeedbackPage({super.key});
  final FeedbackController controller = Get.put(FeedbackController());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('意见反馈'),
        backgroundColor: const Color(0xFF6B4EFF),
        foregroundColor: Colors.white,
        elevation: 0,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [

FeedbackPage是一个StatelessWidget,使用Get.put(FeedbackController())在页面加载时创建并注入控制器。Scaffold提供了基本的页面结构。AppBar使用紫色背景,与应用的主题色保持一致。foregroundColor设置为白色,使标题和图标在紫色背景上清晰可见。elevation: 0移除AppBar的阴影效果。body使用SingleChildScrollView包装,支持页面内容的滚动。padding: 16为页面内容提供充足的边距。

dart 复制代码
            // 反馈类型
            const Text(
              '反馈类型',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
            ),
            const SizedBox(height: 12),
            Obx(() => Wrap(
              spacing: 8,
              runSpacing: 8,
              children: controller.feedbackTypes.map((type) {
                return ChoiceChip(
                  label: Text(type),
                  selected: controller.feedbackType.value == type,
                  selectedColor: const Color(0xFF6B4EFF).withOpacity(0.2),
                  labelStyle: TextStyle(
                    color: controller.feedbackType.value == type
                        ? const Color(0xFF6B4EFF)
                        : Colors.grey[700],
                    fontWeight: controller.feedbackType.value == type
                        ? FontWeight.bold
                        : FontWeight.normal,
                  ),
                  onSelected: (selected) {
                    if (selected) {
                      controller.feedbackType.value = type;
                    }
                  },
                );
              }).toList(),
            )),
            const SizedBox(height: 24),

反馈类型部分使用一个标题和多个ChoiceChip组件。Wrap布局用于自动换行显示多个芯片。spacing: 8设置芯片之间的水平间距,runSpacing: 8设置行之间的垂直间距。使用Obx包装Wrap,使其能够响应feedbackType的变化。ChoiceChip是一个单选芯片组件,用户点击时会选中该类型。selectedColor使用紫色半透明背景表示选中状态。labelStyle根据选中状态改变文字颜色和粗细,选中时使用紫色加粗,未选中时使用灰色正常。onSelected回调在用户选择时更新feedbackType。

dart 复制代码
            // 反馈内容
            const Text(
              '反馈内容',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
            ),
            const SizedBox(height: 12),
            Obx(() => TextField(
              onChanged: (value) => controller.feedbackContent.value = value,
              maxLines: 6,
              minLines: 6,
              decoration: InputDecoration(
                hintText: '请详细描述您的问题或建议...',
                hintStyle: TextStyle(color: Colors.grey[400]),
                filled: true,
                fillColor: Colors.grey[100],
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(8),
                  borderSide: BorderSide(color: Colors.grey[300]!),
                ),
                enabledBorder: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(8),
                  borderSide: BorderSide(color: Colors.grey[300]!),
                ),
                focusedBorder: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(8),
                  borderSide: const BorderSide(color: Color(0xFF6B4EFF), width: 2),
                ),
                contentPadding: const EdgeInsets.all(12),
              ),
            )),
            const SizedBox(height: 24),

反馈内容部分使用一个标题和一个多行TextField组件。maxLines和minLines都设置为6,确保输入框有充足的空间供用户输入详细的反馈内容。onChanged回调在用户输入时更新controller.feedbackContent。decoration设置了多种边框样式:enabledBorder用于未获焦点状态,focusedBorder用于获焦点状态(使用紫色边框宽度为2)。fillColor设置为浅灰色背景,提升输入框的可见性。hintText提供输入提示,引导用户输入详细的反馈内容。contentPadding: 12为输入框内容提供充足的内边距。

dart 复制代码
            // 添加图片
            const Text(
              '添加图片(可选)',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
            ),
            const SizedBox(height: 12),
            Obx(() => SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Row(
                children: [
                  // 添加图片按钮
                  if (controller.selectedImages.length < 3)
                    GestureDetector(
                      onTap: () {
                        Get.snackbar('提示', '点击选择图片');
                        controller.addImage('image_${DateTime.now().millisecondsSinceEpoch}.jpg');
                      },
                      child: Container(
                        width: 80,
                        height: 80,
                        decoration: BoxDecoration(
                          color: Colors.grey[200],
                          borderRadius: BorderRadius.circular(8),
                          border: Border.all(color: Colors.grey[300]!, width: 2),
                        ),
                        child: const Icon(
                          Icons.add_photo_alternate,
                          color: Colors.grey,
                          size: 32,
                        ),
                      ),
                    ),
                  const SizedBox(width: 8),

添加图片部分使用一个标题和一个水平滚动的Row。SingleChildScrollView支持水平滚动,当图片过多时可以滚动查看。当已选择的图片数量少于3张时,显示添加图片按钮。按钮是一个80x80的正方形容器,使用灰色背景和虚线边框。点击按钮时调用controller.addImage()添加图片。在实际项目中,应该使用image_picker插件打开图片选择器。

dart 复制代码
                  // 已选择的图片
                  ...controller.selectedImages.asMap().entries.map((entry) {
                    int index = entry.key;
                    String image = entry.value;
                    return Padding(
                      padding: const EdgeInsets.only(right: 8),
                      child: Stack(
                        children: [
                          Container(
                            width: 80,
                            height: 80,
                            decoration: BoxDecoration(
                              color: Colors.grey[300],
                              borderRadius: BorderRadius.circular(8),
                            ),
                            child: const Icon(
                              Icons.image,
                              color: Colors.grey,
                              size: 32,
                            ),
                          ),
                          Positioned(
                            top: -8,
                            right: -8,
                            child: GestureDetector(
                              onTap: () => controller.removeImage(index),
                              child: Container(
                                width: 24,
                                height: 24,
                                decoration: const BoxDecoration(
                                  color: Colors.red,
                                  shape: BoxShape.circle,
                                ),
                                child: const Icon(
                                  Icons.close,
                                  color: Colors.white,
                                  size: 16,
                                ),
                              ),
                            ),
                          ),
                        ],
                      ),
                    );
                  }).toList(),
                ],
              ),
            )),
            const SizedBox(height: 24),

已选择的图片部分使用asMap().entries.map()遍历selectedImages列表。对于每张图片,创建一个80x80的容器显示图片。使用Stack布局,在图片上方使用Positioned放置删除按钮。删除按钮是一个红色圆形容器,包含一个关闭图标。点击删除按钮时调用controller.removeImage()删除该图片。这个设计使得用户可以轻松管理已选择的图片。

dart 复制代码
            // 反馈历史
            const Text(
              '反馈历史',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
            ),
            const SizedBox(height: 12),
            Obx(() => controller.feedbackList.isEmpty
                ? Center(
                    child: Padding(
                      padding: const EdgeInsets.symmetric(vertical: 32),
                      child: Text(
                        '暂无反馈记录',
                        style: TextStyle(color: Colors.grey[600]),
                      ),
                    ),
                  )
                : ListView.builder(
                    shrinkWrap: true,
                    physics: const NeverScrollableScrollPhysics(),
                    itemCount: controller.feedbackList.length,
                    itemBuilder: (context, index) {
                      final feedback = controller.feedbackList[index];
                      return _buildFeedbackItem(feedback);
                    },
                  )),
            const SizedBox(height: 24),

反馈历史部分使用Obx包装ListView.builder,使其能够响应feedbackList的变化。当列表为空时显示"暂无反馈记录"的提示。当列表不为空时,使用ListView.builder动态构建反馈记录列表。shrinkWrap: true使ListView只占据必要的高度。physics: const NeverScrollableScrollPhysics()禁用ListView的滚动,因为整个页面已经使用SingleChildScrollView支持滚动。

dart 复制代码
            // 提交按钮
            Obx(() => SizedBox(
              width: double.infinity,
              height: 48,
              child: ElevatedButton(
                onPressed: controller.isSubmitting.value
                    ? null
                    : () => controller.submitFeedback(),
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF6B4EFF),
                  disabledBackgroundColor: Colors.grey[300],
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(8),
                  ),
                ),
                child: controller.isSubmitting.value
                    ? const SizedBox(
                        width: 20,
                        height: 20,
                        child: CircularProgressIndicator(
                          strokeWidth: 2,
                          valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                        ),
                      )
                    : const Text(
                        '提交反馈',
                        style: TextStyle(color: Colors.white, fontSize: 16),
                      ),
              ),
            )),
          ],
        ),
      ),
    );
  }

提交按钮使用Obx包装,使其能够响应isSubmitting的变化。当isSubmitting为true时,按钮显示加载动画并禁用。当isSubmitting为false时,按钮显示"提交反馈"文本并启用。_buildFeedbackItem方法构建反馈记录项。使用Container创建卡片效果,包含反馈类型、处理状态、反馈内容和创建时间。处理状态使用不同的颜色表示:已处理为绿色,处理中为橙色,待处理为灰色。反馈内容使用maxLines: 2和overflow: TextOverflow.ellipsis显示前两行,超出部分用省略号表示。

dart 复制代码
  Widget _buildFeedbackItem(FeedbackModel feedback) {
    return Container(
      margin: const EdgeInsets.only(bottom: 12),
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: Colors.grey[200]!),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(
                feedback.type,
                style: const TextStyle(
                  fontWeight: FontWeight.bold,
                  color: Color(0xFF6B4EFF),
                ),
              ),
              Container(
                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                decoration: BoxDecoration(
                  color: feedback.status == '已处理'
                      ? Colors.green.withOpacity(0.1)
                      : feedback.status == '处理中'
                          ? Colors.orange.withOpacity(0.1)
                          : Colors.grey.withOpacity(0.1),
                  borderRadius: BorderRadius.circular(4),
                ),
                child: Text(
                  feedback.status,
                  style: TextStyle(
                    fontSize: 12,
                    color: feedback.status == '已处理'
                        ? Colors.green
                        : feedback.status == '处理中'
                            ? Colors.orange
                            : Colors.grey,
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

技术要点

  1. ChoiceChip组件:单选芯片组件,用于反馈类型选择,支持自定义样式。ChoiceChip提供了简洁的单选界面
  2. 多行TextField:通过maxLines和minLines设置多行输入,提供充足的输入空间。用户可以详细描述问题或建议
  3. 表单验证:提交前检查内容是否为空,确保数据完整性。验证防止提交空反馈
  4. 图片管理:支持添加和删除图片,限制最多3张,提升用户体验。图片限制节省存储空间
  5. 状态管理:使用GetX的响应式编程实现数据的实时更新。当数据变化时,UI会自动刷新
  6. 加载状态:提交时显示加载动画,防止重复提交。加载动画提示用户正在处理

小结

本篇实现了意见反馈功能,用户可以选择反馈类型、输入详细内容并添加图片,帮助开发者收集用户建议。通过完善的表单验证和状态管理,提升了应用的交互体验。反馈历史记录展示了用户之前的反馈及其处理状态,增强了用户的参与感。意见反馈功能是应用与用户沟通的重要渠道,通过收集和分析用户反馈,可以不断改进应用的功能和体验。

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

相关推荐
Miguo94well2 小时前
Flutter框架跨平台鸿蒙开发——班级点名APP的开发流程
flutter·华为·harmonyos·鸿蒙
lbb 小魔仙2 小时前
【Harmonyos】开源鸿蒙跨平台训练营DAY7:Flutter鸿蒙实战轮播图搜索框和导航指示器
flutter·开源·harmonyos
九 龙2 小时前
Flutter框架跨平台鸿蒙开发——存款利息计算器APP的开发流程
flutter·华为·harmonyos·鸿蒙
晚霞的不甘2 小时前
Flutter for OpenHarmony《智慧字典》 App 底部导航栏深度解析:构建多页面应用的核心骨架
前端·经验分享·flutter·ui·前端框架·知识图谱
程序员清洒2 小时前
Flutter for OpenHarmony:Stack 与 Positioned — 层叠布局
开发语言·flutter·华为·鸿蒙
时光慢煮3 小时前
从进度可视化出发:基于 Flutter × OpenHarmony 的驾照学习助手实践
学习·flutter·华为·开源·openharmony
子春一3 小时前
Flutter for OpenHarmony:构建一个工业级 Flutter 计算器,深入解析表达式解析、状态管理与 Material 3 交互设计
flutter·交互
晚霞的不甘3 小时前
Flutter for OpenHarmony字典查询 App 全栈解析:从搜索交互到详情展示的完整实
flutter·架构·前端框架·全文检索·交互·个人开发
kirk_wang3 小时前
Flutter艺术探索-设计模式在Flutter中的应用:单例、工厂、观察者
flutter·移动开发·flutter教程·移动开发教程