124 lines
3.9 KiB
Dart
124 lines
3.9 KiB
Dart
/// ============================================================
|
||
/// 【课堂习题 · 顶部计时与暂停联动】
|
||
///
|
||
/// 1:1 迁移 Vue exerciseTiming.js(createTimingHandlers):
|
||
/// - 顶部计时(可暂停 / 继续),与倒计时组件联动;
|
||
/// - 按 recordId 缓存 / 恢复计时进度(实际逻辑按题目 answerTimeSecond
|
||
/// 汇总,SP 缓存代码在 Vue 端已注释,这里仍保留 persist 写缓存);
|
||
/// - 暂停时统计剩余未答题数,驱动暂停弹窗。
|
||
/// ============================================================
|
||
library;
|
||
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
import 'package:new_learn/features/conquerPackage/classExercises/controllers/exercise_session.dart';
|
||
import 'package:new_learn/features/conquerPackage/classExercises/controllers/exercise_submit.dart';
|
||
import 'package:new_learn/features/conquerPackage/classExercises/controllers/exercise_timing.dart';
|
||
|
||
class ExerciseTimingHandlers {
|
||
ExerciseTimingHandlers({
|
||
required this.session,
|
||
required this.bridge,
|
||
required this.submit,
|
||
required this.isPausePopupVisible,
|
||
this.onUiTick,
|
||
}) {
|
||
timing = ExerciseTiming(onTick: (tick) {
|
||
final total = timingOffsetMs + tick.elapsedMs;
|
||
timingText = formatElapsed(total);
|
||
timingRunning = tick.running;
|
||
// 页面层据此刷新顶部计时胶囊(对标 Vue ref 赋值触发渲染)
|
||
onUiTick?.call();
|
||
});
|
||
}
|
||
|
||
final ExerciseSession session;
|
||
final ExerciseUiBridge bridge;
|
||
final ExerciseSubmitHandlers submit;
|
||
|
||
/// 每次秒表 tick 后回调页面刷新 UI(计时文案)
|
||
final void Function()? onUiTick;
|
||
|
||
/// 暂停弹窗当前是否展示(对标 showPausePopup.value 读取)
|
||
final bool Function() isPausePopupVisible;
|
||
|
||
late final ExerciseTiming timing;
|
||
|
||
/// 顶部计时文案
|
||
String timingText = '00:00';
|
||
|
||
/// 计时是否运行中
|
||
bool timingRunning = true;
|
||
|
||
/// 累计偏移(恢复的已学时长)
|
||
int timingOffsetMs = 0;
|
||
|
||
/// 暂停时剩余未答题数
|
||
int remainingCount = 0;
|
||
|
||
/// 恢复计时缓存(Vue 端实际使用题目 answerTimeSecond 汇总)
|
||
void restoreTimingCache() {
|
||
try {
|
||
var totalStudyTime = 0;
|
||
for (final item in session.questions) {
|
||
totalStudyTime += item.answerTimeSecond;
|
||
}
|
||
if (totalStudyTime != 0) {
|
||
timingOffsetMs = totalStudyTime * 1000;
|
||
timingText = formatElapsed(totalStudyTime * 1000);
|
||
} else {
|
||
timingOffsetMs = 0;
|
||
timingText = '00:00';
|
||
}
|
||
} catch (_) {
|
||
timingOffsetMs = 0;
|
||
timingText = '00:00';
|
||
}
|
||
}
|
||
|
||
/// 持久化计时进度(view 模式 / 无 recordId 不写)
|
||
Future<void> persistTimingCache() async {
|
||
final keyId = session.recordId;
|
||
if (session.isView || keyId.isEmpty) return;
|
||
final elapsedMs = timingOffsetMs + timing.getElapsedMs();
|
||
try {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setString(
|
||
'classExercises_timing_$keyId',
|
||
// 与 Vue 一致:历史上存的是 JSON 字符串
|
||
'{"elapsedMs":$elapsedMs}',
|
||
);
|
||
} catch (_) {
|
||
// 忽略落盘失败
|
||
}
|
||
}
|
||
|
||
/// 切换计时(对标 toggleTiming)
|
||
void toggleTiming() {
|
||
if (isPausePopupVisible()) return;
|
||
if (timing.isRunning()) {
|
||
timing.pause();
|
||
session.countDownKey?.currentState?.pause();
|
||
bridge.setPausePopupVisible(true);
|
||
|
||
final list = submit.batchProcessQuestions();
|
||
remainingCount =
|
||
list.where((res) => res['answered'] != true).length;
|
||
bridge.notifyListeners();
|
||
return;
|
||
}
|
||
timing.resume();
|
||
session.countDownKey?.currentState?.start();
|
||
}
|
||
|
||
/// 关闭暂停弹窗并继续(对标 handlePausePopupClose)
|
||
void handlePausePopupClose() {
|
||
bridge.setPausePopupVisible(false);
|
||
if (!timing.isRunning()) timing.resume();
|
||
session.countDownKey?.currentState?.start();
|
||
}
|
||
|
||
void dispose() {
|
||
timing.stop();
|
||
}
|
||
}
|