|
|
@@ -0,0 +1,255 @@
|
|
|
+// 南通大学拾光课程表适配脚本
|
|
|
+// 通过正方教务V9个人/班级课表接口获取课程,并转换为导入格式
|
|
|
+
|
|
|
+(function () {
|
|
|
+const WINTER_TIME_SLOTS = [
|
|
|
+ { number: 1, startTime: "07:50", endTime: "08:30" },
|
|
|
+ { number: 2, startTime: "08:40", endTime: "09:20" },
|
|
|
+ { number: 3, startTime: "09:35", endTime: "10:15" },
|
|
|
+ { number: 4, startTime: "10:30", endTime: "11:10" },
|
|
|
+ { number: 5, startTime: "11:20", endTime: "12:00" },
|
|
|
+ { number: 6, startTime: "13:30", endTime: "14:10" },
|
|
|
+ { number: 7, startTime: "14:20", endTime: "15:00" },
|
|
|
+ { number: 8, startTime: "15:20", endTime: "16:00" },
|
|
|
+ { number: 9, startTime: "16:10", endTime: "16:50" },
|
|
|
+ { number: 10, startTime: "18:30", endTime: "19:10" },
|
|
|
+ { number: 11, startTime: "19:20", endTime: "20:00" },
|
|
|
+ { number: 12, startTime: "20:10", endTime: "20:50" }
|
|
|
+];
|
|
|
+
|
|
|
+const SUMMER_TIME_SLOTS = [
|
|
|
+ { number: 1, startTime: "07:50", endTime: "08:30" },
|
|
|
+ { number: 2, startTime: "08:40", endTime: "09:20" },
|
|
|
+ { number: 3, startTime: "09:35", endTime: "10:15" },
|
|
|
+ { number: 4, startTime: "10:30", endTime: "11:10" },
|
|
|
+ { number: 5, startTime: "11:20", endTime: "12:00" },
|
|
|
+ { number: 6, startTime: "14:00", endTime: "14:40" },
|
|
|
+ { number: 7, startTime: "14:50", endTime: "15:30" },
|
|
|
+ { number: 8, startTime: "15:50", endTime: "16:30" },
|
|
|
+ { number: 9, startTime: "16:40", endTime: "17:20" },
|
|
|
+ { number: 10, startTime: "19:00", endTime: "19:40" },
|
|
|
+ { number: 11, startTime: "19:50", endTime: "20:30" },
|
|
|
+ { number: 12, startTime: "20:40", endTime: "21:20" }
|
|
|
+];
|
|
|
+
|
|
|
+function parseSections(value) {
|
|
|
+ const numbers = String(value || "").match(/\d+/g);
|
|
|
+ if (!numbers || numbers.length === 0) return null;
|
|
|
+ const startSection = Number(numbers[0]);
|
|
|
+ const endSection = Number(numbers[numbers.length - 1]);
|
|
|
+ if (!Number.isInteger(startSection) || !Number.isInteger(endSection) || startSection < 1 || endSection < startSection) return null;
|
|
|
+ return { startSection, endSection };
|
|
|
+}
|
|
|
+
|
|
|
+// 将周次文本转换为周数数组
|
|
|
+function parseWeeks(value) {
|
|
|
+ const weeks = new Set();
|
|
|
+ String(value || "").replace(/(/g, "(").replace(/)/g, ")").split(/[,,、;]/).forEach((part) => {
|
|
|
+ const numbers = part.match(/\d+/g);
|
|
|
+ if (!numbers) return;
|
|
|
+ const start = Number(numbers[0]);
|
|
|
+ const end = Number(numbers[numbers.length - 1]);
|
|
|
+ const odd = part.includes("单");
|
|
|
+ const even = part.includes("双");
|
|
|
+ for (let week = start; week <= end; week += 1) {
|
|
|
+ if (odd && week % 2 === 0) continue;
|
|
|
+ if (even && week % 2 !== 0) continue;
|
|
|
+ weeks.add(week);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return [...weeks].sort((left, right) => left - right);
|
|
|
+}
|
|
|
+
|
|
|
+function normalizeStartDate(value) {
|
|
|
+ const match = String(value || "").match(/(\d{4})[-\/.年](\d{1,2})[-\/.月](\d{1,2})/);
|
|
|
+ if (!match) return null;
|
|
|
+ return `${match[1]}-${match[2].padStart(2, "0")}-${match[3].padStart(2, "0")}`;
|
|
|
+}
|
|
|
+
|
|
|
+function findStartDate(value) {
|
|
|
+ if (!value || typeof value !== "object") return normalizeStartDate(value);
|
|
|
+ if (Array.isArray(value)) {
|
|
|
+ const firstWeek = value.find((item) => String(item?.zs) === "1" || String(item?.zsmc) === "1") || value[0];
|
|
|
+ return normalizeStartDate(firstWeek?.zrq || firstWeek?.zcrq || firstWeek?.rq);
|
|
|
+ }
|
|
|
+ const firstWeekDate = normalizeStartDate(value.zrq || value.zcrq || value.rq);
|
|
|
+ if (firstWeekDate) return firstWeekDate;
|
|
|
+ for (const [key, item] of Object.entries(value)) {
|
|
|
+ const date = item && typeof item === "object" ? findStartDate(item) : null;
|
|
|
+ if (date) return date;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchSemesterStartDate() {
|
|
|
+ const xnm = document.querySelector("#xnm")?.value;
|
|
|
+ const xqm = document.querySelector("#xqm")?.value;
|
|
|
+ if (!xnm || !xqm) {
|
|
|
+ console.warn("NTU semester start date: missing xnm or xqm", { xnm, xqm });
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ const url = new URL("/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154", window.location.origin);
|
|
|
+ try {
|
|
|
+ const response = await fetch(url.href, {
|
|
|
+ headers: {
|
|
|
+ accept: "application/json, text/javascript, */*; q=0.01",
|
|
|
+ "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
|
+ "x-requested-with": "XMLHttpRequest"
|
|
|
+ },
|
|
|
+ body: `xnm=${encodeURIComponent(xnm)}&xqm=${encodeURIComponent(xqm)}`,
|
|
|
+ method: "POST",
|
|
|
+ credentials: "include"
|
|
|
+ });
|
|
|
+ const rawText = await response.text();
|
|
|
+ let data;
|
|
|
+ try {
|
|
|
+ data = JSON.parse(rawText);
|
|
|
+ } catch {
|
|
|
+ console.warn("NTU semester start date: response is not JSON", response.status, rawText);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ console.log("NTU semester start date response:", data);
|
|
|
+ const startDate = findStartDate(data);
|
|
|
+ console.log("NTU semester start date:", startDate || "not found");
|
|
|
+ return startDate;
|
|
|
+ } catch (error) {
|
|
|
+ console.warn("NTU semester start date request failed:", error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 将课程字段转换为导入协议字段
|
|
|
+function normalizeCourses(rawCourses) {
|
|
|
+ const uniqueCourses = new Map();
|
|
|
+ rawCourses.forEach((rawCourse) => {
|
|
|
+ const name = String(rawCourse.kcmc || "").replace(/[■◆▲]/g, "").trim();
|
|
|
+ const teacher = String(rawCourse.xm || "未知").trim() || "未知";
|
|
|
+ const position = String(rawCourse.cdmc || "待定").trim() || "待定";
|
|
|
+ const day = Number(rawCourse.xqj);
|
|
|
+ const sections = parseSections(rawCourse.jcs || rawCourse.jc);
|
|
|
+ const weeks = parseWeeks(rawCourse.zcd);
|
|
|
+ if (!name || !sections || !Number.isInteger(day) || day < 1 || day > 7 || weeks.length === 0) return;
|
|
|
+ const course = { name, teacher, position, day, startSection: sections.startSection, endSection: sections.endSection, weeks };
|
|
|
+ const key = [name, teacher, position, day, sections.startSection, sections.endSection, weeks.join(",")].join("|");
|
|
|
+ if (!uniqueCourses.has(key)) uniqueCourses.set(key, course);
|
|
|
+ });
|
|
|
+ return [...uniqueCourses.values()].sort((left, right) => left.day - right.day || left.startSection - right.startSection || left.name.localeCompare(right.name));
|
|
|
+}
|
|
|
+
|
|
|
+// 合并同一课程的重复节次
|
|
|
+function mergeAndDistinctCourses(courses) {
|
|
|
+ if (courses.length <= 1) return courses;
|
|
|
+ const list = courses.map((course) => ({ ...course, weeks: [...new Set(course.weeks)].sort((left, right) => left - right) }));
|
|
|
+ const sameBase = (left, right) => left.name === right.name && left.teacher === right.teacher && left.position === right.position && left.day === right.day;
|
|
|
+ list.sort((left, right) => left.name.localeCompare(right.name) || left.teacher.localeCompare(right.teacher) || left.position.localeCompare(right.position) || left.day - right.day || left.startSection - right.startSection);
|
|
|
+ const merged = [];
|
|
|
+ for (const course of list) {
|
|
|
+ const previous = merged[merged.length - 1];
|
|
|
+ if (previous && sameBase(previous, course) && previous.startSection === course.startSection && previous.endSection === course.endSection) {
|
|
|
+ previous.weeks = [...new Set([...previous.weeks, ...course.weeks])].sort((left, right) => left - right);
|
|
|
+ } else if (previous && sameBase(previous, course) && previous.weeks.join(",") === course.weeks.join(",") && previous.endSection + 1 === course.startSection) {
|
|
|
+ previous.endSection = course.endSection;
|
|
|
+ } else {
|
|
|
+ merged.push(course);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return merged;
|
|
|
+}
|
|
|
+
|
|
|
+// 个人课表
|
|
|
+async function fetchApiCourses() {
|
|
|
+ const xnm = document.querySelector("#xnm")?.value;
|
|
|
+ const xqm = document.querySelector("#xqm")?.value;
|
|
|
+ if (!xnm || !xqm) return null;
|
|
|
+ const url = new URL("/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151", window.location.origin);
|
|
|
+ try {
|
|
|
+ const response = await fetch(url.href, {
|
|
|
+ method: "POST",
|
|
|
+ headers: { "content-type": "application/x-www-form-urlencoded;charset=UTF-8", "x-requested-with": "XMLHttpRequest" },
|
|
|
+ body: `xnm=${encodeURIComponent(xnm)}&xqm=${encodeURIComponent(xqm)}&kzlx=ck&xsdm=&kclbdm=`,
|
|
|
+ credentials: "include"
|
|
|
+ });
|
|
|
+ const data = await response.json();
|
|
|
+ console.log("NTU course API response:", data);
|
|
|
+ const rawCourses = data?.kbList || data?.datas?.kbList || data?.rows;
|
|
|
+ if (!Array.isArray(rawCourses) || !rawCourses.length) return null;
|
|
|
+ return mergeAndDistinctCourses(normalizeCourses(rawCourses));
|
|
|
+ } catch (error) {
|
|
|
+ console.warn("NTU course API request failed:", error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 班级课表
|
|
|
+async function fetchClassApiCourses() {
|
|
|
+ const pageMap = window.api?.data?.map || {};
|
|
|
+ const readValue = (key) => pageMap[key] ?? document.querySelector(`#${key}`)?.value ?? "";
|
|
|
+ const requestFields = [
|
|
|
+ "xnm",
|
|
|
+ "xqm",
|
|
|
+ "njdm_id",
|
|
|
+ "zyh_id",
|
|
|
+ "bh_id",
|
|
|
+ "tjkbzdm",
|
|
|
+ "tjkbzxsdm",
|
|
|
+ "zxszjjs"
|
|
|
+ ];
|
|
|
+ const body = new URLSearchParams();
|
|
|
+ requestFields.forEach((key) => {
|
|
|
+ const value = readValue(key);
|
|
|
+ if (value !== "" && value !== null && typeof value !== "undefined") body.set(key, String(value));
|
|
|
+ });
|
|
|
+ if (!body.get("xnm") || !body.get("xqm") || !body.get("bh_id")) {
|
|
|
+ console.warn("NTU class course API parameters are incomplete", Object.fromEntries(body));
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ const url = new URL("/jwglxt/kbdy/bjkbdy_cxBjKb.html", window.location.origin);
|
|
|
+ try {
|
|
|
+ const response = await fetch(url.href, {
|
|
|
+ method: "POST",
|
|
|
+ headers: { "content-type": "application/x-www-form-urlencoded;charset=UTF-8", "x-requested-with": "XMLHttpRequest" },
|
|
|
+ body,
|
|
|
+ credentials: "include"
|
|
|
+ });
|
|
|
+ const data = await response.json();
|
|
|
+ console.log("NTU class course API response:", data);
|
|
|
+ const rawCourses = data?.kbList || data?.datas?.kbList || data?.rows;
|
|
|
+ if (!Array.isArray(rawCourses) || !rawCourses.length) return null;
|
|
|
+ return mergeAndDistinctCourses(normalizeCourses(rawCourses));
|
|
|
+ } catch (error) {
|
|
|
+ console.warn("NTU class course API request failed:", error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function runImport() {
|
|
|
+ try {
|
|
|
+ if (!await window.shiguangBridgePromise.showAlert("南通大学课表导入", "导入前请确保已登录南通大学教务系统。", "开始导入")) return;
|
|
|
+ const formAction = document.querySelector("#ajaxForm")?.getAttribute("action") || "";
|
|
|
+ const isPersonalPage = /xskbcx_cxXskbcxIndex/i.test(formAction) || /\/kbcx\/xskbcx/i.test(window.location.pathname);
|
|
|
+ const pageCourses = isPersonalPage ? await fetchApiCourses() : await fetchClassApiCourses();
|
|
|
+ if (pageCourses?.length) {
|
|
|
+ const pageWeeks = pageCourses.flatMap((course) => course.weeks);
|
|
|
+ const semesterStartDate = await fetchSemesterStartDate();
|
|
|
+ if (!semesterStartDate) throw new Error("未获取到开学日期,无法判断令时");
|
|
|
+ const startMonth = Number(semesterStartDate.slice(5, 7));
|
|
|
+ if (!Number.isInteger(startMonth) || startMonth < 1 || startMonth > 12) throw new Error(`开学日期格式无效:${semesterStartDate}`);
|
|
|
+ const timeSlots = startMonth <= 6 ? WINTER_TIME_SLOTS : SUMMER_TIME_SLOTS;
|
|
|
+ const courseConfig = { semesterTotalWeeks: Math.max(19, ...pageWeeks), defaultClassDuration: 40, firstDayOfWeek: 1 };
|
|
|
+ courseConfig.semesterStartDate = semesterStartDate;
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(courseConfig));
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(pageCourses));
|
|
|
+ window.shiguangBridge.showToast(`导入成功:${pageCourses.length} 条课程安排`);
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ window.shiguangBridge.showToast("当前页面未找到课表,请先打开个人或班级课表页面。");
|
|
|
+ } catch (error) {
|
|
|
+ console.error("NTU adapter error", error);
|
|
|
+ window.shiguangBridge.showToast(`导入失败:${error.message}`);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+runImport();
|
|
|
+})();
|