
前言
用户的反馈是产品改进的重要来源。意见反馈页面让用户可以方便地提交问题和建议,是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;
// ...
}
总结
意见反馈页面是产品和用户沟通的重要渠道。做好这个功能,能帮助产品持续改进,也能让用户感受到被重视。本文介绍的实现方案包括:
- 表单设计:类型选择、内容输入、图片上传
- 输入验证:空值检查、长度检查
- 提交逻辑:防重复提交、错误处理
- 用户体验:草稿保存、历史记录
通过完善的反馈功能,可以建立起产品和用户之间的良好沟通渠道。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net