feedback_bottom_sheet.dart 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_screenutil/flutter_screenutil.dart';
  3. import 'package:get/get.dart';
  4. import 'package:nomo/app/widgets/click_opacity.dart';
  5. import '../../config/translations/strings_enum.dart';
  6. import '../widgets/submit_btn.dart';
  7. import 'all_dialog.dart';
  8. /// 反馈弹窗底部弹出框
  9. class FeedbackBottomSheet extends StatefulWidget {
  10. const FeedbackBottomSheet({super.key});
  11. /// 显示反馈弹窗
  12. static Future<void> show() {
  13. return Get.bottomSheet(
  14. const FeedbackBottomSheet(),
  15. backgroundColor: Colors.transparent,
  16. isDismissible: true,
  17. enableDrag: true,
  18. isScrollControlled: true,
  19. );
  20. }
  21. @override
  22. State<FeedbackBottomSheet> createState() => _FeedbackBottomSheetState();
  23. }
  24. class _FeedbackBottomSheetState extends State<FeedbackBottomSheet>
  25. with SingleTickerProviderStateMixin {
  26. // 选中的表情索引(0-4)
  27. int? selectedEmojiIndex;
  28. // 选中的问题标签
  29. final Set<String> selectedIssues = {};
  30. // 表情列表
  31. final List<String> emojis = ['😡', '😥', '🤭', '😏', '🥰'];
  32. // 每个表情对应的问题标签
  33. Map<int, List<String>> get emojiIssues => {
  34. 0: [
  35. // 差评 😡
  36. Strings.vpnConnectionFailed.tr,
  37. Strings.internetTooSlow.tr,
  38. Strings.keepsDisconnecting.tr,
  39. Strings.appCrashes.tr,
  40. Strings.otherIssues.tr,
  41. ],
  42. 1: [
  43. // 一般偏差 😥
  44. Strings.connectionUnstable.tr,
  45. Strings.speedNotExpected.tr,
  46. Strings.hardToUse.tr,
  47. Strings.otherIssues.tr,
  48. ],
  49. 2: [
  50. // 中等 🤭
  51. Strings.worksFineNotFast.tr,
  52. Strings.limitedFreeServers.tr,
  53. Strings.appCouldBeSimpler.tr,
  54. Strings.sometimesDisconnects.tr,
  55. Strings.nothingSpecial.tr,
  56. ],
  57. 3: [
  58. // 好 😏
  59. Strings.easyToUse.tr,
  60. Strings.fastConnection.tr,
  61. Strings.stablePerformance.tr,
  62. Strings.usefulFreeVersion.tr,
  63. Strings.satisfiedOverall.tr,
  64. ],
  65. 4: [
  66. // 很好 🥰
  67. Strings.fastAndStable.tr,
  68. Strings.greatUserExperience.tr,
  69. Strings.excellentPremiumFeatures.tr,
  70. Strings.worthRecommending.tr,
  71. Strings.loveTheDesign.tr,
  72. ],
  73. };
  74. @override
  75. Widget build(BuildContext context) {
  76. return Container(
  77. decoration: BoxDecoration(
  78. color: Get.theme.highlightColor,
  79. borderRadius: BorderRadius.only(
  80. topLeft: Radius.circular(16.r),
  81. topRight: Radius.circular(16.r),
  82. ),
  83. ),
  84. child: SafeArea(
  85. child: Column(
  86. mainAxisSize: MainAxisSize.min,
  87. children: [
  88. // 关闭按钮
  89. _buildCloseButton(),
  90. // 标题
  91. _buildTitle(),
  92. 10.verticalSpaceFromWidth,
  93. // 副标题
  94. _buildSubtitle(),
  95. 10.verticalSpaceFromWidth,
  96. // 表情选择器
  97. _buildEmojiSelector(),
  98. // 问题标签区域 - 使用 AnimatedSize 实现平滑高度过渡
  99. AnimatedSize(
  100. duration: const Duration(milliseconds: 300),
  101. curve: Curves.easeInOut,
  102. child: selectedEmojiIndex != null
  103. ? Column(
  104. mainAxisSize: MainAxisSize.min,
  105. children: [24.verticalSpaceFromWidth, _buildIssueTags()],
  106. )
  107. : const SizedBox.shrink(),
  108. ),
  109. // 发送按钮 - 使用 AnimatedSize 实现平滑显示/隐藏
  110. AnimatedSize(
  111. duration: const Duration(milliseconds: 300),
  112. curve: Curves.easeInOut,
  113. child: selectedEmojiIndex != null
  114. ? _buildSendButton()
  115. : 24.verticalSpaceFromWidth,
  116. ),
  117. ],
  118. ),
  119. ),
  120. );
  121. }
  122. /// 构建关闭按钮
  123. Widget _buildCloseButton() {
  124. return Align(
  125. alignment: Alignment.centerRight,
  126. child: ClickOpacity(
  127. onTap: () => Navigator.of(Get.context!).pop(),
  128. child: Container(
  129. width: 24.w,
  130. height: 24.w,
  131. margin: EdgeInsets.only(right: 8.w, top: 8.w),
  132. padding: EdgeInsets.all(4.w),
  133. decoration: BoxDecoration(
  134. color: Get.theme.cardColor,
  135. shape: BoxShape.circle,
  136. ),
  137. child: Icon(
  138. Icons.close_rounded,
  139. size: 16.w,
  140. color: Get.theme.textTheme.bodyLarge!.color,
  141. ),
  142. ),
  143. ),
  144. );
  145. }
  146. /// 构建标题
  147. Widget _buildTitle() {
  148. return Text(
  149. Strings.howExperience.tr,
  150. textAlign: TextAlign.center,
  151. style: TextStyle(
  152. fontSize: 22.sp,
  153. fontWeight: FontWeight.w500,
  154. color: Get.theme.textTheme.bodyLarge!.color,
  155. height: 1.3,
  156. ),
  157. );
  158. }
  159. /// 构建副标题
  160. Widget _buildSubtitle() {
  161. return Text(
  162. Strings.wedLoveToKnow.tr,
  163. textAlign: TextAlign.center,
  164. style: TextStyle(
  165. fontSize: 16.sp,
  166. height: 1.4,
  167. color: Get.theme.hintColor,
  168. ),
  169. );
  170. }
  171. /// 构建表情选择器
  172. Widget _buildEmojiSelector() {
  173. return Row(
  174. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  175. children: List.generate(
  176. emojis.length,
  177. (index) => _buildEmojiButton(index),
  178. ),
  179. );
  180. }
  181. /// 构建单个表情按钮
  182. Widget _buildEmojiButton(int index) {
  183. final isSelected = selectedEmojiIndex == index;
  184. return ClickOpacity(
  185. onTap: () {
  186. setState(() {
  187. if (selectedEmojiIndex == index) {
  188. // 如果点击已选中的表情,取消选择
  189. selectedEmojiIndex = null;
  190. } else {
  191. // 选择新的表情
  192. selectedEmojiIndex = index;
  193. }
  194. // 切换表情时清空已选的问题标签
  195. selectedIssues.clear();
  196. });
  197. },
  198. child: Container(
  199. width: 56.w,
  200. height: 56.w,
  201. decoration: BoxDecoration(
  202. shape: BoxShape.circle,
  203. color: isSelected ? Get.theme.cardColor : Colors.transparent,
  204. border: Border.all(
  205. color: isSelected ? Get.theme.dividerColor : Colors.transparent,
  206. width: 1.w,
  207. ),
  208. ),
  209. alignment: Alignment.center,
  210. child: Text(emojis[index], style: TextStyle(fontSize: 32.sp)),
  211. ),
  212. );
  213. }
  214. /// 构建问题标签
  215. Widget _buildIssueTags() {
  216. if (selectedEmojiIndex == null) return const SizedBox.shrink();
  217. final issues = emojiIssues[selectedEmojiIndex!] ?? [];
  218. return Padding(
  219. padding: EdgeInsets.symmetric(horizontal: 24.w),
  220. child: Wrap(
  221. spacing: 10.w,
  222. runSpacing: 10.h,
  223. alignment: WrapAlignment.center,
  224. children: issues.map((issue) => _buildIssueTag(issue)).toList(),
  225. ),
  226. );
  227. }
  228. /// 构建单个问题标签
  229. Widget _buildIssueTag(String issue) {
  230. final isSelected = selectedIssues.contains(issue);
  231. return ClickOpacity(
  232. onTap: () {
  233. setState(() {
  234. if (isSelected) {
  235. selectedIssues.remove(issue);
  236. } else {
  237. selectedIssues.add(issue);
  238. }
  239. });
  240. },
  241. child: AnimatedContainer(
  242. duration: const Duration(milliseconds: 200),
  243. padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 8.h),
  244. decoration: BoxDecoration(
  245. color: isSelected ? Get.theme.primaryColor : Get.theme.cardColor,
  246. borderRadius: BorderRadius.circular(24.r),
  247. border: Border.all(
  248. color: isSelected ? Get.theme.primaryColor : Get.theme.dividerColor,
  249. width: 1.w,
  250. ),
  251. ),
  252. child: Text(
  253. issue,
  254. style: TextStyle(
  255. fontSize: 14.sp,
  256. color: isSelected
  257. ? Get.theme.textTheme.bodyLarge!.color
  258. : Get.theme.hintColor,
  259. ),
  260. ),
  261. ),
  262. );
  263. }
  264. /// 构建发送按钮
  265. Widget _buildSendButton() {
  266. // 必须选中表情并且选中至少一个问题标签才能发送
  267. final canSend = selectedEmojiIndex != null && selectedIssues.isNotEmpty;
  268. return Container(
  269. margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 24.h, bottom: 10.w),
  270. child: SubmitButton(
  271. text: Strings.send.tr,
  272. enabled: canSend,
  273. onPressed: canSend ? _handleSend : null,
  274. ),
  275. );
  276. }
  277. /// 处理发送
  278. void _handleSend() {
  279. Navigator.of(Get.context!).pop();
  280. // 显示成功提示
  281. AllDialog.showFeedback();
  282. }
  283. }