Files
App.K12.Student.Miniapp_Flu…/lib/features/conquerPackage/classExercises/widgets/conquer_popup.dart
T
2026-09-28 20:12:34 +08:00

253 lines
9.3 KiB
Dart
Raw 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.
/// ============================================================
/// 【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<String>? onConfirm;
final VoidCallback? onCancel;
static const Map<String, String> _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,
),
],
),
),
],
],
),
),
),
],
),
);
}
}