Files
2026-09-28 20:12:34 +08:00

281 lines
8.9 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 【FeedbackPopup】题目反馈弹层
/// (1:1 迁移 Vue components/FeedbackPopup/FeedbackPopup.vue,
/// BottomPopup → showModalBottomSheet)
///
/// 用法(与 Vue defineExpose 对齐):
/// `final key = GlobalKey<FeedbackPopupState>();`
/// `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<FeedbackPopup> createState() => FeedbackPopupState();
}
class FeedbackPopupState extends State<FeedbackPopup> {
/// 打开反馈弹层
void open(dynamic id, dynamic questionId) {
showModalBottomSheet<void>(
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<int> _selected = {};
final TextEditingController _contentController = TextEditingController();
List<UploadItem> _imageList = const [];
bool _submitting = false;
@override
void dispose() {
_contentController.dispose();
super.dispose();
}
Future<void> _submit() async {
if (_contentController.text.isEmpty) {
CenterToast.show(context, '请输入反馈内容');
return;
}
if (_selected.isEmpty) {
CenterToast.show(context, '请选择反馈类型');
return;
}
setState(() => _submitting = true);
// 提交中遮罩(对标 uni.showLoading),压在底部弹层之上
unawaited(showDialog<void>(
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),
);
}
}