Flutter for OpenHarmony垃圾分类指南App实战:意见反馈实现

前言

用户的反馈是产品改进的重要来源。意见反馈页面让用户可以方便地提交问题和建议,是App和用户沟通的桥梁。本文将详细介绍如何在Flutter for OpenHarmony环境下实现一个完整的意见反馈页面,包括表单设计、类型选择、输入验证以及提交逻辑等核心技术点。

技术要点概览

本页面涉及的核心技术点:

  • StatefulWidget:管理表单状态
  • TextEditingController:输入框控制器
  • ChoiceChip:反馈类型选择
  • 表单验证:输入内容的有效性检查
  • SingleChildScrollView:键盘弹出时内容滚动

为什么用StatefulWidget

这个页面需要管理两个状态:选中的反馈类型和输入框的内容:

dart 复制代码
class FeedbackPage extends StatefulWidget {
  const FeedbackPage({super.key});

  @override
  State<FeedbackPage> createState() => _FeedbackPageState();
}

class _FeedbackPageState extends State<FeedbackPage> {
  final _controller = TextEditingController();
  String _selectedType = '功能建议';

TextEditingController用来控制输入框,可以获取输入的内容,也可以清空输入框。_selectedType记录当前选中的反馈类型。

资源释放

dart 复制代码
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

页面布局

页面分为三个部分:反馈类型选择、反馈内容输入、提交按钮:

dart 复制代码
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('意见反馈')),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('反馈类型', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500)),
            SizedBox(height: 12.h),

SingleChildScrollView包裹,这样键盘弹出时内容可以滚动,输入框不会被遮挡。

反馈类型选择

ChoiceChip组件实现类型选择:

dart 复制代码
            Wrap(
              spacing: 12.w,
              children: ['功能建议', '问题反馈', '内容纠错', '其他'].map((type) {
                return ChoiceChip(
                  label: Text(type),
                  selected: _selectedType == type,
                  selectedColor: AppTheme.primaryColor.withOpacity(0.2),
                  onSelected: (selected) {
                    if (selected) setState(() => _selectedType = type);
                  },
                );
              }).toList(),
            ),

组件选择ChoiceChip是Material Design提供的选择芯片组件,自带选中和未选中的样式,用起来很方便。Wrap组件让芯片自动换行,不会溢出屏幕。

反馈类型说明

四种反馈类型覆盖了常见的场景:

  • 功能建议:用户希望增加某个功能
  • 问题反馈:用户遇到了bug或问题
  • 内容纠错:垃圾分类信息有误
  • 其他:不属于以上类型的反馈

增强版类型选择

可以为每种类型添加图标和描述:

dart 复制代码
final feedbackTypes = [
  {'type': '功能建议', 'icon': Icons.lightbulb_outline, 'desc': '希望增加的功能'},
  {'type': '问题反馈', 'icon': Icons.bug_report, 'desc': '遇到的bug或问题'},
  {'type': '内容纠错', 'icon': Icons.edit, 'desc': '分类信息有误'},
  {'type': '其他', 'icon': Icons.more_horiz, 'desc': '其他类型的反馈'},
];

Widget _buildTypeSelector() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: feedbackTypes.map((item) {
      final isSelected = _selectedType == item['type'];
      return Card(
        margin: EdgeInsets.only(bottom: 8.h),
        color: isSelected ? AppTheme.primaryColor.withOpacity(0.1) : null,
        child: ListTile(
          leading: Icon(
            item['icon'] as IconData,
            color: isSelected ? AppTheme.primaryColor : Colors.grey,
          ),
          title: Text(item['type'] as String),
          subtitle: Text(item['desc'] as String),
          trailing: isSelected ? Icon(Icons.check, color: AppTheme.primaryColor) : null,
          onTap: () => setState(() => _selectedType = item['type'] as String),
        ),
      );
    }).toList(),
  );
}

反馈内容输入

TextField组件实现多行文本输入:

dart 复制代码
            SizedBox(height: 24.h),
            Text('反馈内容', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500)),
            SizedBox(height: 12.h),
            TextField(
              controller: _controller,
              maxLines: 6,
              maxLength: 500,
              decoration: InputDecoration(
                hintText: '请详细描述您的问题或建议...',
                filled: true,
                fillColor: Colors.white,
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(12.r),
                  borderSide: BorderSide.none,
                ),
              ),
            ),

maxLines: 6让输入框显示6行高度,适合输入较长的内容。hintText给用户一个提示,告诉他们应该输入什么。

样式细节filled: true配合fillColor: Colors.white让输入框有白色背景。borderSide: BorderSide.none去掉边框线,看起来更简洁。

图片上传功能

有些问题用文字描述不清楚,让用户上传截图会更有帮助:

dart 复制代码
final _images = <File>[].obs;

