|
|
@@ -0,0 +1,187 @@
|
|
|
+/**
|
|
|
+ * 华中科技大学 HUB 系统课表适配器。
|
|
|
+ *
|
|
|
+ * 接口与解析规则参考 StayUP-Calendar 的 HUST importer:
|
|
|
+ * https://github.com/Lucas04-nhr/StayUP-Calendar/blob/main/lib/src/pages/crawler/hust.dart
|
|
|
+ */
|
|
|
+
|
|
|
+const HUST_COURSE_API = new URL("/LsController/findNameCourse", location.origin).toString();
|
|
|
+const HUST_DEFAULT_SEMESTER_WEEKS = 20;
|
|
|
+
|
|
|
+function getDefaultTerm(now = new Date()) {
|
|
|
+ return {
|
|
|
+ academicYear: now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(),
|
|
|
+ semester: now.getMonth() >= 1 && now.getMonth() <= 6 ? 2 : 1
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 生成与原 importer 相同范围的学期:当前年份前 4 年至后 1 年,共 12 个。
|
|
|
+ * semester=1 为秋季,semester=2 为春季;倒序排列便于优先选择近期学期。
|
|
|
+ */
|
|
|
+function buildTermOptions(now = new Date()) {
|
|
|
+ const currentYear = now.getFullYear();
|
|
|
+ const terms = [];
|
|
|
+
|
|
|
+ for (let year = currentYear + 1; year >= currentYear - 4; year--) {
|
|
|
+ terms.push({ academicYear: year, semester: 1, label: `${year} 年秋季学期` });
|
|
|
+ terms.push({ academicYear: year, semester: 2, label: `${year} 年春季学期` });
|
|
|
+ }
|
|
|
+
|
|
|
+ return terms;
|
|
|
+}
|
|
|
+
|
|
|
+async function selectTerm() {
|
|
|
+ const terms = buildTermOptions();
|
|
|
+ const defaultTerm = getDefaultTerm();
|
|
|
+ const defaultIndex = Math.max(0, terms.findIndex((term) =>
|
|
|
+ term.academicYear === defaultTerm.academicYear &&
|
|
|
+ term.semester === defaultTerm.semester
|
|
|
+ ));
|
|
|
+
|
|
|
+ const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
|
|
|
+ "选择学期",
|
|
|
+ JSON.stringify(terms.map((term) => term.label)),
|
|
|
+ defaultIndex
|
|
|
+ );
|
|
|
+
|
|
|
+ if (selectedIndex === null || selectedIndex === -1) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (!Number.isInteger(selectedIndex) || selectedIndex < 0 || selectedIndex >= terms.length) {
|
|
|
+ throw new Error("学期选择结果无效");
|
|
|
+ }
|
|
|
+
|
|
|
+ return terms[selectedIndex];
|
|
|
+}
|
|
|
+
|
|
|
+function parsePositiveInteger(value) {
|
|
|
+ const parsed = Number.parseInt(String(value), 10);
|
|
|
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
|
+}
|
|
|
+
|
|
|
+function expandWeeks(startWeek, endWeek) {
|
|
|
+ const weeks = [];
|
|
|
+ for (let week = startWeek; week <= endWeek; week++) {
|
|
|
+ weeks.push(week);
|
|
|
+ }
|
|
|
+ return weeks;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * HUST 的一门课程可包含多个 tr 时段。本项目没有 extraSlots 字段,
|
|
|
+ * 因此每个有效时段转换成一个标准课程块。
|
|
|
+ */
|
|
|
+function parseCourses(payload) {
|
|
|
+ if (!payload || String(payload.code) !== "200" || !Array.isArray(payload.data)) {
|
|
|
+ throw new Error(`课表接口返回异常(code: ${payload?.code ?? "未知"})`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const courses = [];
|
|
|
+
|
|
|
+ for (const item of payload.data) {
|
|
|
+ const name = String(item?.KCMC ?? "").trim();
|
|
|
+ if (!name || !Array.isArray(item?.tr)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const slot of item.tr) {
|
|
|
+ if (slot?.XQS === "<待定>" || slot?.QSJC === "<待定>") {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const day = parsePositiveInteger(slot?.XQS);
|
|
|
+ const startSection = parsePositiveInteger(slot?.QSJC);
|
|
|
+ const endSection = parsePositiveInteger(slot?.JSJC);
|
|
|
+ const startWeek = parsePositiveInteger(slot?.QSZC);
|
|
|
+ const endWeek = parsePositiveInteger(slot?.JSZC);
|
|
|
+
|
|
|
+ if (
|
|
|
+ day === null || day > 7 ||
|
|
|
+ startSection === null || endSection === null || endSection < startSection ||
|
|
|
+ startWeek === null || endWeek === null || endWeek < startWeek
|
|
|
+ ) {
|
|
|
+ console.warn("跳过无法解析的 HUST 课程时段:", name, slot);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ courses.push({
|
|
|
+ name,
|
|
|
+ teacher: String(slot?.XM ?? "").trim(),
|
|
|
+ position: String(slot?.JSMC ?? "").trim(),
|
|
|
+ day,
|
|
|
+ startSection,
|
|
|
+ endSection,
|
|
|
+ weeks: expandWeeks(startWeek, endWeek)
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return courses;
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchCourses(term) {
|
|
|
+ const termCode = `${term.academicYear}${term.semester}`;
|
|
|
+ const response = await fetch(`${HUST_COURSE_API}?kcbxqh=${encodeURIComponent(termCode)}`, {
|
|
|
+ method: "GET",
|
|
|
+ credentials: "include",
|
|
|
+ headers: {
|
|
|
+ Accept: "application/json, text/plain, */*"
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`课表请求失败(HTTP ${response.status})`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const raw = await response.text();
|
|
|
+ if (!raw.trim() || raw.trim().startsWith("<")) {
|
|
|
+ throw new Error("未读取到课表数据,请确认已登录 HUB 系统并选择了正确的学期");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ return parseCourses(JSON.parse(raw));
|
|
|
+ } catch (error) {
|
|
|
+ if (error instanceof SyntaxError) {
|
|
|
+ throw new Error("课表接口未返回有效 JSON,请重新登录后再试");
|
|
|
+ }
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function saveCourses(courses) {
|
|
|
+ await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
+
|
|
|
+ const semesterTotalWeeks = courses.reduce((maximum, course) => {
|
|
|
+ const courseMaximum = course.weeks.length > 0 ? Math.max(...course.weeks) : 0;
|
|
|
+ return Math.max(maximum, courseMaximum);
|
|
|
+ }, HUST_DEFAULT_SEMESTER_WEEKS);
|
|
|
+
|
|
|
+ await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({ semesterTotalWeeks }));
|
|
|
+}
|
|
|
+
|
|
|
+async function runImportFlow() {
|
|
|
+ try {
|
|
|
+ const term = await selectTerm();
|
|
|
+ if (term === null) {
|
|
|
+ AndroidBridge.showToast("已取消导入");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ AndroidBridge.showToast(`正在获取 ${term.label} 课表...`);
|
|
|
+ const courses = await fetchCourses(term);
|
|
|
+ if (courses.length === 0) {
|
|
|
+ AndroidBridge.showToast("该学期未查询到有效课程,请检查学期或登录状态");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await saveCourses(courses);
|
|
|
+ AndroidBridge.showToast(`成功导入 ${courses.length} 个课程时段,请按校历设置开学日期`);
|
|
|
+ AndroidBridge.notifyTaskCompletion();
|
|
|
+ } catch (error) {
|
|
|
+ console.error("课表导入失败:", error);
|
|
|
+ AndroidBridge.showToast(`导入失败:${error?.message ?? error}`);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+runImportFlow();
|