|
@@ -0,0 +1,248 @@
|
|
|
|
|
+// 哈尔滨工业大学(hit.edu.cn) 研究生课表适配脚本
|
|
|
|
|
+// 研究生教育管理系统
|
|
|
|
|
+
|
|
|
|
|
+function parseWeeks(weekStr) {
|
|
|
|
|
+ if (!weekStr) return [];
|
|
|
|
|
+ const cleaned = weekStr.replace(/周/g, '').trim();
|
|
|
|
|
+ if (!cleaned) return [];
|
|
|
|
|
+ const weeks = [];
|
|
|
|
|
+ const parts = cleaned.split(/[,,]/);
|
|
|
|
|
+ for (const part of parts) {
|
|
|
|
|
+ const trimmed = part.trim();
|
|
|
|
|
+ if (!trimmed) continue;
|
|
|
|
|
+ const isOdd = trimmed.startsWith('单');
|
|
|
|
|
+ const isEven = trimmed.startsWith('双');
|
|
|
|
|
+ const rangeStr = isOdd || isEven ? trimmed.substring(1) : trimmed;
|
|
|
|
|
+ const rangeMatch = rangeStr.match(/^(\d+)\s*[-~]\s*(\d+)$/);
|
|
|
|
|
+ const singleMatch = rangeStr.match(/^(\d+)$/);
|
|
|
|
|
+ if (rangeMatch) {
|
|
|
|
|
+ const start = parseInt(rangeMatch[1], 10);
|
|
|
|
|
+ const end = parseInt(rangeMatch[2], 10);
|
|
|
|
|
+ for (let w = start; w <= end; w++) {
|
|
|
|
|
+ if (isOdd && w % 2 === 0) continue;
|
|
|
|
|
+ if (isEven && w % 2 !== 0) continue;
|
|
|
|
|
+ weeks.push(w);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (singleMatch) {
|
|
|
|
|
+ const num = parseInt(singleMatch[1], 10);
|
|
|
|
|
+ if (isOdd && num % 2 === 0) continue;
|
|
|
|
|
+ if (isEven && num % 2 !== 0) continue;
|
|
|
|
|
+ weeks.push(num);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return [...new Set(weeks)].sort((a, b) => a - b);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseCourseCell(cellStr, day, sections) {
|
|
|
|
|
+ if (!cellStr || cellStr === 'null') return [];
|
|
|
|
|
+ const courses = [];
|
|
|
|
|
+ const entries = cellStr.split('<br/>');
|
|
|
|
|
+ for (const entry of entries) {
|
|
|
|
|
+ const trimmed = entry.trim();
|
|
|
|
|
+ if (!trimmed) continue;
|
|
|
|
|
+ // 格式: 课程名◇教师[周次]教室[节次]节
|
|
|
|
|
+ // 先找◇分割课程名和教师
|
|
|
|
|
+ const teacherSplit = trimmed.split('\u25C7');
|
|
|
|
|
+ if (teacherSplit.length < 2) {
|
|
|
|
|
+ console.log("HIT调试: 无◇分隔, text=" + trimmed.substring(0, 40));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ const courseName = teacherSplit[0].trim();
|
|
|
|
|
+ const rest = teacherSplit.slice(1).join('\u25C7').trim();
|
|
|
|
|
+
|
|
|
|
|
+ // 从rest中提取: 教师[周次]教室[节次]节
|
|
|
|
|
+ // 找第一个[之前的是教师
|
|
|
|
|
+ const firstBracket = rest.indexOf('[');
|
|
|
|
|
+ if (firstBracket === -1) continue;
|
|
|
|
|
+ const teacher = rest.substring(0, firstBracket).trim();
|
|
|
|
|
+
|
|
|
|
|
+ // 提取周次: [周次] 中的内容
|
|
|
|
|
+ const weekMatch = rest.match(/\[(\d+[-~,,\d]+)周\]/);
|
|
|
|
|
+ if (!weekMatch) {
|
|
|
|
|
+ console.log("HIT调试: 无周次, rest=" + rest.substring(0, 50));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ const weeksStr = weekMatch[1];
|
|
|
|
|
+ const weeks = parseWeeks(weeksStr);
|
|
|
|
|
+ if (weeks.length === 0) continue;
|
|
|
|
|
+
|
|
|
|
|
+ // 提取节次: 最后一个[节次]节 中的内容
|
|
|
|
|
+ const sectionMatch = rest.match(/\[(.+)\]节/);
|
|
|
|
|
+ if (!sectionMatch) {
|
|
|
|
|
+ console.log("HIT调试: 无节次, rest=" + rest.substring(0, 50));
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ const sectionStr = sectionMatch[1].trim();
|
|
|
|
|
+
|
|
|
|
|
+ // 提取教室: 周次]和节次[之间的内容
|
|
|
|
|
+ const afterWeek = rest.substring(rest.indexOf(']周]') + 3);
|
|
|
|
|
+ const beforeSection = afterWeek.substring(0, afterWeek.lastIndexOf('['));
|
|
|
|
|
+ const room = beforeSection.trim();
|
|
|
|
|
+
|
|
|
|
|
+ const sectionParts = sectionStr.split(/[,,\s]+/);
|
|
|
|
|
+ const startSection = parseInt(sectionParts[0], 10);
|
|
|
|
|
+ const endSection = sectionParts.length > 1 ? parseInt(sectionParts[sectionParts.length - 1], 10) : startSection;
|
|
|
|
|
+ if (isNaN(startSection) || isNaN(endSection)) {
|
|
|
|
|
+ console.log("HIT调试: 节次解析失败, sectionStr=" + sectionStr);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log("HIT调试: 解析成功, " + courseName + "|" + teacher + "|" + room + "|d" + day + "|s" + startSection + "-" + endSection + "|w" + weeks.length);
|
|
|
|
|
+ courses.push({
|
|
|
|
|
+ name: courseName,
|
|
|
|
|
+ teacher: teacher,
|
|
|
|
|
+ position: room,
|
|
|
|
|
+ day: day,
|
|
|
|
|
+ startSection: startSection,
|
|
|
|
|
+ endSection: endSection,
|
|
|
|
|
+ weeks: weeks
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ return courses;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function mergeCourses(courses) {
|
|
|
|
|
+ if (courses.length <= 1) return courses;
|
|
|
|
|
+ courses.sort((a, b) => {
|
|
|
|
|
+ return a.name.localeCompare(b.name) || a.teacher.localeCompare(b.teacher) ||
|
|
|
|
|
+ a.position.localeCompare(b.position) || (a.day || 0) - (b.day || 0) ||
|
|
|
|
|
+ (a.startSection || 0) - (b.startSection || 0);
|
|
|
|
|
+ });
|
|
|
|
|
+ const merged = [];
|
|
|
|
|
+ let cur = courses[0];
|
|
|
|
|
+ for (let i = 1; i < courses.length; i++) {
|
|
|
|
|
+ const nxt = courses[i];
|
|
|
|
|
+ if (cur.name === nxt.name && cur.teacher === nxt.teacher && cur.position === nxt.position &&
|
|
|
|
|
+ cur.day === nxt.day && cur.startSection === nxt.startSection && cur.endSection === nxt.endSection) {
|
|
|
|
|
+ cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ merged.push(cur);
|
|
|
|
|
+ cur = nxt;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ merged.push(cur);
|
|
|
|
|
+ return merged;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function promptUserToStart() {
|
|
|
|
|
+ return await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ "研究生课表导入",
|
|
|
|
|
+ "导入前请确保您已在浏览器中成功登录研究生系统",
|
|
|
|
|
+ "好的,开始导入"
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function fetchCurrentSchedule() {
|
|
|
|
|
+ const url = "/xs/index/getDqxqkb?sf_request_type=ajax";
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(url, {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
|
|
|
+ credentials: "include"
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!response.ok) return null;
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ if (!data.isSuccess || !data.module) return null;
|
|
|
|
|
+ return data;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function fetchSemesterInfo() {
|
|
|
|
|
+ const url = "/xs/index/getZcxx?sf_request_type=ajax";
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(url, {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
|
|
|
+ credentials: "include"
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!response.ok) return null;
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ if (!data.isSuccess || !data.module) return null;
|
|
|
|
|
+ return data.module;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const dayMap = { mon: 1, tues: 2, wed: 3, thur: 4, fri: 5, sat: 6, sun: 7 };
|
|
|
|
|
+
|
|
|
|
|
+function parseScheduleData(scheduleData) {
|
|
|
|
|
+ const allCourses = [];
|
|
|
|
|
+ const module = scheduleData.module;
|
|
|
|
|
+ console.log("HIT调试: module行数=" + module.length);
|
|
|
|
|
+ for (const row of module) {
|
|
|
|
|
+ const sections = row.jcmc;
|
|
|
|
|
+ for (const [dayKey, dayNum] of Object.entries(dayMap)) {
|
|
|
|
|
+ const cellStr = row[dayKey];
|
|
|
|
|
+ if (!cellStr || cellStr === 'null') continue;
|
|
|
|
|
+ console.log("HIT调试: " + dayKey + "=" + cellStr.substring(0, 50));
|
|
|
|
|
+ const courses = parseCourseCell(cellStr, dayNum, sections);
|
|
|
|
|
+ console.log("HIT调试: 解析出" + courses.length + "门课");
|
|
|
|
|
+ allCourses.push(...courses);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return mergeCourses(allCourses);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function saveCourses(courses) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast("课程保存失败: " + error.message);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function runImportFlow() {
|
|
|
|
|
+ const alertConfirmed = await promptUserToStart();
|
|
|
|
|
+ if (!alertConfirmed) {
|
|
|
|
|
+ window.shiguangBridge.showToast("用户取消了导入。");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ window.shiguangBridge.showToast("正在获取课表数据...");
|
|
|
|
|
+
|
|
|
|
|
+ const [scheduleData, semesterInfo] = await Promise.all([
|
|
|
|
|
+ fetchCurrentSchedule(),
|
|
|
|
|
+ fetchSemesterInfo()
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ if (!scheduleData) {
|
|
|
|
|
+ window.shiguangBridge.showToast("未能获取课表数据,请检查网络环境或登录状态。");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log("HIT调试: API返回成功, module长度=" + scheduleData.module.length);
|
|
|
|
|
+ console.log("HIT调试: 第一行数据=" + JSON.stringify(scheduleData.module[0]));
|
|
|
|
|
+
|
|
|
|
|
+ const courses = parseScheduleData(scheduleData);
|
|
|
|
|
+ console.log("HIT调试: 总共解析=" + courses.length + "门课");
|
|
|
|
|
+ if (courses.length > 0) {
|
|
|
|
|
+ console.log("HIT调试: 第一门=" + JSON.stringify(courses[0]));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (courses.length === 0) {
|
|
|
|
|
+ window.shiguangBridge.showToast("未找到课程数据");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const saveResult = await saveCourses(courses);
|
|
|
|
|
+ if (!saveResult) return;
|
|
|
|
|
+
|
|
|
|
|
+ if (semesterInfo && semesterInfo.ZCJSSJ) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
|
|
|
|
|
+ semesterStartDate: null,
|
|
|
|
|
+ semesterTotalWeeks: 20
|
|
|
|
|
+ }));
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const semesterName = semesterInfo ? semesterInfo.MC : "当前学期";
|
|
|
|
|
+ window.shiguangBridge.showToast("课程导入成功!" + semesterName + ",共 " + courses.length + " 门课程");
|
|
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+runImportFlow();
|