Widget _buildImageUploader() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text('上传截图(可选)', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500)),
      SizedBox(height: 12.h),
      Obx(() => Wrap(
        spacing: 8.w,
        runSpacing: 8.h,
        children: [
          ..._images.map((file) => _buildImageItem(file)),
          if (_images.length < 4) _buildAddButton(),
        ],
      )),
    ],
  );
}

Widget _buildImageItem(File file) {
  return Stack(
    children: [
      ClipRRect(
        borderRadius: BorderRadius.circular(8.r),
        child: Image.file(
          file,
          width: 80.w,
          height: 80.w,
          fit: BoxFit.cover,
        ),
      ),
      Positioned(
        top: 0,
        right: 0,
        child: GestureDetector(
          onTap: () => _images.remove(file),
          child: Container(
            padding: EdgeInsets.all(2.w),
            decoration: BoxDecoration(
              color: Colors.red,
              shape: BoxShape.circle,
            ),
            child: Icon(Icons.close, size: 14.sp, color: Colors.white),
          ),
        ),
      ),
    ],
  );
}

Widget _buildAddButton() {
  return GestureDetector(
    onTap: _pickImage,
    child: Container(
      width: 80.w,
      height: 80.w,
      decoration: BoxDecoration(
        color: Colors.grey.shade200,
        borderRadius: BorderRadius.circular(8.r),
        border: Border.all(color: Colors.grey.shade300, style: BorderStyle.solid),
      ),
      child: Icon(Icons.add_photo_alternate, size: 32.sp, color: Colors.grey),
    ),
  );
}

Future<void> _pickImage() async {
  final picker = ImagePicker();
  final image = await picker.pickImage(source: ImageSource.gallery);
  if (image != null) {
    _images.add(File(image.path));
  }
}

联系方式输入

可以让用户留下联系方式,方便后续沟通:

dart 复制代码
final _contactController = TextEditingController();

Widget _buildContactInput() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text('联系方式(可选)', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500)),
      SizedBox(height: 12.h),
      TextField(
        controller: _contactController,
        decoration: InputDecoration(
          hintText: '请输入手机号或邮箱',
          filled: true,
          fillColor: Colors.white,
          prefixIcon: Icon(Icons.contact_mail),
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12.r),
            borderSide: BorderSide.none,
          ),
        ),
      ),
    ],
  );
}

提交按钮

dart 复制代码
            SizedBox(height: 32.h),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: _submit,
                style: ElevatedButton.styleFrom(
                  backgroundColor: AppTheme.primaryColor,
                  padding: EdgeInsets.symmetric(vertical: 16.h),
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12.r),
                  ),
                ),
                child: Text('提交反馈', style: TextStyle(fontSize: 16.sp, color: Colors.white)),
              ),
            ),
          ],
        ),
      ),
    );
  }

按钮宽度撑满屏幕,高度通过padding控制。圆角和输入框保持一致,视觉上更协调。

提交逻辑

提交前要验证用户是否输入了内容:

dart 复制代码
  bool _isSubmitting = false;
  
  void _submit() async {
    // 验证输入
    if (_controller.text.trim().isEmpty) {
      Get.snackbar('提示', '请输入反馈内容');
      return;
    }
    
    if (_controller.text.trim().length < 10) {
      Get.snackbar('提示', '反馈内容至少需要10个字符');
      return;
    }
    
    // 防止重复提交
    if (_isSubmitting) return;
    
    setState(() => _isSubmitting = true);
    
    try {
      // 上传图片
      final imageUrls = await _uploadImages();
      
      // 提交反馈
      await _submitFeedback(
        type: _selectedType,
        content: _controller.text.trim(),
        images: imageUrls,
        contact: _contactController.text.trim(),
      );
      
      Get.snackbar('成功', '感谢您的反馈!');
      Get.back();
    } catch (e) {
      Get.snackbar('错误', '提交失败,请重试');
    } finally {
      setState(() => _isSubmitting = false);
    }
  }
  
  Future<List<String>> _uploadImages() async {
    final urls = <String>[];
    for (var image in _images) {
      final url = await ApiService.uploadImage(image);
      urls.add(url);
    }
    return urls;
  }
  
  Future<void> _submitFeedback({
    required String type,
    required String content,
    required List<String> images,
    required String contact,
  }) async {
    await ApiService.submitFeedback({
      'type': type,
      'content': content,
      'images': images,
      'contact': contact,
      'deviceInfo': await _getDeviceInfo(),
      'appVersion': await _getAppVersion(),
    });
  }
}

验证逻辑说明

trim()去掉首尾空格,避免用户只输入空格就提交。提交成功后显示感谢提示,然后返回上一页。

草稿保存

用户输入到一半退出了,下次进来内容还在:

dart 复制代码
@override
void initState() {
  super.initState();
  _loadDraft();
}

