253 lines
9.3 KiB
Dart
253 lines
9.3 KiB
Dart
/// ============================================================
|
||
/// 【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,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|