-
Notifications
You must be signed in to change notification settings - Fork 48
feat: 主课表页面体育课程支持地点查看 #180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import 'package:signals/signals.dart'; | |
| import 'package:watermeter/controller/custom_class_controller.dart'; | ||
| import 'package:watermeter/controller/global_timer_controller.dart'; | ||
| import 'package:watermeter/controller/semester_controller.dart'; | ||
| import 'package:watermeter/controller/sport_controller.dart'; | ||
| import 'package:watermeter/controller/week_swift_controller.dart'; | ||
| import 'package:watermeter/model/fetch_result.dart'; | ||
| import 'package:watermeter/model/home_arrangement.dart'; | ||
|
|
@@ -91,19 +92,79 @@ class ClassTableController { | |
| ); | ||
|
|
||
| late final classTableComputedSignal = computed<ClassTableData>(() { | ||
| // 第一步:获取教务课表与体育课程。computed 内同时读取两个信号, | ||
| // 体育课程刷新后课表教室会自动重算。 | ||
| final networkClassTable = schoolClassTableComputedSignal.value; | ||
| final sportClasses = SportController.i.sportClassesComputedSignal.value; | ||
|
|
||
| // 第二步-a:按(学期、教师、星期、起止节次)汇总体育上课地点, | ||
| // 同一语义键对应多个地点时保留为集合,唯一匹配才回填。 | ||
| final sportLocations = <(String, String, int, int, int), Set<String>>{}; | ||
| for (final sportClass in sportClasses) { | ||
| final place = sportClass.place.trim(); | ||
| if (sportClass.term != networkClassTable.semesterCode || | ||
| sportClass.teacher.isEmpty || | ||
| place.isEmpty) { | ||
| continue; | ||
| } | ||
| sportLocations | ||
| .putIfAbsent(( | ||
| sportClass.term, | ||
| sportClass.teacher, | ||
| sportClass.week, | ||
| sportClass.start, | ||
| sportClass.stop, | ||
| ), () => <String>{}) | ||
| .add(place); | ||
| } | ||
|
|
||
| // 第二步-b:拷贝一份教务时间安排,只改 classroom,不污染缓存原对象。 | ||
| final timeArrangement = networkClassTable.timeArrangement.map(( | ||
| arrangement, | ||
| ) { | ||
| return TimeArrangement( | ||
| source: arrangement.source, | ||
| index: arrangement.index, | ||
| weekList: List<bool>.from(arrangement.weekList), | ||
| classroom: arrangement.classroom, | ||
| teacher: arrangement.teacher, | ||
| day: arrangement.day, | ||
| start: arrangement.start, | ||
| stop: arrangement.stop, | ||
| ); | ||
| }).toList(); | ||
|
|
||
| for (final arrangement in timeArrangement) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 接着上个,就这里的,timeArrangement 直接改成 networkClassTable.timeArrangement ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个也试了,直接修改在 computed 中可以执行,也没有出现递归触发,但是原始 signal 中的 TimeArrangement 也会被一起修改。运行失败倒是没有,不过会产生 computed 副作用并污染源数据/缓存 |
||
| // 仅处理教务来源、教室为空且索引合法的安排,已有教室不覆盖。 | ||
| if (arrangement.source != Source.school || | ||
| arrangement.classroom?.trim().isNotEmpty == true || | ||
| arrangement.index < 0 || | ||
| arrangement.index >= networkClassTable.classDetail.length) { | ||
| continue; | ||
| } | ||
| final matchedLocations = | ||
| sportLocations[( | ||
| networkClassTable.semesterCode, | ||
| arrangement.teacher ?? '', | ||
| arrangement.day, | ||
| arrangement.start, | ||
| arrangement.stop, | ||
| )]; | ||
| if (matchedLocations?.length == 1) { | ||
| arrangement.classroom = matchedLocations!.single; | ||
| } | ||
| } | ||
|
|
||
| // 第二步-c:输出全新的 ClassTableData。 | ||
| return ClassTableData( | ||
| semesterLength: networkClassTable.semesterLength, | ||
| semesterCode: networkClassTable.semesterCode, | ||
| termStartDay: networkClassTable.termStartDay, | ||
| classDetail: List<ClassDetail>.from(networkClassTable.classDetail), | ||
| notArranged: List<NotArrangementClassDetail>.from( | ||
| networkClassTable.notArranged, | ||
| ), | ||
| timeArrangement: List<TimeArrangement>.from( | ||
| networkClassTable.timeArrangement, | ||
| ), | ||
| classDetail: networkClassTable.classDetail.map(ClassDetail.from).toList(), | ||
| notArranged: networkClassTable.notArranged | ||
| .map(NotArrangementClassDetail.from) | ||
| .toList(), | ||
| timeArrangement: timeArrangement, | ||
| classChanges: List<ClassChange>.from(networkClassTable.classChanges), | ||
| ); | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // Copyright 2025 Traintime PDA authors. | ||
| // SPDX-License-Identifier: MPL-2.0 | ||
|
|
||
| import 'package:signals/signals.dart'; | ||
| import 'package:watermeter/model/fetch_result.dart'; | ||
| import 'package:watermeter/model/xidian_sport/sport_class.dart'; | ||
| import 'package:watermeter/repository/logger.dart'; | ||
| import 'package:watermeter/repository/miscellaneous_session/xidian_sport_session.dart'; | ||
| import 'package:watermeter/repository/single_flight.dart'; | ||
|
|
||
| class SportController { | ||
| static final SportController i = SportController._(); | ||
|
|
||
| SportController._(); | ||
|
|
||
| final SportSession session = SportSession(); | ||
| final _classFlight = SingleFlight<FetchResult<SportClass>>(); | ||
| final _lastValidClass = signal<FetchResult<SportClass>?>(null); | ||
|
|
||
| final sportClassStateSignal = signal<AsyncState<FetchResult<SportClass>>>( | ||
| const AsyncLoading(), | ||
| ); | ||
|
|
||
| late final sportClassesComputedSignal = computed<SportClass>( | ||
| () => _lastValidClass.value?.data ?? const <SportClassItem>[], | ||
| ); | ||
|
|
||
| Future<FetchResult<SportClass>> reloadClass() { | ||
| final previous = _lastValidClass.value; | ||
| sportClassStateSignal.value = previous != null | ||
| ? AsyncState.dataRefreshing(previous) | ||
| : AsyncState.loading(); | ||
|
|
||
| return _classFlight.run(() async { | ||
| try { | ||
| final result = await session.getClass(); | ||
| _lastValidClass.value = result; | ||
| sportClassStateSignal.set(AsyncState.data(result), force: true); | ||
| return result; | ||
| } catch (e, s) { | ||
| sportClassStateSignal.value = AsyncState.error(e, s); | ||
| log.handle(e, s, '[SportController][reloadClass] Have issue'); | ||
| rethrow; | ||
| } | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| import 'package:flutter/material.dart'; | ||
| import 'package:flutter_i18n/flutter_i18n.dart'; | ||
| import 'package:styled_widget/styled_widget.dart'; | ||
| import 'package:watermeter/controller/sport_controller.dart'; | ||
| import 'package:watermeter/model/fetch_result.dart'; | ||
| import 'package:watermeter/model/xidian_sport/sport_class.dart'; | ||
| import 'package:watermeter/page/public_widget/cache_alerter.dart'; | ||
|
|
@@ -26,7 +27,7 @@ class _SportClassWindowState extends State<SportClassWindow> | |
| @override | ||
| bool get wantKeepAlive => true; | ||
|
|
||
| Future<FetchResult<SportClass>> _future = SportSession().getClass(); | ||
| Future<FetchResult<SportClass>> _future = SportController.i.reloadClass(); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 跟此PR无关,但请不用AI对体育中查体测成绩的窗口进行修改,把里面的 SportController() 改成控制器里的实现。 |
||
|
|
||
| Object? _translateError(BuildContext context, Object? error) { | ||
| if (error is SportCredentialMissingException || | ||
|
|
@@ -45,7 +46,7 @@ class _SportClassWindowState extends State<SportClassWindow> | |
| return RefreshIndicator( | ||
| onRefresh: () async { | ||
| setState(() { | ||
| _future = SportSession().getClass(); | ||
| _future = SportController.i.reloadClass(); | ||
| }); | ||
| }, | ||
| child: FutureBuilder( | ||
|
|
@@ -109,7 +110,7 @@ class _SportClassWindowState extends State<SportClassWindow> | |
| snapshot.hasError) { | ||
| return ReloadWidget( | ||
| function: () => setState(() { | ||
| _future = SportSession().getClass(); | ||
| _future = SportController.i.reloadClass(); | ||
| }), | ||
| errorStatus: _translateError(context, snapshot.error), | ||
| stackTrace: snapshot.stackTrace, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
试试:List.from(networkClassTable.timeArrangement) ?
TimeArrangement 里面的元素应该是不能更改的,所以深拷贝一个有必要。但是 List 应该也是可以改变的,所以可以试试原先数组修改?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
测试后确认了,List.from 只复制外层列表,直接修改复制列表中的 TimeArrangement.classroom 会同步修改原对象,只有替换成新的 TimeArrangement 元素时原对象才不会变化,我觉得只能逐元素复制(