Future<void> _loadDraft() async {
  final draft = await storage.read('feedback_draft');
  if (draft != null) {
    _controller.text = draft['content'] ?? '';
    _selectedType = draft['type'] ?? '功能建议';
    setState(() {});
  }
}

Future<void> _saveDraft() async {
  await storage.write('feedback_draft', {
    'content': _controller.text,
    'type': _selectedType,
  });
}

@override
void dispose() {
  _saveDraft();
  _controller.dispose();
  super.dispose();
}

历史记录

让用户可以查看自己提交过的反馈和处理状态:

dart 复制代码
Widget _buildHistoryButton() {
  return TextButton.icon(
    onPressed: () => Get.toNamed(Routes.feedbackHistory),
    icon: Icon(Icons.history),
    label: Text('历史反馈'),
  );
}

// 历史反馈页面
class FeedbackHistoryPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('历史反馈')),
      body: Obx(() {
        final list = controller.feedbackHistory;
        if (list.isEmpty) {
          return Center(child: Text('暂无反馈记录'));
        }
        
        return ListView.builder(
          itemCount: list.length,
          itemBuilder: (context, index) {
            final item = list[index];
            return Card(
              margin: EdgeInsets.all(8.w),
              child: ListTile(
                title: Text(item.type),
                subtitle: Text(item.content, maxLines: 2, overflow: TextOverflow.ellipsis),
                trailing: _buildStatusChip(item.status),
                onTap: () => _showDetail(item),
              ),
            );
          },
        );
      }),
    );
  }
  
  Widget _buildStatusChip(String status) {
    Color color;
    switch (status) {
      case 'pending':
        color = Colors.orange;
        break;
      case 'processing':
        color = Colors.blue;
        break;
      case 'resolved':
        color = Colors.green;
        break;
      default:
        color = Colors.grey;
    }
    
    return Chip(
      label: Text(_getStatusText(status), style: TextStyle(fontSize: 12.sp, color: Colors.white)),
      backgroundColor: color,
    );
  }
}

设备信息收集

收集设备信息有助于排查问题:

dart 复制代码
Future<Map<String, dynamic>> _getDeviceInfo() async {
  final deviceInfo = DeviceInfoPlugin();
  
  if (Platform.isAndroid) {
    final info = await deviceInfo.androidInfo;
    return {
      'platform': 'Android',
      'model': info.model,
      'version': info.version.release,
      'sdk': info.version.sdkInt,
    };
  } else if (Platform.isIOS) {
    final info = await deviceInfo.iosInfo;
    return {
      'platform': 'iOS',
      'model': info.model,
      'version': info.systemVersion,
    };
  }
  
  return {'platform': 'Unknown'};
}

性能优化

1. 使用const构造函数

dart 复制代码
const Text('反馈类型')
const Icon(Icons.add_photo_alternate, size: 32)

2. 防止重复提交

dart 复制代码
bool _isSubmitting = false;

void _submit() {
  if (_isSubmitting) return;
  _isSubmitting = true;
  // ...
}

总结

意见反馈页面是产品和用户沟通的重要渠道。做好这个功能,能帮助产品持续改进,也能让用户感受到被重视。本文介绍的实现方案包括:

  1. 表单设计:类型选择、内容输入、图片上传
  2. 输入验证:空值检查、长度检查
  3. 提交逻辑:防重复提交、错误处理
  4. 用户体验:草稿保存、历史记录

通过完善的反馈功能,可以建立起产品和用户之间的良好沟通渠道。


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

相关推荐
子春一2 小时前
Flutter for OpenHarmony:构建一个 Flutter 天气卡片组件,深入解析动态 UI、响应式布局与语义化设计
javascript·flutter·ui
雨季6662 小时前
Flutter 三端应用实战:OpenHarmony “极简文本行数统计器”
开发语言·前端·flutter·ui·交互
爱吃大芒果2 小时前
Flutter for OpenHarmony 适配:mango_shop 页面布局的鸿蒙多设备屏幕适配方案
flutter·华为·harmonyos
雨季6662 小时前
Flutter 三端应用实战:OpenHarmony 简易“动态字体大小调节器”交互模式深度解析
开发语言·flutter·ui·交互·dart
urkay-3 小时前
Android 中实现 HMAC-SHA256
android·开发语言·python
YIN_尹3 小时前
【MySQL】增删查改的艺术——数据库CRUD完全指南(下)
android·数据库·mysql
m0_748233173 小时前
PHP8.0新特性全解析
android
2601_949543013 小时前
Flutter for OpenHarmony垃圾分类指南App实战:政策法规实现
大数据·flutter
一起养小猫3 小时前
Flutter for OpenHarmony 实战:从零开发一款五子棋游戏
android·前端·javascript·flutter·游戏·harmonyos