| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- // 济宁医学院教务(乘方教务 · 旧版 .action 接口)适配器
- const PRESET_TIME_SLOTS = [
- { number: 1, startTime: "08:00", endTime: "08:40" },
- { number: 2, startTime: "08:50", endTime: "09:30" },
- { number: 3, startTime: "09:50", endTime: "10:30" },
- { number: 4, startTime: "10:40", endTime: "11:20" },
- { number: 5, startTime: "11:30", endTime: "12:10" },
- { 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: "17:30", endTime: "18:10" },
- { number: 11, startTime: "19:00", endTime: "19:40" },
- { number: 12, startTime: "19:50", endTime: "20:30" },
- { number: 13, startTime: "20:50", endTime: "21:30" },
- { number: 14, startTime: "21:40", endTime: "22:20" }
- ];
- function uniqueSortedNumbers(numbers) {
- return [...new Set(numbers)].sort((a, b) => a - b);
- }
- // 支持 "10,7,8,9"、"1-4"、"1-4,7" 等周次写法。
- function parseWeeks(weekStr) {
- if (!weekStr) return [];
- const weeks = [];
- String(weekStr).replace(/[,、]/g, ",").split(",").forEach(part => {
- const text = part.trim().replace(/周/g, "");
- const range = text.match(/^(\d+)\s*[-~]\s*(\d+)$/);
- if (range) {
- const start = parseInt(range[1], 10);
- const end = parseInt(range[2], 10);
- if (start > 0 && end >= start) {
- for (let week = start; week <= end; week += 1) weeks.push(week);
- }
- return;
- }
- const week = parseInt(text, 10);
- if (!isNaN(week) && week > 0) weeks.push(week);
- });
- return uniqueSortedNumbers(weeks);
- }
- function parseSections(sectionStr) {
- return uniqueSortedNumbers(
- String(sectionStr || "")
- .replace(/[,、]/g, ",")
- .split(",")
- .map(section => parseInt(section.trim(), 10))
- .filter(section => !isNaN(section) && section > 0)
- );
- }
- // 反斜杠表示合并教室(如 "B205\206教室"),逗号才是不同地点的分隔符。
- function resolvePosition(raw) {
- const position = String(raw || "").replace(/\\/g, "/").replace(/\s+/g, " ").trim();
- return position || "待定";
- }
- function cleanTeacher(raw) {
- const teacher = String(raw || "")
- .split(/[,,、]/)
- .map(name => name.trim())
- .filter(Boolean)
- .join("、");
- return teacher || "未知";
- }
- function courseKey(course) {
- return [
- course.name,
- course.teacher,
- course.position,
- course.day,
- course.startSection,
- course.endSection
- ].join("__");
- }
- function addCourse(courseMap, item, weeks) {
- const day = parseInt(item.xq, 10);
- const sections = parseSections(item.jcdm2);
- const allWeeks = uniqueSortedNumbers(weeks);
- if (!item.kcmc || sections.length === 0 || allWeeks.length === 0 ||
- isNaN(day) || day < 1 || day > 7) return;
- const course = {
- name: String(item.kcmc).trim(),
- teacher: cleanTeacher(item.teaxms),
- position: resolvePosition(item.jxcdmc || item.jxcdmcs),
- day,
- startSection: Math.min(...sections),
- endSection: Math.max(...sections),
- weeks: allWeeks
- };
- const key = courseKey(course);
- const existing = courseMap.get(key);
- if (existing) {
- existing.weeks = uniqueSortedNumbers([...existing.weeks, ...course.weeks]);
- } else {
- courseMap.set(key, course);
- }
- }
- function sortCourses(courses) {
- return courses.sort((a, b) =>
- a.day - b.day ||
- a.startSection - b.startSection ||
- a.endSection - b.endSection ||
- a.name.localeCompare(b.name)
- );
- }
- // 从整学期课表解析课程,作为周课表接口不可用时的回退。
- function parseCourseList(kbxx) {
- if (!Array.isArray(kbxx)) throw new Error("课表接口返回格式不正确");
- const courseMap = new Map();
- kbxx.forEach(item => {
- addCourse(courseMap, item, parseWeeks(item.zcs));
- });
- return sortCourses(Array.from(courseMap.values()));
- }
- // 日历接口按周返回精确教室;同一课程换教室时,按周次拆成多个课程项。
- function parseWeeklyCourseList(rows) {
- if (!Array.isArray(rows)) return [];
- const courseMap = new Map();
- rows.forEach(({ item, week }) => {
- const actualWeek = parseInt(week, 10);
- if (isNaN(actualWeek) || actualWeek < 1) return;
- addCourse(courseMap, item, [actualWeek]);
- });
- return sortCourses(Array.from(courseMap.values()));
- }
- function maxWeekOf(kbxx) {
- return kbxx.reduce((maxWeek, item) => {
- const weeks = parseWeeks(item.zcs);
- return Math.max(maxWeek, ...(weeks.length ? weeks : [0]));
- }, 0);
- }
- // 读取课表页中的学期下拉框
- 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() {
- const confirmed = await window.shiguangBridgePromise.showAlert(
- "济宁医学院教务导入",
- "请先确保已登录教务系统,再继续导入。",
- "我已登录"
- );
- if (!confirmed) {
- window.shiguangBridge.showToast("用户取消了导入。");
- return null;
- }
- return true;
- }
- // 选择学期
- async function selectSemester(semesterOptions) {
- const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
- "选择学期",
- JSON.stringify(semesterOptions.semesters),
- semesterOptions.defaultIndex
- );
- if (selectedIndex === null || selectedIndex === undefined) return null;
- const index = Number(selectedIndex);
- if (!Number.isInteger(index) || index < 0 || index >= semesterOptions.semesters.length) return null;
- return {
- label: semesterOptions.semesters[index],
- value: semesterOptions.semesterValues[index]
- };
- }
- // 拉取课表页 HTML(含学期列表)
- async function fetchSchedulePage() {
- const response = await fetch("/xsgrkbcx!getXsgrbkList.action", { method: "GET", credentials: "include" });
- if (!response.ok) throw new Error(`无法打开课表页面(HTTP ${response.status})`);
- return response.text();
- }
- // 拉取指定学期课表(HTML 内嵌 var kbxx=[...])
- async function fetchCourseData(xnxqdm) {
- const response = await fetch(
- `/xsgrkbcx!xsAllKbList.action?xnxqdm=${encodeURIComponent(xnxqdm)}`,
- { method: "GET", credentials: "include" }
- );
- if (!response.ok) throw new Error(`课表请求失败(HTTP ${response.status})`);
- const htmlText = await response.text();
- const match = htmlText.match(/var\s+kbxx\s*=\s*(\[[\s\S]*?\]);/);
- if (!match) throw new Error("课表数据解析失败,请检查登录状态");
- let kbxx;
- try {
- kbxx = JSON.parse(match[1]);
- } catch (error) {
- throw new Error(`课表数据解析失败:${error.message}`);
- }
- return kbxx;
- }
- function normalizeDate(raw) {
- const match = String(raw || "").match(/^(\d{4}-\d{2}-\d{2})/);
- return match ? match[1] : null;
- }
- function shiftDate(dateText, dayOffset) {
- const match = String(dateText || "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
- if (!match) return null;
- const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
- date.setDate(date.getDate() + dayOffset);
- return [
- date.getFullYear(),
- String(date.getMonth() + 1).padStart(2, "0"),
- String(date.getDate()).padStart(2, "0")
- ].join("-");
- }
- function extractWeekStartDate(payload) {
- const dateRows = Array.isArray(payload) && Array.isArray(payload[1]) ? payload[1] : [];
- const monday = dateRows.find(item => parseInt(item.xqmc, 10) === 1) || dateRows[0];
- return normalizeDate(monday && monday.rq);
- }
- // 获取某一周的日历数据,jxcdmc 是该周实际使用的教室。
- async function fetchWeeklyCourseData(xnxqdm, week) {
- const response = await fetch(
- `/xsgrkbcx!getKbRq.action?xnxqdm=${encodeURIComponent(xnxqdm)}&zc=${encodeURIComponent(week)}`,
- { method: "GET", credentials: "include" }
- );
- if (!response.ok) throw new Error(`第${week}周课表请求失败(HTTP ${response.status})`);
- const payload = await response.json();
- const rows = Array.isArray(payload) && Array.isArray(payload[0]) ? payload[0] : [];
- return {
- rows: rows.map(item => ({ item, week })),
- weekStartDate: extractWeekStartDate(payload)
- };
- }
- async function fetchWeeklyCourses(xnxqdm, kbxx) {
- const weeks = Array.from({ length: maxWeekOf(kbxx) }, (_, index) => index + 1);
- if (weeks.length === 0) return { rows: [], successfulWeeks: [], semesterStartDate: null };
- const results = await Promise.all(weeks.map(async week => {
- try {
- const data = await fetchWeeklyCourseData(xnxqdm, week);
- return { week, ...data };
- } catch (error) {
- console.warn(`读取第${week}周课表失败`, error);
- return { week, rows: null, weekStartDate: null };
- }
- }));
- const datedWeek = results.find(result => result.weekStartDate);
- const semesterStartDate = datedWeek
- ? shiftDate(datedWeek.weekStartDate, -7 * (datedWeek.week - 1))
- : null;
- return {
- rows: results.flatMap(result => result.rows || []),
- successfulWeeks: results.filter(result => result.rows !== null).map(result => result.week),
- semesterStartDate
- };
- }
- function mergeWeeklyAndFallbackCourses(kbxx, weeklyData) {
- if (weeklyData.rows.length === 0) return parseCourseList(kbxx);
- const courseMap = new Map();
- parseWeeklyCourseList(weeklyData.rows).forEach(course => {
- courseMap.set(courseKey(course), course);
- });
- const successfulWeeks = new Set(weeklyData.successfulWeeks);
- if (successfulWeeks.size === weeklyData.successfulWeeks.length &&
- successfulWeeks.size >= maxWeekOf(kbxx)) return sortCourses(Array.from(courseMap.values()));
- // 仅为请求失败的周次使用整学期数据,避免覆盖周课表中的精确教室。
- kbxx.forEach(item => {
- const weeks = parseWeeks(item.zcs).filter(week => !successfulWeeks.has(week));
- if (weeks.length > 0) addCourse(courseMap, item, weeks);
- });
- return sortCourses(Array.from(courseMap.values()));
- }
- async function saveCourseConfig(config) {
- await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
- }
- async function saveCourses(courses) {
- await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
- }
- async function savePresetTimeSlots() {
- await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(PRESET_TIME_SLOTS));
- }
- // 主流程:提示 → 选学期 → 拉课表 → 保存课程与作息
- async function runImportFlow() {
- try {
- const alertConfirmed = await promptUserToStart();
- if (!alertConfirmed) 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; }
- window.shiguangBridge.showToast(`正在获取 ${semester.label} 的课表...`);
- const allCourseData = await fetchCourseData(semester.value);
- const weeklyData = await fetchWeeklyCourses(semester.value, allCourseData);
- const courses = mergeWeeklyAndFallbackCourses(allCourseData, weeklyData);
- if (courses.length === 0) {
- await window.shiguangBridgePromise.showAlert(
- "提示",
- "该学期没有获取到课程数据,请检查登录状态和所选学期。",
- "确定"
- );
- return;
- }
- const courseConfig = {
- semesterTotalWeeks: Math.max(maxWeekOf(allCourseData), 20),
- firstDayOfWeek: 1
- };
- if (weeklyData.semesterStartDate) {
- courseConfig.semesterStartDate = weeklyData.semesterStartDate;
- }
- await saveCourseConfig(courseConfig);
- await saveCourses(courses);
- try {
- await savePresetTimeSlots();
- } catch (error) {
- window.shiguangBridge.showToast(`课程已导入,作息时间导入失败:${error.message}`);
- }
- window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
- if (!weeklyData.semesterStartDate) {
- window.shiguangBridge.showToast("未获取到开学日期,请在应用内手动设置。");
- }
- window.shiguangBridge.notifyTaskCompletion();
- } catch (error) {
- await window.shiguangBridgePromise.showAlert(
- "导入失败",
- error.message || String(error),
- "确定"
- );
- }
- }
- runImportFlow();
|