This commit is contained in:
chenshuainan
2026-09-28 20:12:34 +08:00
parent 847986f861
commit 5f53bd8f94
60 changed files with 14624 additions and 398 deletions
@@ -0,0 +1,252 @@
/// ============================================================
/// 【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,
),
],
),
),
],
],
),
),
),
],
),
);
}
}