- 为conquerPackage模块添加过渡页面路由并实现页面跳转逻辑 - 新增通用多图上传组件Upload,支持多种模式与图片操作功能 - 新增任务相关的API及Model基础文件 - 注释课前课中页面的测试试题内容
704 lines
21 KiB
Dart
704 lines
21 KiB
Dart
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:new_learn/core/api/conquerPackage/beforeDuringClass/api.dart';
|
|
import 'package:new_learn/core/api/conquerPackage/beforeDuringClass/model.dart';
|
|
import 'package:new_learn/core/utils/img_url.dart';
|
|
|
|
/// ============================================================
|
|
/// 【TransitionPage】课前/课中/课后 过渡页面
|
|
/// 对标 Vue pages/conquerPackage/transitionPage/transitionPage.vue
|
|
///
|
|
/// 一、三种状态:
|
|
/// status = 1 → 课前讲义(带滑动蒙层引导)
|
|
/// status = 2 → 课中视频
|
|
/// status = 3 → 课后练习(含提示卡片 + 攻克标准)
|
|
///
|
|
/// 二、路由参数(通过 extra 传入):
|
|
/// - courseId 课程 id
|
|
/// - packId 攻克包 id
|
|
/// - recordId 学习记录 id
|
|
/// - status 状态:1课前 / 2课中 / 3课后
|
|
///
|
|
/// 三、尺寸换算(本工程统一):
|
|
/// Vue 的 N rpx → Flutter 的 (N/2).w/.h/.sp/.r
|
|
///
|
|
/// 四、跳转:
|
|
/// status 1/2 → 跳 beforeDuringClass 页
|
|
/// status 3 → 跳 classExercises 页
|
|
/// ============================================================
|
|
|
|
/// 过渡页状态
|
|
enum TransitionStatus {
|
|
beforeClass(1), // 课前讲义
|
|
duringClass(2), // 课中视频
|
|
afterClass(3); // 课后练习
|
|
|
|
final int value;
|
|
const TransitionStatus(this.value);
|
|
|
|
factory TransitionStatus.fromInt(dynamic v) {
|
|
final n = int.tryParse(v?.toString() ?? '') ?? 1;
|
|
return TransitionStatus.values.firstWhere(
|
|
(e) => e.value == n,
|
|
orElse: () => TransitionStatus.beforeClass,
|
|
);
|
|
}
|
|
}
|
|
|
|
class TransitionPage extends StatefulWidget {
|
|
const TransitionPage({super.key, this.extra});
|
|
|
|
final Map<String, dynamic>? extra;
|
|
|
|
@override
|
|
State<TransitionPage> createState() => _TransitionPageState();
|
|
}
|
|
|
|
class _TransitionPageState extends State<TransitionPage> {
|
|
// ========== 颜色常量 ==========
|
|
static const Color _bgColor = Color(0xFFF5F8FA);
|
|
static const Color _titleColor = Color(0xFF1D2B47);
|
|
static const Color _subtitleColor = Color(0xFF99A4BA);
|
|
static const Color _primary = Color(0xFF3D7DF5);
|
|
static const Color _labelBg = Color(0xFFE3ECFF);
|
|
|
|
// ========== 路由参数 ==========
|
|
late String _courseId;
|
|
late String _packId;
|
|
late String _recordId;
|
|
late TransitionStatus _status;
|
|
|
|
// ========== 数据 ==========
|
|
OvercomeRecordInfo _overcomeInfo = const OvercomeRecordInfo();
|
|
bool _loading = true;
|
|
bool _showMask = true; // 课前蒙层是否显示
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_parseExtra();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_loadData();
|
|
});
|
|
}
|
|
|
|
// ===================== 初始化 =====================
|
|
|
|
void _parseExtra() {
|
|
final extra = widget.extra ?? const <String, dynamic>{};
|
|
_courseId = (extra['courseId']?.toString()) ?? '';
|
|
_packId = (extra['packId']?.toString()) ?? '';
|
|
_recordId = (extra['recordId']?.toString()) ?? '';
|
|
_status = TransitionStatus.fromInt(extra['status']);
|
|
}
|
|
|
|
Future<void> _loadData() async {
|
|
setState(() => _loading = true);
|
|
try {
|
|
if (_recordId.isEmpty) {
|
|
throw Exception('recordId 无效');
|
|
}
|
|
final info = await BeforeDuringClassApi.instance.getRecord(_recordId);
|
|
if (!mounted) return;
|
|
setState(() => _overcomeInfo = info);
|
|
} catch (e) {
|
|
debugPrint('[TransitionPage] 加载失败: $e');
|
|
// 失败时回退到攻克列表页
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('获取攻克信息失败:${e.toString()}'),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
context.go('/conquerPackage/captureList', extra: _courseId);
|
|
}
|
|
} finally {
|
|
if (mounted) setState(() => _loading = false);
|
|
}
|
|
}
|
|
|
|
// ===================== 点击开始学习 =====================
|
|
|
|
void _startLearning() {
|
|
if (_status == TransitionStatus.beforeClass ||
|
|
_status == TransitionStatus.duringClass) {
|
|
context.push(
|
|
'/conquerPackage/beforeDuringClass',
|
|
extra: {
|
|
'status': '${_status.value}',
|
|
'courseId': _courseId,
|
|
'packId': _packId,
|
|
'recordId': _recordId,
|
|
},
|
|
);
|
|
} else if (_status == TransitionStatus.afterClass) {
|
|
if (_overcomeInfo.hasQuestion) {
|
|
// TODO(路由): classExercises 页建好后替换
|
|
// context.push('/conquerPackage/classExercises', extra: {
|
|
// 'courseId': _courseId,
|
|
// 'packId': _packId,
|
|
// 'recordId': _recordId,
|
|
// });
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('习题页待开发'),
|
|
behavior: SnackBarBehavior.floating,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===================== Build =====================
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: _bgColor,
|
|
body: Stack(
|
|
children: [
|
|
SafeArea(
|
|
child: Column(
|
|
children: [
|
|
_buildNavBar(),
|
|
Expanded(
|
|
child: _loading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: _buildBody(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// 课前引导蒙层
|
|
if (_status == TransitionStatus.beforeClass && _showMask)
|
|
_SlideMask(
|
|
visible: _showMask,
|
|
onClose: () => setState(() => _showMask = false),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ===================== NavBar =====================
|
|
|
|
Widget _buildNavBar() {
|
|
return SizedBox(
|
|
height: 44.h,
|
|
child: Stack(
|
|
children: [
|
|
Positioned(
|
|
left: 16.w,
|
|
top: 0,
|
|
bottom: 0,
|
|
child: IconButton(
|
|
icon: const Icon(Icons.arrow_back_ios,
|
|
color: Color.fromRGBO(0, 0, 0, 0.8), size: 20),
|
|
onPressed: () => Navigator.of(context).maybePop(),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ===================== Body =====================
|
|
|
|
Widget _buildBody() {
|
|
switch (_status) {
|
|
case TransitionStatus.beforeClass:
|
|
return _buildBeforeClass();
|
|
case TransitionStatus.duringClass:
|
|
return _buildDuringClass();
|
|
case TransitionStatus.afterClass:
|
|
return _buildAfterClass();
|
|
}
|
|
}
|
|
|
|
// ---------- 课前讲义 ----------
|
|
|
|
Widget _buildBeforeClass() {
|
|
return Column(
|
|
children: [
|
|
// 背景插图
|
|
CachedNetworkImage(
|
|
imageUrl: ImgUrl.build(
|
|
'/StaticResource/2026-04-01/before-class-bac.png',
|
|
),
|
|
width: 170.w,
|
|
height: 149.h,
|
|
fit: BoxFit.contain,
|
|
placeholder: (_, __) => SizedBox.shrink(),
|
|
errorWidget: (_, __, ___) => SizedBox(
|
|
width: 170.w,
|
|
height: 149.h,
|
|
child: const Icon(Icons.menu_book_outlined,
|
|
size: 80, color: _primary),
|
|
),
|
|
),
|
|
SizedBox(height: 50.h),
|
|
// 标签
|
|
_buildLabel('课前讲义'),
|
|
SizedBox(height: 12.h),
|
|
// 标题
|
|
_buildTitle(
|
|
name: _overcomeInfo.name,
|
|
subtitle: '知识点讲义学习',
|
|
),
|
|
const Spacer(),
|
|
// 开始学习按钮
|
|
_buildStartButton('开始学习'),
|
|
SizedBox(height: 46.h),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ---------- 课中视频 ----------
|
|
|
|
Widget _buildDuringClass() {
|
|
return Column(
|
|
children: [
|
|
// 背景插图
|
|
CachedNetworkImage(
|
|
imageUrl: ImgUrl.build(
|
|
'/StaticResource/2026-04-01/during-class-bac.png',
|
|
),
|
|
width: 186.w,
|
|
height: 120.h,
|
|
fit: BoxFit.contain,
|
|
placeholder: (_, __) => SizedBox.shrink(),
|
|
errorWidget: (_, __, ___) => SizedBox(
|
|
width: 186.w,
|
|
height: 120.h,
|
|
child:
|
|
const Icon(Icons.play_circle_outline, size: 80, color: _primary),
|
|
),
|
|
),
|
|
SizedBox(height: 42.h),
|
|
_buildLabel('课中视频'),
|
|
SizedBox(height: 12.h),
|
|
_buildTitle(
|
|
name: _overcomeInfo.name,
|
|
subtitle: '知识点视频学习',
|
|
),
|
|
const Spacer(),
|
|
_buildStartButton('开始学习'),
|
|
SizedBox(height: 46.h),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ---------- 课后练习 ----------
|
|
|
|
Widget _buildAfterClass() {
|
|
return SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
// 背景插图
|
|
CachedNetworkImage(
|
|
imageUrl: ImgUrl.build(
|
|
'/StaticResource/2026-04-01/after-class-bac.png',
|
|
),
|
|
width: 170.w,
|
|
height: 170.h,
|
|
fit: BoxFit.contain,
|
|
placeholder: (_, __) => SizedBox.shrink(),
|
|
errorWidget: (_, __, ___) => SizedBox(
|
|
width: 170.w,
|
|
height: 170.h,
|
|
child:
|
|
const Icon(Icons.assignment_outlined, size: 80, color: _primary),
|
|
),
|
|
),
|
|
SizedBox(height: 25.h),
|
|
_buildLabel('课后练习'),
|
|
SizedBox(height: 12.h),
|
|
if (_overcomeInfo.name.isNotEmpty)
|
|
_buildTitle(
|
|
name: _overcomeInfo.name,
|
|
subtitle: '知识点题目攻克',
|
|
padding: EdgeInsets.symmetric(horizontal: 5.w),
|
|
),
|
|
SizedBox(height: 28.h),
|
|
// 提示卡片
|
|
_buildAfterTipsCard(),
|
|
SizedBox(height: 28.h),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ===================== 公共组件 =====================
|
|
|
|
Widget _buildLabel(String text) {
|
|
return Container(
|
|
width: 60.w,
|
|
height: 20.h,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: _labelBg,
|
|
borderRadius: BorderRadius.circular(4.r),
|
|
),
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
color: _primary,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTitle({
|
|
required String name,
|
|
required String subtitle,
|
|
EdgeInsetsGeometry? padding,
|
|
}) {
|
|
return Padding(
|
|
padding: padding ?? EdgeInsets.zero,
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
name.isNotEmpty ? '《$name》' : '',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 18.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: _titleColor,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
SizedBox(height: 4.h),
|
|
Text(
|
|
subtitle,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 18.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: _titleColor,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStartButton(String text) {
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 14.w),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
height: 47.h,
|
|
child: FilledButton(
|
|
onPressed: _startLearning,
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: _primary,
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20.r),
|
|
),
|
|
textStyle: TextStyle(
|
|
fontSize: 18.sp,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
child: Text(text),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ===================== 课后提示卡片 =====================
|
|
|
|
Widget _buildAfterTipsCard() {
|
|
return Center(
|
|
child: Container(
|
|
margin: EdgeInsets.symmetric(horizontal: 14.w),
|
|
constraints: BoxConstraints(maxWidth: 347.w),
|
|
width: double.infinity,
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
// 卡片实际宽度(平板上被 maxWidth 限制,手机上 = 屏宽 - 左右边距)
|
|
final cardW = constraints.maxWidth;
|
|
// 设计稿卡片宽度 = 750rpx - 28rpx*2 = 694rpx = 347dp
|
|
// 内部所有水平/垂直定位都按卡片宽度比例换算,
|
|
// 保证背景图(fitWidth)和内部定位永远同步
|
|
double wRatio(double designPx) => cardW * designPx / 347.0;
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF2F7FF),
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
image: DecorationImage(
|
|
image: NetworkImage(
|
|
ImgUrl.build(
|
|
'/StaticResource/2026-04-01/after-class-tips.png',
|
|
),
|
|
),
|
|
fit: BoxFit.fitWidth,
|
|
alignment: Alignment.topCenter,
|
|
),
|
|
),
|
|
padding: EdgeInsets.fromLTRB(
|
|
wRatio(14),
|
|
wRatio(129), // 背景图随宽度缩放,padding-top 也按卡片宽比例
|
|
wRatio(14),
|
|
14.h,
|
|
),
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
// 攻克标准百分比(按卡片宽度比例定位,与背景图缩放同步)
|
|
Positioned(
|
|
right: wRatio(75.5),
|
|
top: -wRatio(64),
|
|
child: Text(
|
|
'${_overcomeInfo.overcomeStandard}%',
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: _primary,
|
|
),
|
|
),
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildTipItem('包含单选、多选、判断、填空全题型'),
|
|
SizedBox(height: 4.h),
|
|
_buildTipItem('每题展示深度解析与解题逻辑'),
|
|
SizedBox(height: 15.h),
|
|
// 正式进入攻克按钮
|
|
Opacity(
|
|
opacity: _overcomeInfo.hasQuestion ? 1.0 : 0.6,
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
height: 47.h,
|
|
child: FilledButton(
|
|
onPressed: _overcomeInfo.hasQuestion
|
|
? _startLearning
|
|
: null,
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: _primary,
|
|
foregroundColor: Colors.white,
|
|
disabledBackgroundColor: _primary,
|
|
disabledForegroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20.r),
|
|
),
|
|
textStyle: TextStyle(
|
|
fontSize: 18.sp,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
child: const Text('正式进入攻克'),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTipItem(String text) {
|
|
return Padding(
|
|
padding: EdgeInsets.only(left: 24.w, bottom: 4.h),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: 4.w,
|
|
height: 4.w,
|
|
margin: EdgeInsets.only(right: 13.w),
|
|
decoration: const BoxDecoration(
|
|
color: _primary,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
color: _subtitleColor,
|
|
height: 1.5,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// ============================================================
|
|
/// 【_SlideMask】滑动蒙层引导组件
|
|
/// 对标 Vue pages/conquerPackage/transitionPage/slide-mask.vue
|
|
///
|
|
/// 功能:
|
|
/// - 半透明黑色蒙层背景
|
|
/// - 显示引导图
|
|
/// - 向左滑动超过阈值 → 整个蒙层滑出屏幕并触发 onClose
|
|
/// - 滑动不足阈值 → 回弹
|
|
/// ============================================================
|
|
class _SlideMask extends StatefulWidget {
|
|
const _SlideMask({
|
|
required this.visible,
|
|
required this.onClose,
|
|
this.slideThreshold = 80.0,
|
|
});
|
|
|
|
final bool visible;
|
|
final VoidCallback onClose;
|
|
final double slideThreshold;
|
|
|
|
@override
|
|
State<_SlideMask> createState() => _SlideMaskState();
|
|
}
|
|
|
|
class _SlideMaskState extends State<_SlideMask>
|
|
with SingleTickerProviderStateMixin {
|
|
double _offsetX = 0;
|
|
double _startX = 0;
|
|
bool _isSliding = false;
|
|
bool _isAnimatingOut = false;
|
|
late final AnimationController _slideOutController;
|
|
late Animation<double> _slideOutAnim;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_slideOutController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 200),
|
|
);
|
|
_slideOutAnim = Tween<double>(begin: 0, end: 0).animate(
|
|
CurvedAnimation(parent: _slideOutController, curve: Curves.easeInOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_slideOutController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant _SlideMask oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (widget.visible && !oldWidget.visible) {
|
|
_offsetX = 0;
|
|
_isAnimatingOut = false;
|
|
_slideOutController.reset();
|
|
}
|
|
}
|
|
|
|
void _handleTouchStart(DragStartDetails details) {
|
|
if (!widget.visible || _isAnimatingOut) return;
|
|
_startX = details.globalPosition.dx;
|
|
_isSliding = true;
|
|
}
|
|
|
|
void _handleTouchUpdate(DragUpdateDetails details) {
|
|
if (!widget.visible || !_isSliding || _isAnimatingOut) return;
|
|
final moveX = details.globalPosition.dx;
|
|
final diffX = _startX - moveX;
|
|
// 仅处理向左滑动
|
|
if (diffX > 0) {
|
|
setState(() => _offsetX = -diffX);
|
|
}
|
|
}
|
|
|
|
void _handleTouchEnd(DragEndDetails details) {
|
|
if (!widget.visible || !_isSliding) return;
|
|
_isSliding = false;
|
|
|
|
if (_offsetX.abs() >= widget.slideThreshold) {
|
|
// 超过阈值 → 滑出动画
|
|
_startSlideOut();
|
|
} else {
|
|
// 回弹
|
|
setState(() => _offsetX = 0);
|
|
}
|
|
}
|
|
|
|
void _startSlideOut() {
|
|
_isAnimatingOut = true;
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
_slideOutAnim = Tween<double>(begin: _offsetX, end: -screenWidth).animate(
|
|
CurvedAnimation(parent: _slideOutController, curve: Curves.easeInOut),
|
|
);
|
|
_slideOutController.forward().then((_) {
|
|
widget.onClose();
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!widget.visible) return const SizedBox.shrink();
|
|
|
|
return AnimatedBuilder(
|
|
animation: _slideOutAnim,
|
|
builder: (context, child) {
|
|
final dx = _isAnimatingOut ? _slideOutAnim.value : _offsetX;
|
|
return Positioned.fill(
|
|
child: GestureDetector(
|
|
onHorizontalDragStart: _handleTouchStart,
|
|
onHorizontalDragUpdate: _handleTouchUpdate,
|
|
onHorizontalDragEnd: _handleTouchEnd,
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Transform.translate(
|
|
offset: Offset(dx, 0),
|
|
child: Container(
|
|
color: const Color(0x8C000000), // 55% 透明黑
|
|
child: Stack(
|
|
children: [
|
|
// 引导图
|
|
Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 187.h,
|
|
child: Center(
|
|
child: CachedNetworkImage(
|
|
imageUrl: ImgUrl.build(
|
|
'/StaticResource/2026-04-01/guidance.png',
|
|
),
|
|
width: 244.5.w,
|
|
height: 122.5.h,
|
|
fit: BoxFit.contain,
|
|
placeholder: (_, __) => SizedBox.shrink(),
|
|
errorWidget: (_, __, ___) => SizedBox(
|
|
width: 244.5.w,
|
|
height: 122.5.h,
|
|
child: const Icon(
|
|
Icons.swipe_left,
|
|
color: Colors.white,
|
|
size: 60,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|