/// ============================================================ /// 【FeedbackPopup】题目反馈弹层 /// (1:1 迁移 Vue components/FeedbackPopup/FeedbackPopup.vue, /// BottomPopup → showModalBottomSheet) /// /// 用法(与 Vue defineExpose 对齐): /// `final key = GlobalKey();` /// `FeedbackPopup(key: key);` /// `key.currentState!.open(id, questionId);` /// /// 校验顺序:反馈内容 → 反馈类型;成功后自动关闭并重置。 /// ============================================================ library; import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:new_learn/core/api/conquerPackage/classExercises/api.dart'; import 'package:new_learn/core/components/center_toast.dart'; import 'package:new_learn/core/components/upload.dart'; import 'exercise_styles.dart'; class _FeedbackOption { const _FeedbackOption(this.label, this.value); final String label; final int value; } const List<_FeedbackOption> _kOptions = [ _FeedbackOption('题干错误', 1), _FeedbackOption(' 解析看不懂', 2), _FeedbackOption('答案错误', 3), _FeedbackOption('选项错误', 4), _FeedbackOption('批阅错误', 5), _FeedbackOption('其他', 0), ]; class FeedbackPopup extends StatefulWidget { const FeedbackPopup({super.key}); @override State createState() => FeedbackPopupState(); } class FeedbackPopupState extends State { /// 打开反馈弹层 void open(dynamic id, dynamic questionId) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => _FeedbackSheet(id: id, questionId: questionId), ); } @override Widget build(BuildContext context) => const SizedBox.shrink(); } class _FeedbackSheet extends StatefulWidget { const _FeedbackSheet({required this.id, required this.questionId}); final dynamic id; final dynamic questionId; @override State<_FeedbackSheet> createState() => _FeedbackSheetState(); } class _FeedbackSheetState extends State<_FeedbackSheet> { final Set _selected = {}; final TextEditingController _contentController = TextEditingController(); List _imageList = const []; bool _submitting = false; @override void dispose() { _contentController.dispose(); super.dispose(); } Future _submit() async { if (_contentController.text.isEmpty) { CenterToast.show(context, '请输入反馈内容'); return; } if (_selected.isEmpty) { CenterToast.show(context, '请选择反馈类型'); return; } setState(() => _submitting = true); // 提交中遮罩(对标 uni.showLoading),压在底部弹层之上 unawaited(showDialog( context: context, barrierDismissible: false, barrierColor: Colors.black38, builder: (_) => const Center(child: CircularProgressIndicator()), )); final payload = { 'id': widget.id, 'questionId': widget.questionId, 'feedbackTypes': _selected.toList(), 'description': _contentController.text, 'images': _imageList.map((e) => e.url).toList(), }; try { final res = await ClassExercisesApi.instance.feedback(payload); if (!mounted) return; Navigator.of(context).pop(); // 关闭 loading if (res != null && res != false) { CenterToast.show(context, '反馈成功'); Navigator.of(context).pop(); // 关闭反馈弹层 } else { CenterToast.show(context, '反馈失败,请检查网络'); } } catch (_) { if (!mounted) return; Navigator.of(context).pop(); // 关闭 loading CenterToast.show(context, '反馈失败,请检查网络'); } finally { if (mounted) setState(() => _submitting = false); } } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom), child: Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(10)), ), child: SafeArea( top: false, child: Column( mainAxisSize: MainAxisSize.min, children: [ _buildHeader(), Flexible( child: SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 14.w), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildOptionsGrid(), Padding( padding: EdgeInsets.symmetric(vertical: 20.h), child: _buildTextarea(), ), Upload( title: '上传图片', maxCount: 5, type: UploadType.feedback, initialImageList: _imageList, onChanged: (list) => setState(() => _imageList = list), ), Padding( padding: EdgeInsets.only(top: 30.h), child: ExerciseButton( text: '提交反馈', style: ExerciseButtonStyle.blueGradient, width: double.infinity, enabled: !_submitting, onTap: _submit, ), ), SizedBox(height: 20.h), ], ), ), ), ], ), ), ), ); } Widget _buildHeader() { return Container( height: 60.h, // 120rpx padding: EdgeInsets.symmetric(horizontal: 14.w), decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFFE1EEFF), Colors.white], ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox(width: 24.w), Text( '题目反馈', style: TextStyle(fontSize: 16.sp, color: ExerciseColors.title), ), GestureDetector( onTap: () => Navigator.of(context).pop(), behavior: HitTestBehavior.opaque, child: Icon(Icons.close, size: 22.w, color: ExerciseColors.title), ), ], ), ); } Widget _buildOptionsGrid() { final chipW = (1.sw - 14.w * 2 - 16.w * 2) / 3; return Wrap( spacing: 16.w, // column-gap 32rpx runSpacing: 10.h, // row-gap 20rpx children: [ for (final opt in _kOptions) GestureDetector( onTap: () => setState(() { if (!_selected.remove(opt.value)) _selected.add(opt.value); }), behavior: HitTestBehavior.opaque, child: Container( width: chipW, padding: EdgeInsets.only(top: 11.w, bottom: 13.5.h), alignment: Alignment.center, decoration: BoxDecoration( color: _selected.contains(opt.value) ? const Color(0xFF6195F6) : const Color(0xFFF5F8FA), borderRadius: BorderRadius.circular(6.r), ), child: Text( opt.label, style: TextStyle( fontSize: 14.sp, color: _selected.contains(opt.value) ? Colors.white : const Color(0xFF687FAD), ), ), ), ), ], ); } Widget _buildTextarea() { return TextField( controller: _contentController, maxLines: 4, minLines: 4, maxLength: 500, onChanged: (_) => setState(() {}), decoration: InputDecoration( hintText: '请描述您的反馈...', hintStyle: TextStyle(fontSize: 14.sp, color: const Color(0xFFB3C2E0)), counterStyle: TextStyle(fontSize: 12.sp, color: const Color(0xFFB3C2E0)), filled: true, fillColor: const Color(0xFFFAFBFD), contentPadding: EdgeInsets.all(12.w), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8.r), borderSide: BorderSide(color: ExerciseColors.border, width: 1.w), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8.r), borderSide: BorderSide(color: ExerciseColors.primary, width: 1.w), ), ), style: TextStyle(fontSize: 14.sp, color: ExerciseColors.title), ); } }