| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359 |
- // 解析周次字符串,输出拾光课程表需要的数字数组。
- function parseWeeks(weekStr) {
- 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);
- }
- // 从按周展开的场地字符串中提取去重后的教室名称。
- 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);
- }
- });
- return Array.from(locationSet);
- }
- // 按页面现有逻辑优先级生成课程地点文案。
- function resolvePosition(item) {
- 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("、");
- }
- return "待定";
- }
- // 解析课表接口返回的数据并转换为课程数组。
- function parseCourseList(apiJson) {
- if (!apiJson || apiJson.code !== 0 || !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 weeks = parseWeeks(item.zc);
- if (
- !item.kcmc ||
- isNaN(day) ||
- isNaN(startSection) ||
- isNaN(endSection) ||
- day < 1 ||
- day > 7 ||
- startSection > endSection ||
- weeks.length === 0
- ) {
- return;
- }
- const teacher = (item.teaxms || item.pkr || "").trim() || "未知";
- const position = resolvePosition(item);
- const key = [
- item.kcmc.trim(),
- teacher,
- position,
- day,
- startSection,
- endSection,
- weeks.join(',')
- ].join("__");
- if (!courseMap.has(key)) {
- courseMap.set(key, {
- name: item.kcmc.trim(),
- teacher,
- position,
- day,
- startSection,
- endSection,
- weeks
- });
- }
- });
- 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)
- );
- }
- // 从 week.page 源码中提取学校真实作息时间。
- function parseBusinessHoursFromHtml(htmlText) {
- 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 [];
- }
- 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);
- }
- // 从页面脚本中识别总周数上限,作为后续配置接入的线索。
- 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 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, 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;
- }
- });
- 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;
- }
- // 导入开始前提示用户先进入课表页面。
- async function promptUserToStart() {
- 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) {
- const selectedIndex = await window.AndroidBridgePromise.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 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");
- const response = await fetch("/new/student/xsgrkb/getCalendarWeekDatas", {
- 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 await response.json();
- }
- // 保存课程数据到应用。
- async function saveCourses(courses) {
- await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
- }
- // 保存课表页面中解析出的作息时间。
- async function saveTimeSlots(timeSlots) {
- if (!timeSlots.length) {
- return;
- }
- await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
- }
- // 编排导入流程:提示、选学期、请求课程、保存课程与作息时间。
- async function runImportFlow() {
- try {
- const confirmed = await promptUserToStart();
- if (!confirmed) {
- AndroidBridge.showToast("已取消导入");
- return;
- }
- AndroidBridge.showToast("正在读取课表页面信息...");
- const pageContext = await loadSchedulePageContext();
- const semesterOptions = extractSemesterOptions(pageContext.doc);
- if (!semesterOptions) {
- throw new Error("未找到学期列表,请先进入教务系统课表页面后再试");
- }
- const selectedSemester = await selectSemester(semesterOptions);
- if (!selectedSemester) {
- AndroidBridge.showToast("已取消导入");
- return;
- }
- AndroidBridge.showToast(`正在获取 ${selectedSemester.label} 的课表...`);
- const apiJson = await fetchCourseData(selectedSemester.value);
- const courses = parseCourseList(apiJson);
- if (courses.length === 0) {
- await window.AndroidBridgePromise.showAlert(
- "提示",
- "该学期没有获取到课程数据,请确认当前登录状态和所选学期是否正确。",
- "确定"
- );
- return;
- }
- const timeSlots = parseBusinessHoursFromHtml(pageContext.htmlText);
- await saveCourses(courses);
- try {
- await saveTimeSlots(timeSlots);
- } catch (error) {
- AndroidBridge.showToast(`课程已导入,作息时间导入失败:${error.message}`);
- }
- if (pageContext.weekCount) {
- console.log(`CMC: 从课表页识别到总周数 ${pageContext.weekCount} 周`);
- }
- AndroidBridge.showToast(`成功导入 ${courses.length} 门课程`);
- AndroidBridge.notifyTaskCompletion();
- } catch (error) {
- console.error("CMC import failed:", error);
- await window.AndroidBridgePromise.showAlert(
- "导入失败",
- error.message || String(error),
- "确定"
- );
- }
- }
- runImportFlow();
|