| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- // 成都医学院教务(乘方教务)适配器
- // 流程:选一次学期(课表与考试共用)→ 导入课表 → 询问是否导入考试 → 合并保存
- // 接口:
- // GET /new/student/xsgrkb/week.page 课表页(学期下拉 + 作息表)
- // POST /new/student/xsgrkb/getCalendarWeekDatas 整学期课程数据
- // POST /new/student/xsksrw/paginateXsksrw 学生考试任务
- // 周次字符串
- function parseWeeks(weekStr) {
- if (!weekStr) return [];
- 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 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 venueMap;
- }
- // 课程地点
- function resolvePosition(item) {
- const primary = String(item.jxcdmc || "").trim();
- if (primary) return primary;
- if (String(item.bapjxcd || "") === "1") return "不用场地";
- return "待定";
- }
- 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();
- apiJson.data.forEach(item => {
- const day = parseInt(item.xq, 10);
- const startSection = parseInt(item.ps, 10);
- const endSection = parseInt(item.pe, 10);
- 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}`].join("__");
- const existing = courseMap.get(key);
- if (existing) existing.weeks = [...new Set([...existing.weeks, ...course.weeks])].sort((a, b) => a - b);
- else courseMap.set(key, course);
- });
- });
- 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)
- );
- }
- // 考试安排数据
- 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,
- 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 exams;
- }
- // 从 week.page 源码提取作息表
- function parseBusinessHoursFromHtml(htmlText) {
- const match = htmlText.match(/var\s+businessHours\s*=\s*\$\.parseJSON\('(\[.*?\])'\);/);
- 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 { slots, map };
- }
- // 插入午间段期中考试时间
- 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) {
- const selectElem = doc.getElementById("xnxqdm");
- if (!selectElem) return null;
- const semesters = [];
- const semesterValues = [];
- let defaultIndex = 0;
- 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;
- 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() {
- return await window.shiguangBridgePromise.showAlert(
- "成都医学院教务导入",
- "请先确保已登录教务系统,再继续导入。",
- "我已登录"
- );
- }
- // 从页面已有学期中选择目标学期
- async function selectSemester(semesterOptions) {
- const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
- "选择学期",
- JSON.stringify(semesterOptions.semesters),
- semesterOptions.defaultIndex
- );
- if (selectedIndex === null || selectedIndex < 0) return null;
- return {
- label: semesterOptions.semesters[selectedIndex],
- value: semesterOptions.semesterValues[selectedIndex]
- };
- }
- // 询问是否同时导入考试
- async function askImportExams() {
- const bridge = window.shiguangBridgePromise;
- if (!bridge || typeof bridge.showAlert !== "function") return true;
- return await bridge.showAlert(
- "导入考试安排",
- "是否同时导入本学期的考试安排?\n(期中/期末/补考将显示在课表对应日期)",
- "确定导入"
- );
- }
- // 获取课表页 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",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
- "X-Requested-With": "XMLHttpRequest"
- },
- credentials: "include",
- body: formData.toString()
- });
- if (!response.ok) throw new Error(`请求失败(HTTP ${response.status})`);
- return response;
- }
- // 请求指定学期的课程数据
- 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();
- }
- // 分页拉取指定学期的全部考试任务
- 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) {
- await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
- }
- async function saveTimeSlots(timeSlots) {
- if (timeSlots.length === 0) return;
- await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
- }
- // 编排导入流程:提示 → 选学期 → 请求课表与考试 → 合并保存课程与作息时间
- async function runImportFlow() {
- try {
- const confirmed = await promptUserToStart();
- if (!confirmed) { window.shiguangBridge.showToast("导入已取消"); return; }
- const pageHtml = await fetchSchedulePage();
- const semesterOptions = extractSemesterOptions(new DOMParser().parseFromString(pageHtml, "text/html"));
- if (!semesterOptions) throw new Error("未找到学期列表,请先登录教务系统");
- const semester = await selectSemester(semesterOptions);
- if (!semester) { window.shiguangBridge.showToast("导入已取消"); return; }
- const { slots, map: slotMap } = parseBusinessHoursFromHtml(pageHtml);
- window.shiguangBridge.showToast(`正在获取 ${semester.label} 的课表...`);
- const courses = parseCourseList(await fetchCourseData(semester.value), slotMap);
- if (courses.length === 0) {
- await window.shiguangBridgePromise.showAlert(
- "提示",
- "该学期没有获取到课程数据,请检查登录状态和所选学期。",
- "确定"
- );
- return;
- }
- 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;
- });
- window.shiguangBridge.showToast("正在获取考试安排...");
- exams.push(...parseExamList(await fetchExamData(semester.value), timeSlots));
- if (exams.length === 0) window.shiguangBridge.showToast("该学期暂时没有考试安排");
- }
- await saveCourses([...courses, ...exams]);
- try {
- await saveTimeSlots(timeSlots);
- } catch (error) {
- window.shiguangBridge.showToast(`课程已导入,作息时间导入失败:${error.message}`);
- }
- const examTip = exams.length > 0 ? `成功导入 ${exams.length} 门考试` : "导入完成";
- window.shiguangBridge.showToast(examTip);
- window.shiguangBridge.notifyTaskCompletion();
- } catch (error) {
- await window.shiguangBridgePromise.showAlert(
- "导入失败",
- error.message || String(error),
- "确定"
- );
- }
- }
- runImportFlow();
|