|
@@ -1,271 +1,220 @@
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-// 解析周次字符串,输出拾光课程表需要的数字数组。
|
|
|
|
|
|
|
+// 成都医学院教务(乘方教务)适配器
|
|
|
|
|
+// 流程:选一次学期(课表与考试共用)→ 导入课表 → 询问是否导入考试 → 合并保存
|
|
|
|
|
+// 接口:
|
|
|
|
|
+// GET /new/student/xsgrkb/week.page 课表页(学期下拉 + 作息表)
|
|
|
|
|
+// POST /new/student/xsgrkb/getCalendarWeekDatas 整学期课程数据
|
|
|
|
|
+// POST /new/student/xsksrw/paginateXsksrw 学生考试任务
|
|
|
|
|
+
|
|
|
|
|
+// 周次字符串
|
|
|
function parseWeeks(weekStr) {
|
|
function parseWeeks(weekStr) {
|
|
|
if (!weekStr) return [];
|
|
if (!weekStr) return [];
|
|
|
-
|
|
|
|
|
- const weeks = new Set();
|
|
|
|
|
- String(weekStr).split(',').forEach(part => {
|
|
|
|
|
- const trimmed = part.trim();
|
|
|
|
|
- if (!trimmed) return;
|
|
|
|
|
-
|
|
|
|
|
- if (trimmed.includes('-')) {
|
|
|
|
|
- const [start, end] = trimmed.split('-').map(n => parseInt(n, 10));
|
|
|
|
|
- if (!isNaN(start) && !isNaN(end) && start <= end) {
|
|
|
|
|
- for (let i = start; i <= end; i++) {
|
|
|
|
|
- weeks.add(i);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const week = parseInt(trimmed, 10);
|
|
|
|
|
- if (!isNaN(week) && week > 0) {
|
|
|
|
|
- weeks.add(week);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- return Array.from(weeks).sort((a, b) => a - b);
|
|
|
|
|
|
|
+ const weeks = weekStr.split(",").map(w => parseInt(w.trim(), 10)).filter(w => !isNaN(w) && w > 0);
|
|
|
|
|
+ return [...new Set(weeks)].sort((a, b) => a - b);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 从按周展开的场地字符串中提取去重后的教室名称。
|
|
|
|
|
-function extractLocationsFromJxcdmc2(jxcdmc2) {
|
|
|
|
|
- if (!jxcdmc2) return [];
|
|
|
|
|
-
|
|
|
|
|
- const locationSet = new Set();
|
|
|
|
|
- String(jxcdmc2).split(",").forEach(item => {
|
|
|
|
|
- const trimmed = item.trim();
|
|
|
|
|
- if (!trimmed) return;
|
|
|
|
|
-
|
|
|
|
|
- const match = trimmed.match(/^(.*?)-(\d+)$/);
|
|
|
|
|
- const location = (match ? match[1] : trimmed).trim();
|
|
|
|
|
- if (location && location !== "-1") {
|
|
|
|
|
- locationSet.add(location);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// 解析按周场地字符串
|
|
|
|
|
+function parseVenueWeeks(jxcdmc2) {
|
|
|
|
|
+ const venueMap = new Map();
|
|
|
|
|
+ let lastRoom = null;
|
|
|
|
|
+ String(jxcdmc2 || "").split(",").forEach(part => {
|
|
|
|
|
+ const match = part.trim().match(/^(.*?)-(\d+)$/);
|
|
|
|
|
+ if (!match) return;
|
|
|
|
|
+ const room = match[1].trim();
|
|
|
|
|
+ const week = parseInt(match[2], 10);
|
|
|
|
|
+ if (room) lastRoom = room;
|
|
|
|
|
+ if (!lastRoom || isNaN(week)) return;
|
|
|
|
|
+ if (!venueMap.has(lastRoom)) venueMap.set(lastRoom, []);
|
|
|
|
|
+ venueMap.get(lastRoom).push(week);
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
- return Array.from(locationSet);
|
|
|
|
|
|
|
+ return venueMap;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 按页面现有逻辑优先级生成课程地点文案。
|
|
|
|
|
|
|
+// 课程地点
|
|
|
function resolvePosition(item) {
|
|
function resolvePosition(item) {
|
|
|
const primary = String(item.jxcdmc || "").trim();
|
|
const primary = String(item.jxcdmc || "").trim();
|
|
|
- if (primary) {
|
|
|
|
|
- return primary;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (String(item.bapjxcd || "") === "1") {
|
|
|
|
|
- return "不用场地";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const fallbackLocations = extractLocationsFromJxcdmc2(item.jxcdmc2);
|
|
|
|
|
- if (fallbackLocations.length > 0) {
|
|
|
|
|
- return fallbackLocations.join("、");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ if (primary) return primary;
|
|
|
|
|
+ if (String(item.bapjxcd || "") === "1") return "不用场地";
|
|
|
return "待定";
|
|
return "待定";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 解析课表接口返回的数据并转换为课程数组。
|
|
|
|
|
-function parseCourseList(apiJson) {
|
|
|
|
|
- if (!apiJson || apiJson.code !== 0 || !Array.isArray(apiJson.data)) {
|
|
|
|
|
- throw new Error("课表接口返回格式不正确");
|
|
|
|
|
|
|
+function cleanTeacherName(raw) {
|
|
|
|
|
+ return String(raw || "").replace(/\[[^\]]*\]/g, "").trim();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 课表接口数据
|
|
|
|
|
+function parseCourseList(apiJson, slotMap) {
|
|
|
|
|
+ if (!apiJson) throw new Error("课表接口无响应");
|
|
|
|
|
+ if (apiJson.code !== 0) {
|
|
|
|
|
+ const message = String(apiJson.message || "").trim();
|
|
|
|
|
+ throw new Error(message || `课表接口返回错误(code=${apiJson.code})`);
|
|
|
}
|
|
}
|
|
|
|
|
+ if (!Array.isArray(apiJson.data)) throw new Error("课表接口返回格式不正确");
|
|
|
|
|
|
|
|
const courseMap = new Map();
|
|
const courseMap = new Map();
|
|
|
-
|
|
|
|
|
apiJson.data.forEach(item => {
|
|
apiJson.data.forEach(item => {
|
|
|
const day = parseInt(item.xq, 10);
|
|
const day = parseInt(item.xq, 10);
|
|
|
const startSection = parseInt(item.ps, 10);
|
|
const startSection = parseInt(item.ps, 10);
|
|
|
const endSection = parseInt(item.pe, 10);
|
|
const endSection = parseInt(item.pe, 10);
|
|
|
- const weeks = parseWeeks(item.zc);
|
|
|
|
|
-
|
|
|
|
|
- if (
|
|
|
|
|
- !item.kcmc ||
|
|
|
|
|
- isNaN(day) ||
|
|
|
|
|
- isNaN(startSection) ||
|
|
|
|
|
- isNaN(endSection) ||
|
|
|
|
|
- day < 1 ||
|
|
|
|
|
- day > 7 ||
|
|
|
|
|
- startSection > endSection ||
|
|
|
|
|
- weeks.length === 0
|
|
|
|
|
- ) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const allWeeks = parseWeeks(item.zc);
|
|
|
|
|
+ if (!item.kcmc || !allWeeks.length || isNaN(day) || isNaN(startSection) || isNaN(endSection) ||
|
|
|
|
|
+ day < 1 || day > 7 || startSection > endSection) return;
|
|
|
|
|
+
|
|
|
|
|
+ const teacher = cleanTeacherName(item.teaxms || item.pkr) || "未知";
|
|
|
|
|
+ const venues = parseVenueWeeks(item.jxcdmc2);
|
|
|
|
|
+ const venueEntries = venues.size > 0
|
|
|
|
|
+ ? Array.from(venues.entries(), ([position, weeks]) => ({ position, weeks: [...new Set(weeks)].sort((a, b) => a - b) }))
|
|
|
|
|
+ : [{ position: resolvePosition(item), weeks: allWeeks }];
|
|
|
|
|
+
|
|
|
|
|
+ venueEntries.forEach(({ position, weeks }) => {
|
|
|
|
|
+ const course = { name: item.kcmc.trim(), teacher, position, day, startSection, endSection, weeks };
|
|
|
|
|
+
|
|
|
|
|
+ const actualStart = String(item.qssj || "").slice(0, 5);
|
|
|
|
|
+ const actualEnd = String(item.jssj || "").slice(0, 5);
|
|
|
|
|
+ const expectedStart = slotMap[startSection] && slotMap[startSection].start;
|
|
|
|
|
+ const expectedEnd = slotMap[endSection] && slotMap[endSection].end;
|
|
|
|
|
+ if (actualStart && actualEnd && (actualStart !== expectedStart || actualEnd !== expectedEnd)) {
|
|
|
|
|
+ course.isCustomTime = true;
|
|
|
|
|
+ course.customStartTime = actualStart;
|
|
|
|
|
+ course.customEndTime = actualEnd;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const key = [course.name, teacher, position, day,
|
|
|
|
|
+ course.isCustomTime ? actualStart + actualEnd : `${startSection}-${endSection}`, weeks.join(",")].join("__");
|
|
|
|
|
+ if (!courseMap.has(key)) courseMap.set(key, course);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- const teacher = (item.teaxms || item.pkr || "").trim() || "未知";
|
|
|
|
|
- const position = resolvePosition(item);
|
|
|
|
|
|
|
+ return Array.from(courseMap.values()).sort((a, b) =>
|
|
|
|
|
+ a.day - b.day || a.startSection - b.startSection || a.endSection - b.endSection || a.name.localeCompare(b.name)
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- const key = [
|
|
|
|
|
- item.kcmc.trim(),
|
|
|
|
|
- teacher,
|
|
|
|
|
- position,
|
|
|
|
|
|
|
+// 考试安排数据
|
|
|
|
|
+function parseExamList(rows, slots) {
|
|
|
|
|
+ const exams = [];
|
|
|
|
|
+ rows.forEach(item => {
|
|
|
|
|
+ const day = parseInt(item.xq, 10);
|
|
|
|
|
+ const week = parseInt(item.zc, 10);
|
|
|
|
|
+ const [startTime, endTime] = String(item.kssj || "").split("--").map(part => part.trim().slice(0, 5));
|
|
|
|
|
+ const validTime = startTime && endTime && /^\d{2}:\d{2}$/.test(startTime) && /^\d{2}:\d{2}$/.test(endTime);
|
|
|
|
|
+ if (!item.kcmc || isNaN(day) || day < 1 || day > 7 || isNaN(week) || week < 1 || !validTime) return;
|
|
|
|
|
+
|
|
|
|
|
+ const examType = String(item.kslbmc || "").trim().replace(/考试$/, "");
|
|
|
|
|
+ const exam = {
|
|
|
|
|
+ name: `${item.kcmc.trim()}${examType ? `(${examType})` : ""}`,
|
|
|
|
|
+ teacher: "",
|
|
|
|
|
+ position: String(item.kscdmc || "").trim() || "待定",
|
|
|
day,
|
|
day,
|
|
|
- startSection,
|
|
|
|
|
- endSection,
|
|
|
|
|
- weeks.join(',')
|
|
|
|
|
- ].join("__");
|
|
|
|
|
-
|
|
|
|
|
- if (!courseMap.has(key)) {
|
|
|
|
|
- courseMap.set(key, {
|
|
|
|
|
- name: item.kcmc.trim(),
|
|
|
|
|
- teacher,
|
|
|
|
|
- position,
|
|
|
|
|
- day,
|
|
|
|
|
- startSection,
|
|
|
|
|
- endSection,
|
|
|
|
|
- weeks
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ weeks: [week]
|
|
|
|
|
+ };
|
|
|
|
|
+ const matched = slots && slots.find(s => s.startTime === startTime && s.endTime === endTime);
|
|
|
|
|
+ if (matched) {
|
|
|
|
|
+ exam.startSection = exam.endSection = matched.number;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ exam.isCustomTime = true;
|
|
|
|
|
+ exam.customStartTime = startTime;
|
|
|
|
|
+ exam.customEndTime = endTime;
|
|
|
}
|
|
}
|
|
|
|
|
+ exams.push(exam);
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
- return Array.from(courseMap.values()).sort((a, b) =>
|
|
|
|
|
- a.day - b.day ||
|
|
|
|
|
- a.startSection - b.startSection ||
|
|
|
|
|
- a.endSection - b.endSection ||
|
|
|
|
|
- a.name.localeCompare(b.name)
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ return exams;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 从 week.page 源码中提取学校真实作息时间。
|
|
|
|
|
|
|
+// 从 week.page 源码提取作息表
|
|
|
function parseBusinessHoursFromHtml(htmlText) {
|
|
function parseBusinessHoursFromHtml(htmlText) {
|
|
|
const match = htmlText.match(/var\s+businessHours\s*=\s*\$\.parseJSON\('(\[.*?\])'\);/);
|
|
const match = htmlText.match(/var\s+businessHours\s*=\s*\$\.parseJSON\('(\[.*?\])'\);/);
|
|
|
- if (!match || !match[1]) {
|
|
|
|
|
- return [];
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- let rawData;
|
|
|
|
|
- try {
|
|
|
|
|
- rawData = JSON.parse(match[1]);
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.warn("businessHours 解析失败", error);
|
|
|
|
|
- return [];
|
|
|
|
|
|
|
+ const slots = [];
|
|
|
|
|
+ const map = {};
|
|
|
|
|
+ if (match) {
|
|
|
|
|
+ JSON.parse(match[1]).forEach(item => {
|
|
|
|
|
+ const number = parseInt(item.jcdm, 10);
|
|
|
|
|
+ const startTime = String(item.qssj || "").slice(0, 5);
|
|
|
|
|
+ const endTime = String(item.jssj || "").slice(0, 5);
|
|
|
|
|
+ if (isNaN(number) || !startTime || !endTime) return;
|
|
|
|
|
+ slots.push({ number, startTime, endTime });
|
|
|
|
|
+ map[number] = { start: startTime, end: endTime };
|
|
|
|
|
+ });
|
|
|
|
|
+ slots.sort((a, b) => a.number - b.number);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return rawData
|
|
|
|
|
- .map(item => ({
|
|
|
|
|
- number: parseInt(item.jcdm, 10),
|
|
|
|
|
- startTime: String(item.qssj || "").slice(0, 5),
|
|
|
|
|
- endTime: String(item.jssj || "").slice(0, 5)
|
|
|
|
|
- }))
|
|
|
|
|
- .filter(item => !isNaN(item.number) && item.startTime && item.endTime)
|
|
|
|
|
- .sort((a, b) => a.number - b.number);
|
|
|
|
|
|
|
+ return { slots, map };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 从页面脚本中识别总周数上限,作为后续配置接入的线索。
|
|
|
|
|
-function extractWeekCountFromHtml(htmlText) {
|
|
|
|
|
- const loopMatch = htmlText.match(/for\s*\(\s*var\s+i\s*=\s*0\s*;\s*i\s*<\s*(\d+)\s*;\s*i\+\+\s*\)/);
|
|
|
|
|
- if (loopMatch) {
|
|
|
|
|
- const weekCount = parseInt(loopMatch[1], 10);
|
|
|
|
|
- if (!isNaN(weekCount) && weekCount > 0) {
|
|
|
|
|
- return weekCount;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return null;
|
|
|
|
|
|
|
+// 插入午间段期中考试时间
|
|
|
|
|
+function withLunchSlot(slots) {
|
|
|
|
|
+ return slots
|
|
|
|
|
+ .map(s => s.number >= 6 ? { ...s, number: s.number + 1 } : s)
|
|
|
|
|
+ .concat([{ number: 6, startTime: "12:15", endTime: "14:15" }])
|
|
|
|
|
+ .sort((a, b) => a.number - b.number);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 读取页面中的学期下拉框选项和值。
|
|
|
|
|
|
|
+// 读取页面中的学期下拉框
|
|
|
function extractSemesterOptions(doc) {
|
|
function extractSemesterOptions(doc) {
|
|
|
const selectElem = doc.getElementById("xnxqdm");
|
|
const selectElem = doc.getElementById("xnxqdm");
|
|
|
- if (!selectElem) {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ if (!selectElem) return null;
|
|
|
const semesters = [];
|
|
const semesters = [];
|
|
|
const semesterValues = [];
|
|
const semesterValues = [];
|
|
|
let defaultIndex = 0;
|
|
let defaultIndex = 0;
|
|
|
-
|
|
|
|
|
- Array.from(selectElem.querySelectorAll("option")).forEach((option, index) => {
|
|
|
|
|
- const label = option.innerText.trim();
|
|
|
|
|
- const value = option.value;
|
|
|
|
|
- if (!label || !value) return;
|
|
|
|
|
-
|
|
|
|
|
- semesters.push(label);
|
|
|
|
|
- semesterValues.push(value);
|
|
|
|
|
- if (option.selected || option.hasAttribute("selected")) {
|
|
|
|
|
- defaultIndex = index;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ Array.from(selectElem.querySelectorAll("option")).forEach(option => {
|
|
|
|
|
+ if (!option.value) return;
|
|
|
|
|
+ semesters.push(option.innerText.trim());
|
|
|
|
|
+ semesterValues.push(option.value);
|
|
|
|
|
+ if (option.selected || option.hasAttribute("selected")) defaultIndex = semesters.length - 1;
|
|
|
});
|
|
});
|
|
|
|
|
+ if (semesters.length === 0) return null;
|
|
|
|
|
|
|
|
- if (semesters.length === 0) {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return { semesters, semesterValues, defaultIndex };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 粗略判断当前是否已经处于个人课表页面。
|
|
|
|
|
-function isProbablySchedulePage() {
|
|
|
|
|
- const href = window.location.href;
|
|
|
|
|
- return /\/new\/student\/xsgrkb\/week\.page/i.test(href) || document.getElementById("xnxqdm") !== null;
|
|
|
|
|
|
|
+ const start = Math.max(0, defaultIndex - 1);
|
|
|
|
|
+ const end = Math.min(semesters.length, defaultIndex + 10);
|
|
|
|
|
+ return {
|
|
|
|
|
+ semesters: semesters.slice(start, end),
|
|
|
|
|
+ semesterValues: semesterValues.slice(start, end),
|
|
|
|
|
+ defaultIndex: defaultIndex - start
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 导入开始前提示用户先进入课表页面。
|
|
|
|
|
|
|
+// 导入前提示用户先登录教务系统
|
|
|
async function promptUserToStart() {
|
|
async function promptUserToStart() {
|
|
|
return await window.AndroidBridgePromise.showAlert(
|
|
return await window.AndroidBridgePromise.showAlert(
|
|
|
"成都医学院教务导入",
|
|
"成都医学院教务导入",
|
|
|
- "请先确保自己已经进入教务系统的课表页面,再继续导入。",
|
|
|
|
|
- "我已进入课表页"
|
|
|
|
|
|
|
+ "请先确保已登录教务系统,再继续导入。",
|
|
|
|
|
+ "我已登录"
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 获取课表页 HTML 和文档对象,优先复用当前页面。
|
|
|
|
|
-async function loadSchedulePageContext() {
|
|
|
|
|
- if (isProbablySchedulePage()) {
|
|
|
|
|
- return {
|
|
|
|
|
- htmlText: document.documentElement.outerHTML,
|
|
|
|
|
- doc: document,
|
|
|
|
|
- weekCount: extractWeekCountFromHtml(document.documentElement.outerHTML)
|
|
|
|
|
- };
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const response = await fetch("/new/student/xsgrkb/week.page", {
|
|
|
|
|
- method: "GET",
|
|
|
|
|
- credentials: "include"
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- if (!response.ok) {
|
|
|
|
|
- throw new Error(`无法打开课表页面(HTTP ${response.status})`);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const htmlText = await response.text();
|
|
|
|
|
- const parser = new DOMParser();
|
|
|
|
|
- const doc = parser.parseFromString(htmlText, "text/html");
|
|
|
|
|
- return {
|
|
|
|
|
- htmlText,
|
|
|
|
|
- doc,
|
|
|
|
|
- weekCount: extractWeekCountFromHtml(htmlText)
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 让用户从页面已有学期中选择一个目标学期。
|
|
|
|
|
|
|
+// 从页面已有学期中选择目标学期
|
|
|
async function selectSemester(semesterOptions) {
|
|
async function selectSemester(semesterOptions) {
|
|
|
const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
|
|
const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
|
|
|
"选择学期",
|
|
"选择学期",
|
|
|
JSON.stringify(semesterOptions.semesters),
|
|
JSON.stringify(semesterOptions.semesters),
|
|
|
semesterOptions.defaultIndex
|
|
semesterOptions.defaultIndex
|
|
|
);
|
|
);
|
|
|
-
|
|
|
|
|
- if (selectedIndex === null || selectedIndex < 0) {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
|
|
+ if (selectedIndex === null || selectedIndex < 0) return null;
|
|
|
return {
|
|
return {
|
|
|
label: semesterOptions.semesters[selectedIndex],
|
|
label: semesterOptions.semesters[selectedIndex],
|
|
|
value: semesterOptions.semesterValues[selectedIndex]
|
|
value: semesterOptions.semesterValues[selectedIndex]
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 请求指定学期的课程数据。
|
|
|
|
|
-async function fetchCourseData(xnxqdm) {
|
|
|
|
|
- const formData = new URLSearchParams();
|
|
|
|
|
- formData.append("xnxqdm", xnxqdm);
|
|
|
|
|
- formData.append("zc", "");
|
|
|
|
|
- formData.append("d1", "2020-01-01 00:00:00");
|
|
|
|
|
- formData.append("d2", "2040-01-01 00:00:00");
|
|
|
|
|
|
|
+// 询问是否同时导入考试
|
|
|
|
|
+async function askImportExams() {
|
|
|
|
|
+ const bridge = window.AndroidBridgePromise;
|
|
|
|
|
+ if (!bridge || typeof bridge.showAlert !== "function") return true;
|
|
|
|
|
+ return await bridge.showAlert(
|
|
|
|
|
+ "导入考试安排",
|
|
|
|
|
+ "是否同时导入本学期的考试安排?\n(期中/期末/补考将显示在课表对应日期)",
|
|
|
|
|
+ "确定导入"
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- const response = await fetch("/new/student/xsgrkb/getCalendarWeekDatas", {
|
|
|
|
|
|
|
+// 获取课表页 HTML(含学期列表与作息表)
|
|
|
|
|
+async function fetchSchedulePage() {
|
|
|
|
|
+ const response = await fetch("/new/student/xsgrkb/week.page", { method: "GET", credentials: "include" });
|
|
|
|
|
+ if (!response.ok) throw new Error(`无法打开课表页面(HTTP ${response.status})`);
|
|
|
|
|
+ return response.text();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 乘方统一表单 POST(课表/考试共用),附带 JSON 请求头与会话
|
|
|
|
|
+async function postForm(url, formData) {
|
|
|
|
|
+ const response = await fetch(url, {
|
|
|
method: "POST",
|
|
method: "POST",
|
|
|
headers: {
|
|
headers: {
|
|
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
@@ -274,80 +223,105 @@ async function fetchCourseData(xnxqdm) {
|
|
|
credentials: "include",
|
|
credentials: "include",
|
|
|
body: formData.toString()
|
|
body: formData.toString()
|
|
|
});
|
|
});
|
|
|
|
|
+ if (!response.ok) throw new Error(`请求失败(HTTP ${response.status})`);
|
|
|
|
|
+ return response;
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- if (!response.ok) {
|
|
|
|
|
- throw new Error(`课表请求失败(HTTP ${response.status})`);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+// 请求指定学期的课程数据
|
|
|
|
|
+async function fetchCourseData(xnxqdm) {
|
|
|
|
|
+ const year = parseInt(xnxqdm.slice(0, 4), 10);
|
|
|
|
|
+ const formData = new URLSearchParams();
|
|
|
|
|
+ formData.append("xnxqdm", xnxqdm);
|
|
|
|
|
+ formData.append("zc", "");
|
|
|
|
|
+ formData.append("d1", `${year}-08-01 00:00:00`);
|
|
|
|
|
+ formData.append("d2", `${year + 1}-08-31 23:59:59`);
|
|
|
|
|
+ return (await postForm("/new/student/xsgrkb/getCalendarWeekDatas", formData)).json();
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- return await response.json();
|
|
|
|
|
|
|
+// 分页拉取指定学期的全部考试任务
|
|
|
|
|
+async function fetchExamData(xnxqdm) {
|
|
|
|
|
+ const allRows = [];
|
|
|
|
|
+ const pageSize = 100;
|
|
|
|
|
+ let page = 1;
|
|
|
|
|
+
|
|
|
|
|
+ for (;;) {
|
|
|
|
|
+ const formData = new URLSearchParams();
|
|
|
|
|
+ formData.append("xnxqdm", xnxqdm);
|
|
|
|
|
+ formData.append("page", String(page));
|
|
|
|
|
+ formData.append("rows", String(pageSize));
|
|
|
|
|
+ formData.append("sort", "zc,xq,jcdm2");
|
|
|
|
|
+ formData.append("order", "asc");
|
|
|
|
|
+
|
|
|
|
|
+ const json = await (await postForm("/new/student/xsksrw/paginateXsksrw", formData)).json();
|
|
|
|
|
+ const rows = Array.isArray(json.rows) ? json.rows : [];
|
|
|
|
|
+ allRows.push(...rows);
|
|
|
|
|
+
|
|
|
|
|
+ const total = parseInt(json.total, 10);
|
|
|
|
|
+ if (!total || allRows.length >= total || rows.length === 0) break;
|
|
|
|
|
+ page += 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ return allRows;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 保存课程数据到应用。
|
|
|
|
|
async function saveCourses(courses) {
|
|
async function saveCourses(courses) {
|
|
|
await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 保存课表页面中解析出的作息时间。
|
|
|
|
|
async function saveTimeSlots(timeSlots) {
|
|
async function saveTimeSlots(timeSlots) {
|
|
|
- if (!timeSlots.length) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (timeSlots.length === 0) return;
|
|
|
await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 编排导入流程:提示、选学期、请求课程、保存课程与作息时间。
|
|
|
|
|
|
|
+// 编排导入流程:提示 → 选学期 → 请求课表与考试 → 合并保存课程与作息时间
|
|
|
async function runImportFlow() {
|
|
async function runImportFlow() {
|
|
|
try {
|
|
try {
|
|
|
const confirmed = await promptUserToStart();
|
|
const confirmed = await promptUserToStart();
|
|
|
- if (!confirmed) {
|
|
|
|
|
- AndroidBridge.showToast("已取消导入");
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- AndroidBridge.showToast("正在读取课表页面信息...");
|
|
|
|
|
- const pageContext = await loadSchedulePageContext();
|
|
|
|
|
- const semesterOptions = extractSemesterOptions(pageContext.doc);
|
|
|
|
|
|
|
+ if (!confirmed) { AndroidBridge.showToast("导入已取消"); return; }
|
|
|
|
|
|
|
|
- if (!semesterOptions) {
|
|
|
|
|
- throw new Error("未找到学期列表,请先进入教务系统课表页面后再试");
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const pageHtml = await fetchSchedulePage();
|
|
|
|
|
+ const semesterOptions = extractSemesterOptions(new DOMParser().parseFromString(pageHtml, "text/html"));
|
|
|
|
|
+ if (!semesterOptions) throw new Error("未找到学期列表,请先登录教务系统");
|
|
|
|
|
|
|
|
- const selectedSemester = await selectSemester(semesterOptions);
|
|
|
|
|
- if (!selectedSemester) {
|
|
|
|
|
- AndroidBridge.showToast("已取消导入");
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const semester = await selectSemester(semesterOptions);
|
|
|
|
|
+ if (!semester) { AndroidBridge.showToast("导入已取消"); return; }
|
|
|
|
|
|
|
|
- AndroidBridge.showToast(`正在获取 ${selectedSemester.label} 的课表...`);
|
|
|
|
|
- const apiJson = await fetchCourseData(selectedSemester.value);
|
|
|
|
|
- const courses = parseCourseList(apiJson);
|
|
|
|
|
|
|
+ const { slots, map: slotMap } = parseBusinessHoursFromHtml(pageHtml);
|
|
|
|
|
+ AndroidBridge.showToast(`正在获取 ${semester.label} 的课表...`);
|
|
|
|
|
+ const courses = parseCourseList(await fetchCourseData(semester.value), slotMap);
|
|
|
|
|
|
|
|
if (courses.length === 0) {
|
|
if (courses.length === 0) {
|
|
|
await window.AndroidBridgePromise.showAlert(
|
|
await window.AndroidBridgePromise.showAlert(
|
|
|
"提示",
|
|
"提示",
|
|
|
- "该学期没有获取到课程数据,请确认当前登录状态和所选学期是否正确。",
|
|
|
|
|
|
|
+ "该学期没有获取到课程数据,请检查登录状态和所选学期。",
|
|
|
"确定"
|
|
"确定"
|
|
|
);
|
|
);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const timeSlots = parseBusinessHoursFromHtml(pageContext.htmlText);
|
|
|
|
|
|
|
+ const exams = [];
|
|
|
|
|
+ let timeSlots = slots;
|
|
|
|
|
+ if (await askImportExams()) {
|
|
|
|
|
+ timeSlots = withLunchSlot(slots);
|
|
|
|
|
+ courses.forEach(c => {
|
|
|
|
|
+ if (c.startSection >= 6) c.startSection += 1;
|
|
|
|
|
+ if (c.endSection >= 6) c.endSection += 1;
|
|
|
|
|
+ });
|
|
|
|
|
+ AndroidBridge.showToast("正在获取考试安排...");
|
|
|
|
|
+ exams.push(...parseExamList(await fetchExamData(semester.value), timeSlots));
|
|
|
|
|
+ if (exams.length === 0) AndroidBridge.showToast("该学期暂时没有考试安排");
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- await saveCourses(courses);
|
|
|
|
|
|
|
+ await saveCourses([...courses, ...exams]);
|
|
|
try {
|
|
try {
|
|
|
await saveTimeSlots(timeSlots);
|
|
await saveTimeSlots(timeSlots);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
AndroidBridge.showToast(`课程已导入,作息时间导入失败:${error.message}`);
|
|
AndroidBridge.showToast(`课程已导入,作息时间导入失败:${error.message}`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (pageContext.weekCount) {
|
|
|
|
|
- console.log(`CMC: 从课表页识别到总周数 ${pageContext.weekCount} 周`);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- AndroidBridge.showToast(`成功导入 ${courses.length} 门课程`);
|
|
|
|
|
|
|
+ const examTip = exams.length > 0 ? `成功导入 ${exams.length} 门考试` : "导入完成";
|
|
|
|
|
+ AndroidBridge.showToast(examTip);
|
|
|
AndroidBridge.notifyTaskCompletion();
|
|
AndroidBridge.notifyTaskCompletion();
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error("CMC import failed:", error);
|
|
|
|
|
await window.AndroidBridgePromise.showAlert(
|
|
await window.AndroidBridgePromise.showAlert(
|
|
|
"导入失败",
|
|
"导入失败",
|
|
|
error.message || String(error),
|
|
error.message || String(error),
|