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,123 @@
/// ============================================================
/// 【课堂习题 · 顶部计时与暂停联动】
///
/// 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();
}
}