|
@@ -0,0 +1,330 @@
|
|
|
|
|
+// 辽宁大学程表适配脚本
|
|
|
|
|
+// 基于 URP 教务系统 JSON 接口适配
|
|
|
|
|
+
|
|
|
|
|
+// 解析 classWeek 二进制串,如 "111111111111111100000000" -> [1..16]
|
|
|
|
|
+function parseWeeks(binaryStr) {
|
|
|
|
|
+ const weeks = [];
|
|
|
|
|
+ if (!binaryStr) return weeks;
|
|
|
|
|
+ for (let i = 0; i < binaryStr.length; i++) {
|
|
|
|
|
+ if (binaryStr[i] === '1') weeks.push(i + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ return weeks;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function cleanTeacher(teacher) {
|
|
|
|
|
+ return (teacher || "").replace(/\*/g, " ").replace(/\s+/g, " ").trim();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatTime(timeStr) {
|
|
|
|
|
+ if (timeStr && timeStr.length === 4) {
|
|
|
|
|
+ return timeStr.substring(0, 2) + ":" + timeStr.substring(2);
|
|
|
|
|
+ }
|
|
|
|
|
+ return timeStr || "";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function buildPosition(campus, building, room) {
|
|
|
|
|
+ const campusPart = (campus || "").trim();
|
|
|
|
|
+ let buildingPart = (building || "").trim();
|
|
|
|
|
+ let roomPart = (room || "").trim();
|
|
|
|
|
+ if (campusPart && buildingPart.startsWith(campusPart)) {
|
|
|
|
|
+ buildingPart = buildingPart.slice(campusPart.length);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (campusPart && roomPart.startsWith(campusPart)) {
|
|
|
|
|
+ roomPart = roomPart.slice(campusPart.length);
|
|
|
|
|
+ }
|
|
|
|
|
+ const result = (campusPart + buildingPart + roomPart).trim();
|
|
|
|
|
+ return result || "待定";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseCourses(data) {
|
|
|
|
|
+ const courses = [];
|
|
|
|
|
+ const seen = new Set();
|
|
|
|
|
+ const list = (data && data.xkxx) || [];
|
|
|
|
|
+ for (let i = 0; i < list.length; i++) {
|
|
|
|
|
+ const map = list[i];
|
|
|
|
|
+ if (!map) continue;
|
|
|
|
|
+ for (const key in map) {
|
|
|
|
|
+ const course = map[key];
|
|
|
|
|
+ if (!course || !course.courseName || !course.timeAndPlaceList) continue;
|
|
|
|
|
+ const teacher = cleanTeacher(course.attendClassTeacher);
|
|
|
|
|
+ for (const timePlace of course.timeAndPlaceList) {
|
|
|
|
|
+ const day = parseInt(timePlace.classDay);
|
|
|
|
|
+ const start = parseInt(timePlace.classSessions);
|
|
|
|
|
+ const continuing = parseInt(timePlace.continuingSession) || 2;
|
|
|
|
|
+ const end = start + continuing - 1;
|
|
|
|
|
+ const weeks = parseWeeks(timePlace.classWeek);
|
|
|
|
|
+ if (!day || !start || weeks.length === 0) continue;
|
|
|
|
|
+ const position = buildPosition(timePlace.campusName, timePlace.teachingBuildingName, timePlace.classroomName);
|
|
|
|
|
+ const dedupKey = course.courseName + '|' + teacher + '|' + day + '|' + start + '|' + end + '|' + weeks.join(',') + '|' + position;
|
|
|
|
|
+ if (seen.has(dedupKey)) continue;
|
|
|
|
|
+ seen.add(dedupKey);
|
|
|
|
|
+ courses.push({
|
|
|
|
|
+ name: course.courseName,
|
|
|
|
|
+ teacher: teacher,
|
|
|
|
|
+ position: position,
|
|
|
|
|
+ day: day,
|
|
|
|
|
+ startSection: start,
|
|
|
|
|
+ endSection: end,
|
|
|
|
|
+ weeks: weeks,
|
|
|
|
|
+ isCustomTime: false
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return courses;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseTimeSlots(data) {
|
|
|
|
|
+ const slots = [];
|
|
|
|
|
+ const list = (data && data.jcsjbs) || [];
|
|
|
|
|
+ for (const slot of list) {
|
|
|
|
|
+ const num = parseInt(slot.jc, 10);
|
|
|
|
|
+ if (isNaN(num)) continue;
|
|
|
|
|
+ slots.push({
|
|
|
|
|
+ number: num,
|
|
|
|
|
+ startTime: formatTime(slot.kssj),
|
|
|
|
|
+ endTime: formatTime(slot.jssj)
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ return slots.sort((a, b) => a.number - b.number);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 教务系统提供的时间可能把校区搞混,所以提供各校区的作息时间表,供用户选择
|
|
|
|
|
+const CHONGSHAN_TIME_SLOTS = [
|
|
|
|
|
+ { number: 1, startTime: "08:00", endTime: "08:50" },
|
|
|
|
|
+ { number: 2, startTime: "09:00", endTime: "09:50" },
|
|
|
|
|
+ { number: 3, startTime: "10:10", endTime: "11:00" },
|
|
|
|
|
+ { number: 4, startTime: "11:10", endTime: "12:00" },
|
|
|
|
|
+ { number: 5, startTime: "13:30", endTime: "14:20" },
|
|
|
|
|
+ { number: 6, startTime: "14:30", endTime: "15:20" },
|
|
|
|
|
+ { number: 7, startTime: "15:40", endTime: "16:30" },
|
|
|
|
|
+ { number: 8, startTime: "16:40", endTime: "17:30" },
|
|
|
|
|
+ { number: 9, startTime: "18:00", endTime: "18:50" },
|
|
|
|
|
+ { number: 10, startTime: "19:00", endTime: "19:50" },
|
|
|
|
|
+ { number: 11, startTime: "20:10", endTime: "21:00" },
|
|
|
|
|
+ { number: 12, startTime: "21:10", endTime: "22:00" }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const PUHE_TIME_SLOTS = [
|
|
|
|
|
+ { number: 1, startTime: "08:30", endTime: "09:15" },
|
|
|
|
|
+ { number: 2, startTime: "09:20", endTime: "10:05" },
|
|
|
|
|
+ { number: 3, startTime: "10:25", endTime: "11:10" },
|
|
|
|
|
+ { number: 4, startTime: "11:15", endTime: "12:00" },
|
|
|
|
|
+ { number: 5, startTime: "13:30", endTime: "14:15" },
|
|
|
|
|
+ { number: 6, startTime: "14:20", endTime: "15:05" },
|
|
|
|
|
+ { number: 7, startTime: "15:25", endTime: "16:10" },
|
|
|
|
|
+ { number: 8, startTime: "16:15", endTime: "17:00" },
|
|
|
|
|
+ { number: 9, startTime: "17:30", endTime: "18:15" },
|
|
|
|
|
+ { number: 10, startTime: "18:20", endTime: "19:05" },
|
|
|
|
|
+ { number: 11, startTime: "19:25", endTime: "20:10" },
|
|
|
|
|
+ { number: 12, startTime: "20:15", endTime: "21:00" }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const WUSHENG_TIME_SLOTS = [
|
|
|
|
|
+ { number: 1, startTime: "08:00", endTime: "08:50" },
|
|
|
|
|
+ { number: 2, startTime: "09:00", endTime: "09:50" },
|
|
|
|
|
+ { number: 3, startTime: "10:10", endTime: "11:00" },
|
|
|
|
|
+ { number: 4, startTime: "11:10", endTime: "12:00" },
|
|
|
|
|
+ { number: 5, startTime: "13:30", endTime: "14:20" },
|
|
|
|
|
+ { number: 6, startTime: "14:30", endTime: "15:20" },
|
|
|
|
|
+ { number: 7, startTime: "15:30", endTime: "16:20" },
|
|
|
|
|
+ { number: 8, startTime: "16:30", endTime: "17:20" },
|
|
|
|
|
+ { number: 9, startTime: "18:00", endTime: "18:50" },
|
|
|
|
|
+ { number: 10, startTime: "19:00", endTime: "19:50" },
|
|
|
|
|
+ { number: 11, startTime: "20:10", endTime: "21:00" },
|
|
|
|
|
+ { number: 12, startTime: "21:10", endTime: "22:00" }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const CAMPUS_CHONGSHAN = "崇山";
|
|
|
|
|
+const CAMPUS_PUHE = "蒲河";
|
|
|
|
|
+const CAMPUS_WUSHENG = "武圣";
|
|
|
|
|
+
|
|
|
|
|
+const TIME_SLOT_OPTIONS = [
|
|
|
|
|
+ "从教务提取",
|
|
|
|
|
+ "内置" + CAMPUS_CHONGSHAN + "校区作息",
|
|
|
|
|
+ "内置" + CAMPUS_PUHE + "校区作息",
|
|
|
|
|
+ "内置" + CAMPUS_WUSHENG + "校区作息"
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const TIME_SLOT_OPTION_EXTRACT = 0;
|
|
|
|
|
+const TIME_SLOT_OPTION_CHONGSHAN = 1;
|
|
|
|
|
+const TIME_SLOT_OPTION_PUHE = 2;
|
|
|
|
|
+const TIME_SLOT_OPTION_WUSHENG = 3;
|
|
|
|
|
+
|
|
|
|
|
+// 根据课程数据的 campusName 字段猜测所在校区
|
|
|
|
|
+function guessCampus(data) {
|
|
|
|
|
+ const counts = {};
|
|
|
|
|
+ const list = (data && data.xkxx) || [];
|
|
|
|
|
+ for (const map of list) {
|
|
|
|
|
+ if (!map) continue;
|
|
|
|
|
+ for (const key in map) {
|
|
|
|
|
+ const course = map[key];
|
|
|
|
|
+ if (!course || !course.timeAndPlaceList) continue;
|
|
|
|
|
+ for (const timePlace of course.timeAndPlaceList) {
|
|
|
|
|
+ const campus = (timePlace.campusName || "").trim();
|
|
|
|
|
+ if (campus) {
|
|
|
|
|
+ counts[campus] = (counts[campus] || 0) + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ let best = null;
|
|
|
|
|
+ let bestCount = 0;
|
|
|
|
|
+ for (const name in counts) {
|
|
|
|
|
+ if (counts[name] > bestCount) {
|
|
|
|
|
+ best = name;
|
|
|
|
|
+ bestCount = counts[name];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (best && best.includes(CAMPUS_CHONGSHAN)) {
|
|
|
|
|
+ return CAMPUS_CHONGSHAN;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (best && best.includes(CAMPUS_PUHE)) {
|
|
|
|
|
+ return CAMPUS_PUHE;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (best && best.includes(CAMPUS_WUSHENG)) {
|
|
|
|
|
+ return CAMPUS_WUSHENG;
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function computeTotalWeeks(courses) {
|
|
|
|
|
+ let max = 0;
|
|
|
|
|
+ for (const course of courses) {
|
|
|
|
|
+ for (const week of course.weeks) {
|
|
|
|
|
+ if (week > max) {
|
|
|
|
|
+ max = week;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return max > 0 ? max : 20;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function readSemesterOptions() {
|
|
|
|
|
+ const select = document.getElementById('planCode');
|
|
|
|
|
+ if (!select || !select.options || select.options.length === 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ const texts = [];
|
|
|
|
|
+ const values = [];
|
|
|
|
|
+ for (let i = 0; i < select.options.length; i++) {
|
|
|
|
|
+ texts.push(select.options[i].text.replace(/\s+/g, ' ').trim());
|
|
|
|
|
+ values.push(select.options[i].value);
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ texts: texts,
|
|
|
|
|
+ values: values,
|
|
|
|
|
+ defaultIndex: select.selectedIndex >= 0 ? select.selectedIndex : 0
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function runImportFlow() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ let method = "GET";
|
|
|
|
|
+ let body = undefined;
|
|
|
|
|
+ let idx = -1;
|
|
|
|
|
+ let options = null;
|
|
|
|
|
+
|
|
|
|
|
+ const apiPath = "/student/courseSelect/thisSemesterCurriculum/ajaxStudentSchedule/callback";
|
|
|
|
|
+ const headers = {
|
|
|
|
|
+ "Accept": "*/*",
|
|
|
|
|
+ "X-Requested-With": "XMLHttpRequest"
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const confirmed = await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ "辽宁大学课表导入",
|
|
|
|
|
+ "请确认已登录教务系统,并处于课表页面。",
|
|
|
|
|
+ "确定"
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!confirmed) return;
|
|
|
|
|
+
|
|
|
|
|
+ options = readSemesterOptions();
|
|
|
|
|
+ if (options) {
|
|
|
|
|
+ idx = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
|
|
+ "选择学期",
|
|
|
|
|
+ JSON.stringify(options.texts),
|
|
|
|
|
+ options.defaultIndex
|
|
|
|
|
+ );
|
|
|
|
|
+ if (idx === null || idx < 0) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入已取消");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (options && idx >= 0) {
|
|
|
|
|
+ method = "POST";
|
|
|
|
|
+ headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
|
|
|
+ headers["Accept"] = "application/json, text/javascript, */*; q=0.01";
|
|
|
|
|
+ body = "planCode=" + encodeURIComponent(options.values[idx]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ window.shiguangBridge.showToast("正在获取教务数据...");
|
|
|
|
|
+ const res = await fetch(apiPath, {
|
|
|
|
|
+ method: method,
|
|
|
|
|
+ headers: headers,
|
|
|
|
|
+ body: body,
|
|
|
|
|
+ credentials: "include"
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!res.ok) {
|
|
|
|
|
+ window.shiguangBridge.showToast("网络请求失败,状态码: " + res.status);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+
|
|
|
|
|
+ const courses = parseCourses(data);
|
|
|
|
|
+ if (courses.length === 0) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入失败: 未解析到课程数据,请确认所选学期有课");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const totalWeeks = computeTotalWeeks(courses);
|
|
|
|
|
+
|
|
|
|
|
+ const campus = guessCampus(data);
|
|
|
|
|
+ let defaultIndex = TIME_SLOT_OPTION_EXTRACT;
|
|
|
|
|
+ if (campus === CAMPUS_CHONGSHAN) {
|
|
|
|
|
+ defaultIndex = TIME_SLOT_OPTION_CHONGSHAN;
|
|
|
|
|
+ } else if (campus === CAMPUS_PUHE) {
|
|
|
|
|
+ defaultIndex = TIME_SLOT_OPTION_PUHE;
|
|
|
|
|
+ } else if (campus === CAMPUS_WUSHENG) {
|
|
|
|
|
+ defaultIndex = TIME_SLOT_OPTION_WUSHENG;
|
|
|
|
|
+ }
|
|
|
|
|
+ const choice = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
|
|
+ "作息时间来源",
|
|
|
|
|
+ JSON.stringify(TIME_SLOT_OPTIONS),
|
|
|
|
|
+ defaultIndex
|
|
|
|
|
+ );
|
|
|
|
|
+ if (choice === null || choice < 0) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入已取消");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ let timeSlots;
|
|
|
|
|
+ if (choice === TIME_SLOT_OPTION_CHONGSHAN) {
|
|
|
|
|
+ timeSlots = CHONGSHAN_TIME_SLOTS;
|
|
|
|
|
+ } else if (choice === TIME_SLOT_OPTION_PUHE) {
|
|
|
|
|
+ timeSlots = PUHE_TIME_SLOTS;
|
|
|
|
|
+ } else if (choice === TIME_SLOT_OPTION_WUSHENG) {
|
|
|
|
|
+ timeSlots = WUSHENG_TIME_SLOTS;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ timeSlots = parseTimeSlots(data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const saved = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
|
|
+ if (!saved) {
|
|
|
|
|
+ window.shiguangBridge.showToast("课程保存失败");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({ semesterTotalWeeks: totalWeeks }));
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast("作息时间或配置保存失败: " + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ window.shiguangBridge.showToast("成功导入 " + courses.length + " 个课程时段");
|
|
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入失败: " + e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+runImportFlow();
|