/// ============================================================ /// 【ConquerPopup】攻克流程弹窗 /// (1:1 迁移 Vue components/ConquerPopup/ConquerPopup.vue) /// /// 四种类型: /// note 过程笔记提示 /// time 最低作答时长提示(确认键在左、取消键在右) /// submit 确认交卷(取消键在左、确认键在右) /// grading 题目批改中(遮罩与按钮都不能关闭;唯一按钮 emit confirm) /// /// 事件(与 Vue emit 对齐): /// onConfirm(action) 点确认按钮 /// onCancel() 点遮罩/取消;若 confirmAction == 'showAnswerSheet' /// (isConfirm 单按钮或遮罩)会紧跟一次 onConfirm(action) /// ============================================================ library; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:new_learn/core/components/rich_text_content.dart'; import 'package:new_learn/core/utils/img_url.dart'; import 'exercise_styles.dart'; class ConquerPopup extends StatelessWidget { const ConquerPopup({ super.key, required this.show, this.type = 'note', this.title = '', this.subtitle = '', this.confirmText = '确定', this.cancelText = '取消', this.isConfirm = false, this.confirmAction = '', this.onConfirm, this.onCancel, }); final bool show; /// note / time / submit / grading final String type; final String title; final String subtitle; final String confirmText; final String cancelText; final bool isConfirm; final String confirmAction; final ValueChanged? onConfirm; final VoidCallback? onCancel; static const Map _bgMap = { 'note': '/StaticResource/2026-04-08/conquer-note-bg.png', 'time': '/StaticResource/2026-04-08/conquer-time-bg.png', 'submit': '/StaticResource/2026-04-08/conquer-submit-bg.png', 'grading': '/StaticResource/2026-04-08/conquer-grading-bg.png', }; /// grading 遮罩点击:直接拦截不关闭 void _handleMaskCancel() { if (type == 'grading') return; _handleCancel(); } /// 普通取消:关闭 + cancel + showAnswerSheet 时补发 confirm void _handleCancel() { onCancel?.call(); if (confirmAction == 'showAnswerSheet') { onConfirm?.call(confirmAction); } } void _handleConfirm() { onConfirm?.call(confirmAction); } /// 标题/副标题可能是 HTML(v-html) Widget _htmlOrText( String html, { required double fontSize, required Color color, FontWeight? fontWeight, double? height, }) { final style = TextStyle( fontSize: fontSize, color: color, fontWeight: fontWeight, height: height, ); if (html.contains(RegExp(r'<[a-zA-Z][^>]*>'))) { return RichTextContent(html: html, textStyle: style); } return Text(html, textAlign: TextAlign.center, style: style); } @override Widget build(BuildContext context) { if (!show) return const SizedBox.shrink(); final isPad = isPadDevice(context); final isGrading = type == 'grading'; return Positioned.fill( child: Stack( children: [ // mask rgba(0,0,0,.6) Positioned.fill( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: _handleMaskCancel, child: const ColoredBox(color: Color(0x99000000)), ), ), Center( child: Container( width: isPad ? 305.w : 278.w, // 556rpx / 610rpx constraints: BoxConstraints(minHeight: 352.5.h), // 705rpx padding: EdgeInsets.only( left: 27.5.w, // 55rpx right: 25.w, // 50rpx bottom: 30.h, // 60rpx ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15.r), image: DecorationImage( image: CachedNetworkImageProvider( ImgUrl.build(_bgMap[type] ?? _bgMap['note']!), ), fit: BoxFit.fitWidth, alignment: Alignment.topCenter, ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: EdgeInsets.only(top: isPad ? 170.h : 150.h), child: _htmlOrText( title, fontSize: 18.sp, // 36rpx color: ExerciseColors.title, fontWeight: FontWeight.w600, height: 25.h / 18.sp, ), ), if (isGrading) ...[ // 预计 3~5 分钟有结果 Container( margin: EdgeInsets.only(top: 20.h), height: 44.h, // 88rpx alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xFFF2F8FF), borderRadius: BorderRadius.circular(6.r), ), child: Text.rich( TextSpan( style: TextStyle( fontSize: 14.sp, color: ExerciseColors.subtitle, ), children: [ const TextSpan(text: '预计 '), TextSpan( text: '3~5', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: ExerciseColors.primary, ), ), const TextSpan(text: ' 分钟有结果'), ], ), ), ), Padding( padding: EdgeInsets.only(top: 15.h), child: ExerciseButton( text: confirmText, style: ExerciseButtonStyle.primary, width: 220.w, // 440rpx onTap: _handleConfirm, ), ), ] else ...[ if (subtitle.isNotEmpty) Padding( padding: EdgeInsets.only(top: 10.h, left: 10.w, right: 10.w), child: _htmlOrText( subtitle, fontSize: 14.sp, // 28rpx color: ExerciseColors.subtitle, height: 22.h / 14.sp, ), ), Padding( padding: EdgeInsets.only(top: 25.h), child: isConfirm ? ExerciseButton( text: confirmText, style: ExerciseButtonStyle.primary, width: double.infinity, onTap: _handleCancel, ) : Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: type == 'time' ? [ // time:确认在左(白底)、取消在右(蓝底) ExerciseButton( text: confirmText, style: ExerciseButtonStyle.secondary, width: 105.w, // 210rpx onTap: _handleConfirm, ), ExerciseButton( text: cancelText, style: ExerciseButtonStyle.primary, width: 105.w, onTap: _handleCancel, ), ] : [ // note / submit:取消在左、确认在右 ExerciseButton( text: cancelText, style: ExerciseButtonStyle.secondary, width: 105.w, onTap: _handleCancel, ), ExerciseButton( text: confirmText, style: ExerciseButtonStyle.primary, width: 105.w, onTap: _handleConfirm, ), ], ), ), ], ], ), ), ), ], ), ); } }