Files
2026-09-28 20:12:34 +08:00

124 lines
3.9 KiB
Dart
Raw Permalink 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.
/// ============================================================
/// 【课堂习题 · 顶部计时与暂停联动】
///
/// 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();
}
}