From 43100cfdf8379fa08f684985c16b00c171a7b1bb Mon Sep 17 00:00:00 2001 From: w568w <1278297578@qq.com> Date: Sun, 18 Jan 2026 00:44:57 +0800 Subject: [PATCH 1/2] feat: support viewing graduate course scores (#621) --- lib/page/dashboard/exam_detail.dart | 53 ++++- .../fdu/edu_service_repository.dart | 31 +++ .../fdu/graduate_exam_repository.dart | 193 ++++++++++++++++++ 3 files changed, 273 insertions(+), 4 deletions(-) create mode 100644 lib/repository/fdu/graduate_exam_repository.dart diff --git a/lib/page/dashboard/exam_detail.dart b/lib/page/dashboard/exam_detail.dart index f683ab97..e1371f45 100644 --- a/lib/page/dashboard/exam_detail.dart +++ b/lib/page/dashboard/exam_detail.dart @@ -20,9 +20,11 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:dan_xi/generated/l10n.dart'; +import 'package:dan_xi/model/person.dart'; import 'package:dan_xi/provider/state_provider.dart' as sp; import 'package:dan_xi/repository/fdu/data_center_repository.dart'; import 'package:dan_xi/repository/fdu/edu_service_repository.dart'; +import 'package:dan_xi/repository/fdu/graduate_exam_repository.dart'; import 'package:dan_xi/util/master_detail_view.dart'; import 'package:dan_xi/util/noticing.dart'; import 'package:dan_xi/util/platform_universal.dart'; @@ -123,6 +125,13 @@ GpaListItem? userGpa(Ref ref) { ); } +/// Provider for graduate student exam scores. +/// Returns all scores across all semesters. +@riverpod +Future> graduateExamScoreList(Ref ref) async { + return await GraduateExamRepository.getInstance().loadExamScore(); +} + /// A list page showing user's GPA scores and exam information. /// It will try to fetch [SemesterInfo] first. /// If successful, it will fetch exams in this term. In case that there is no exam, it shows the score of this term. @@ -134,6 +143,12 @@ class ExamList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + // Graduate students use a simplified view without semester navigation + if (sp.StateProvider.personInfo.value?.group == + UserGroup.FUDAN_POSTGRADUATE_STUDENT) { + return _buildGraduateView(context, ref); + } + final semesterBundle = ref.watch(semesterBundleProvider); final currentSemesterIndex = useState(null); final currentExamRef = useState?>(null); @@ -174,6 +189,35 @@ class ExamList extends HookConsumerWidget { )); } + /// Build simplified view for graduate students. + /// Shows all scores without semester navigation or exam schedule. + Widget _buildGraduateView(BuildContext context, WidgetRef ref) { + final scores = ref.watch(graduateExamScoreListProvider); + + return PlatformScaffold( + iosContentBottomPadding: false, + iosContentPadding: false, + appBar: PlatformAppBarX( + title: Text(S.of(context).exam_schedule), + ), + body: SafeArea( + child: switch (scores) { + AsyncData(value: final data) => + _buildGradeLayout(context, ref, data, null, isGraduate: true), + AsyncError(:final error, :final stackTrace) => ErrorPageWidget( + errorMessage: + '${S.of(context).failed}\n\nError:\n${ErrorPageWidget.generateUserFriendlyDescription(S.of(context), error)}', + error: error, + trace: stackTrace, + onTap: () => ref.invalidate(graduateExamScoreListProvider), + buttonText: S.of(context).retry, + ), + _ => Center(child: PlatformCircularProgressIndicator()), + }, + ), + ); + } + Widget _loadExamGradeHybridView(BuildContext context, WidgetRef ref, List semesters, ValueNotifier currentSemesterIndex, {ValueNotifier?>? currentExamRef}) { @@ -304,9 +348,9 @@ class ExamList extends HookConsumerWidget { Widget _buildGradeLayout(BuildContext context, WidgetRef ref, List examScores, String? semesterId, - {bool isFallback = false}) => + {bool isFallback = false, bool isGraduate = false}) => _buildRefreshableListView(context, ref, _getListWidgetsGrade( - context, ref, examScores, isFallback: isFallback),); + context, ref, examScores, isFallback: isFallback, isGraduate: isGraduate),); Widget _buildHybridLayout(BuildContext context, WidgetRef ref, List exams, List scores, String? semesterId) => @@ -360,7 +404,7 @@ class ExamList extends HookConsumerWidget { } List _getListWidgetsGrade(BuildContext context, WidgetRef ref, - List scores, {bool isFallback = false}) { + List scores, {bool isFallback = false, bool isGraduate = false}) { Widget buildLimitedCard() => Card( color: Theme.of(context).colorScheme.error, child: ListTile( @@ -376,7 +420,8 @@ class ExamList extends HookConsumerWidget { List widgets = []; if (isFallback) { widgets.add(buildLimitedCard()); - } else { + } else if (!isGraduate) { + // Only show GPA card for undergraduate students widgets.add(_buildGpaCard(context, ref)); } for (var value in scores) { diff --git a/lib/repository/fdu/edu_service_repository.dart b/lib/repository/fdu/edu_service_repository.dart index 03aac59d..3b1cd780 100644 --- a/lib/repository/fdu/edu_service_repository.dart +++ b/lib/repository/fdu/edu_service_repository.dart @@ -476,6 +476,37 @@ class ExamScore { elements[5].text.trim(), null); } + + /// Parse from graduate student score JSON. + /// + /// Expected JSON format: + /// ```json + /// { + /// "KCMC": "Course Name", + /// "KCDM": "Course Code", + /// "XF": 2.0, + /// "KCLBMC": "Course Type", + /// "CJ": "A", + /// "JDZ": 4.0 + /// } + /// ``` + factory ExamScore.fromGraduateJson(Map json) { + final String id = json['KCDM']?.toString() ?? ''; + final String name = json['KCMC']?.toString() ?? ''; + final String type = json['KCLBMC']?.toString() ?? ''; + final num? credit = json['XF']; + final String level = json['CJ']?.toString() ?? ''; + final num? gpa = json['JDZ']; + + return ExamScore( + id, + name, + type, + credit?.toString() ?? '', + level, + gpa?.toString(), + ); + } } class GpaListItem { diff --git a/lib/repository/fdu/graduate_exam_repository.dart b/lib/repository/fdu/graduate_exam_repository.dart new file mode 100644 index 00000000..da711d77 --- /dev/null +++ b/lib/repository/fdu/graduate_exam_repository.dart @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2025 DanXi-Dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import 'package:dan_xi/repository/base_repository.dart'; +import 'package:dan_xi/repository/fdu/edu_service_repository.dart'; +import 'package:dio/dio.dart'; + +import 'neo_login_tool.dart'; + +/// Repository for graduate student exam/score data. +/// +/// This repository fetches data from the graduate student system. +/// +/// ## API Details +/// +/// ### Get Semesters +/// URL: https://zlapp.fudan.edu.cn/fudanyjskb/wap/default/get-index +/// +/// Response: +/// ```json +/// { +/// "e": 0, +/// "m": "", +/// "d": { +/// "params": { +/// "year": "2023-2024", +/// "term": "2", +/// "startday": "2024-02-26", +/// "countweek": 18, +/// "week": 4 +/// }, +/// "termInfo": [ +/// { +/// "year": "2024-2025", +/// "term": "2", +/// "startday": "2025-03-01", +/// "countweek": 18 +/// } +/// ] +/// } +/// } +/// ``` +/// +/// ### Get Scores +/// URL: https://yzsfwapp.fudan.edu.cn/gsapp/sys/wdcjapp/modules/xscjcx/jdjscjcx.do +/// Login URL: https://yzsfwapp.fudan.edu.cn/gsapp/sys/wdcjapp/*default/index.do +/// +/// Response: +/// ```json +/// { +/// "code": "0", +/// "datas": { +/// "jdjscjcx": { +/// "totalSize": 8, +/// "pageSize": 999, +/// "rows": [ +/// { +/// "KCMC": "Course Name", +/// "KCDM": "Course Code", +/// "XF": 2.0, +/// "CZRXM": "Teacher", +/// "KCLBMC": "Course Type", +/// "CJ": "A", +/// "JDZ": 4.0 +/// } +/// ] +/// } +/// } +/// } +/// ``` +class GraduateExamRepository extends BaseRepositoryWithDio { + static const String _SEMESTER_URL = + 'https://zlapp.fudan.edu.cn/fudanyjskb/wap/default/get-index'; + + static const String _SCORE_URL = + 'https://yzsfwapp.fudan.edu.cn/gsapp/sys/wdcjapp/modules/xscjcx/jdjscjcx.do'; + + static const String _SCORE_LOGIN_URL = + 'https://yzsfwapp.fudan.edu.cn/gsapp/sys/wdcjapp/*default/index.do'; + + @override + String get linkHost => "fudan.edu.cn"; + + GraduateExamRepository._(); + + static final _instance = GraduateExamRepository._(); + + factory GraduateExamRepository.getInstance() => _instance; + + /// Load all semesters for graduate students. + /// + /// Returns a list of [SemesterInfo] objects. + /// + /// Note: The `startday` property may be incorrect in previous semesters. + Future> loadSemesters() { + final options = RequestOptions( + method: "GET", + path: _SEMESTER_URL, + ); + return FudanSession.request(options, (response) { + final Map data = response.data; + + final int errorCode = data['e']; + if (errorCode != 0) { + throw GraduateExamException('Failed to load semesters: ${data['m']}'); + } + + final Map d = data['d']; + final List termInfo = d['termInfo']; + + final semesters = []; + for (final term in termInfo) { + final Map termMap = term; + final String yearString = termMap['year']; // e.g., "2024-2025" + final String termString = termMap['term']; // "1" or "2" + + // Parse year from "2024-2025" format + final yearMatch = RegExp(r'(\d{4})-\d{4}').firstMatch(yearString); + if (yearMatch == null) continue; + + final schoolYear = yearString; + final name = termString; // "1" for first semester, "2" for second + + // Generate a unique semester ID from year and term + // Format: startYear * 10 + term (e.g., 20241 for 2024-2025 first semester) + final startYear = int.parse(yearMatch.group(1)!); + final semesterId = '${startYear}0$termString'; + + semesters.add(SemesterInfo(semesterId, schoolYear, name)); + } + + return semesters; + }); + } + + /// Load all exam scores for graduate students. + /// + /// Returns all scores across all semesters. + Future> loadExamScore() { + final options = RequestOptions( + method: "GET", + path: _SCORE_URL, + ); + return FudanSession.request( + options, + (response) { + final Map data = response.data; + + final String? code = data['code']; + if (code != '0') { + throw GraduateExamException('Failed to load scores: code=$code'); + } + + final Map datas = data['datas']; + final Map jdjscjcx = datas['jdjscjcx']; + final List rows = jdjscjcx['rows']; + + final scores = []; + for (final row in rows) { + final Map scoreMap = row; + scores.add(ExamScore.fromGraduateJson(scoreMap)); + } + + return scores; + }, + manualLoginUrl: Uri.parse(_SCORE_LOGIN_URL), + ); + } +} + +/// Exception for graduate exam/score related errors. +class GraduateExamException implements Exception { + final String message; + + GraduateExamException(this.message); + + @override + String toString() => 'GraduateExamException: $message'; +} From dc29c9a0b0b426b037708f66f3a2a02329137a14 Mon Sep 17 00:00:00 2001 From: w568w <1278297578@qq.com> Date: Sun, 18 Jan 2026 00:51:11 +0800 Subject: [PATCH 2/2] style & refactor: remove some AI slops --- .../fdu/graduate_exam_repository.dart | 114 +++--------------- 1 file changed, 14 insertions(+), 100 deletions(-) diff --git a/lib/repository/fdu/graduate_exam_repository.dart b/lib/repository/fdu/graduate_exam_repository.dart index da711d77..2672892c 100644 --- a/lib/repository/fdu/graduate_exam_repository.dart +++ b/lib/repository/fdu/graduate_exam_repository.dart @@ -26,35 +26,6 @@ import 'neo_login_tool.dart'; /// This repository fetches data from the graduate student system. /// /// ## API Details -/// -/// ### Get Semesters -/// URL: https://zlapp.fudan.edu.cn/fudanyjskb/wap/default/get-index -/// -/// Response: -/// ```json -/// { -/// "e": 0, -/// "m": "", -/// "d": { -/// "params": { -/// "year": "2023-2024", -/// "term": "2", -/// "startday": "2024-02-26", -/// "countweek": 18, -/// "week": 4 -/// }, -/// "termInfo": [ -/// { -/// "year": "2024-2025", -/// "term": "2", -/// "startday": "2025-03-01", -/// "countweek": 18 -/// } -/// ] -/// } -/// } -/// ``` -/// /// ### Get Scores /// URL: https://yzsfwapp.fudan.edu.cn/gsapp/sys/wdcjapp/modules/xscjcx/jdjscjcx.do /// Login URL: https://yzsfwapp.fudan.edu.cn/gsapp/sys/wdcjapp/*default/index.do @@ -101,84 +72,27 @@ class GraduateExamRepository extends BaseRepositoryWithDio { factory GraduateExamRepository.getInstance() => _instance; - /// Load all semesters for graduate students. - /// - /// Returns a list of [SemesterInfo] objects. + /// Load all exam scores for graduate students. /// - /// Note: The `startday` property may be incorrect in previous semesters. - Future> loadSemesters() { - final options = RequestOptions( - method: "GET", - path: _SEMESTER_URL, - ); + /// Returns all scores across all semesters. + Future> loadExamScore() { + final options = RequestOptions(method: "GET", path: _SCORE_URL); return FudanSession.request(options, (response) { final Map data = response.data; - final int errorCode = data['e']; - if (errorCode != 0) { - throw GraduateExamException('Failed to load semesters: ${data['m']}'); + final String? code = data['code']; + if (code != '0') { + throw GraduateExamException('Failed to load scores: code=$code'); } - final Map d = data['d']; - final List termInfo = d['termInfo']; + final Map datas = data['datas']; + final Map jdjscjcx = datas['jdjscjcx']; + final List rows = jdjscjcx['rows']; - final semesters = []; - for (final term in termInfo) { - final Map termMap = term; - final String yearString = termMap['year']; // e.g., "2024-2025" - final String termString = termMap['term']; // "1" or "2" - - // Parse year from "2024-2025" format - final yearMatch = RegExp(r'(\d{4})-\d{4}').firstMatch(yearString); - if (yearMatch == null) continue; - - final schoolYear = yearString; - final name = termString; // "1" for first semester, "2" for second - - // Generate a unique semester ID from year and term - // Format: startYear * 10 + term (e.g., 20241 for 2024-2025 first semester) - final startYear = int.parse(yearMatch.group(1)!); - final semesterId = '${startYear}0$termString'; - - semesters.add(SemesterInfo(semesterId, schoolYear, name)); - } - - return semesters; - }); - } - - /// Load all exam scores for graduate students. - /// - /// Returns all scores across all semesters. - Future> loadExamScore() { - final options = RequestOptions( - method: "GET", - path: _SCORE_URL, - ); - return FudanSession.request( - options, - (response) { - final Map data = response.data; - - final String? code = data['code']; - if (code != '0') { - throw GraduateExamException('Failed to load scores: code=$code'); - } - - final Map datas = data['datas']; - final Map jdjscjcx = datas['jdjscjcx']; - final List rows = jdjscjcx['rows']; - - final scores = []; - for (final row in rows) { - final Map scoreMap = row; - scores.add(ExamScore.fromGraduateJson(scoreMap)); - } - - return scores; - }, - manualLoginUrl: Uri.parse(_SCORE_LOGIN_URL), - ); + return rows + .map((row) => ExamScore.fromGraduateJson(row as Map)) + .toList(); + }, manualLoginUrl: Uri.parse(_SCORE_LOGIN_URL)); } }