更新
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/// ============================================================
|
||||
/// 【课堂习题 · 计时】(1:1 迁移 Vue timing.js / exerciseTiming.js)
|
||||
///
|
||||
/// - formatElapsed / formatTime / formatDuration 与 Vue 纯函数一致;
|
||||
/// - ExerciseTiming 对应 createTiming(每秒一次 onTick);
|
||||
/// - getDisplayPlanTimeSecond 对应 exerciseTiming.js 的同名导出:
|
||||
/// 优先用本地缓存的剩余秒数 NewPlanTimeSecond,否则用
|
||||
/// planTimeSecond - answerTimeSecond;<=0 时置 ISFinishPlanTime=true。
|
||||
/// ============================================================
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:new_learn/features/conquerPackage/classExercises/models/question_model.dart';
|
||||
|
||||
String _pad2(int n) => n < 10 ? '0$n' : '$n';
|
||||
|
||||
/// mm:ss(不足 1 小时)/ hh:mm:ss(达到 1 小时)
|
||||
String formatElapsed(int ms) {
|
||||
final safeMs = ms < 0 ? 0 : ms;
|
||||
final totalSeconds = safeMs ~/ 1000;
|
||||
|
||||
if (totalSeconds < 3600) {
|
||||
final mm = totalSeconds ~/ 60;
|
||||
final ss = totalSeconds % 60;
|
||||
return '${_pad2(mm)}:${_pad2(ss)}';
|
||||
}
|
||||
|
||||
final hh = totalSeconds ~/ 3600;
|
||||
final mm = (totalSeconds % 3600) ~/ 60;
|
||||
final ss = totalSeconds % 60;
|
||||
return '${_pad2(hh)}:${_pad2(mm)}:${_pad2(ss)}';
|
||||
}
|
||||
|
||||
/// 建议学习时长文案:N秒 / N分钟 / N小时 / Nd
|
||||
String formatTime(int seconds) {
|
||||
final t = seconds < 0 ? 0 : seconds;
|
||||
if (t < 60) return '$t秒';
|
||||
if (t < 3600) return '${t ~/ 60}分钟';
|
||||
if (t < 86400) return '${t ~/ 3600}小时';
|
||||
return '${t ~/ 86400}d';
|
||||
}
|
||||
|
||||
/// 已学习时长文案:N秒 / N分钟N秒 / N小时N分钟 / Nd
|
||||
String formatDuration(int seconds) {
|
||||
final t = seconds < 0 ? 0 : seconds;
|
||||
if (t < 60) return '$t秒';
|
||||
if (t < 3600) {
|
||||
final m = t ~/ 60;
|
||||
final s = t % 60;
|
||||
return '$m分钟$s秒';
|
||||
}
|
||||
if (t < 86400) {
|
||||
final h = t ~/ 3600;
|
||||
final m = (t % 3600) ~/ 60;
|
||||
return '$h小时$m分钟';
|
||||
}
|
||||
return '${t ~/ 86400}d';
|
||||
}
|
||||
|
||||
/// 计时回调载荷(对标 JS {elapsedMs,text,running})
|
||||
class TimingTick {
|
||||
const TimingTick({
|
||||
required this.elapsedMs,
|
||||
required this.text,
|
||||
required this.running,
|
||||
});
|
||||
|
||||
final int elapsedMs;
|
||||
final String text;
|
||||
final bool running;
|
||||
}
|
||||
|
||||
/// 页面计时器(对应 createTiming)。
|
||||
///
|
||||
/// 注意与 JS 保持一致的几个细节:
|
||||
/// - pause() 不取消周期定时器,仅冻结累计值(running=false),
|
||||
/// 周期回调继续按冻结值触发;
|
||||
/// - start() 已在计时时直接返回;
|
||||
/// - stop() 才真正取消定时器;
|
||||
/// - reset() 不清运行状态,只把累计值归零。
|
||||
class ExerciseTiming {
|
||||
ExerciseTiming({this.onTick, this.intervalMs = 1000});
|
||||
|
||||
final void Function(TimingTick tick)? onTick;
|
||||
final int intervalMs;
|
||||
|
||||
Timer? _timer;
|
||||
bool _running = false;
|
||||
int _lastStartAt = 0;
|
||||
int _elapsedMs = 0;
|
||||
|
||||
void _emit() {
|
||||
final cb = onTick;
|
||||
if (cb == null) return;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final current = _running ? _elapsedMs + (now - _lastStartAt) : _elapsedMs;
|
||||
cb(TimingTick(
|
||||
elapsedMs: current,
|
||||
text: formatElapsed(current),
|
||||
running: _running,
|
||||
));
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (_timer != null) return;
|
||||
_running = true;
|
||||
_lastStartAt = DateTime.now().millisecondsSinceEpoch;
|
||||
_emit();
|
||||
_timer = Timer.periodic(Duration(milliseconds: intervalMs), (_) => _emit());
|
||||
}
|
||||
|
||||
void pause() {
|
||||
if (!_running) return;
|
||||
_elapsedMs += DateTime.now().millisecondsSinceEpoch - _lastStartAt;
|
||||
_running = false;
|
||||
_emit();
|
||||
}
|
||||
|
||||
void resume() {
|
||||
if (_running) return;
|
||||
_running = true;
|
||||
_lastStartAt = DateTime.now().millisecondsSinceEpoch;
|
||||
_emit();
|
||||
}
|
||||
|
||||
void toggle() {
|
||||
if (_running) {
|
||||
pause();
|
||||
} else {
|
||||
resume();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_running = false;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_elapsedMs = 0;
|
||||
_lastStartAt = DateTime.now().millisecondsSinceEpoch;
|
||||
_emit();
|
||||
}
|
||||
|
||||
int getElapsedMs() {
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
return _running ? _elapsedMs + (now - _lastStartAt) : _elapsedMs;
|
||||
}
|
||||
|
||||
bool isRunning() => _running;
|
||||
}
|
||||
|
||||
/// 单题倒计时展示秒数(对应 getDisplayPlanTimeSecond)。
|
||||
///
|
||||
/// NewPlanTimeSecond 是切题/暂停前本地缓存的“剩余秒数”,不是总时长;
|
||||
/// 首次进入题目没有缓存时,用总时长 - 后端累计作答时长。
|
||||
/// 结果 <=0 时直接视为倒计时结束并回写 ISFinishPlanTime。
|
||||
int getDisplayPlanTimeSecond(QuestionModel? q) {
|
||||
if (q == null) return 0;
|
||||
final hasLocalRemain = q.m['NewPlanTimeSecond'] != null;
|
||||
final remain = hasLocalRemain
|
||||
? (q.newPlanTimeSecond ?? 0)
|
||||
: q.planTimeSecond - q.answerTimeSecond;
|
||||
if (remain <= 0) {
|
||||
q.isFinishPlanTime = true;
|
||||
return 0;
|
||||
}
|
||||
return remain;
|
||||
}
|
||||
Reference in New Issue
Block a user