| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- // 南华大学(http://jwzx.usc.edu.cn:8924)拾光课程表适配脚本
- const BASE_URL = "http://jwzx.usc.edu.cn:8924";
- const [URL_COURSE, URL_CALENDAR] = [`${BASE_URL}/jsxsd/xskb/xskb_list.do`, `${BASE_URL}/jsxsd/jxzl/jxzl_query`];
- // 工具函数
- const parseSlots = arr => arr.map((s, i) => ({ number: i + 1, startTime: s.slice(0, 5), endTime: s.slice(6) }));
- const TIME_SLOTS_SUMMER = parseSlots(["08:00-08:45","08:55-09:40","10:00-10:45","10:55-11:40","15:00-15:45","15:55-16:40","17:00-17:45","17:55-18:40","20:00-20:45","20:55-21:40"]);
- const TIME_SLOTS_WINTER = parseSlots(["08:20-09:05","09:15-10:00","10:20-11:05","11:15-12:00","14:30-15:15","15:25-16:10","16:30-17:15","17:25-18:10","19:30-20:15","20:25-21:10"]);
- const pad2 = n => String(n).padStart(2, "0");
- const WEEK_MAP = { 一:1, 二:2, 三:3, 四:4, 五:5, 六:6, 日:7, 天:7 };
- const parseWeeks = str => [...new Set((str || "").split("(")[0].split(",").flatMap(s => {
- const [st, ed] = s.split("-").map(Number);
- return (st && ed) ? Array.from({length: ed - st + 1}, (_, i) => st + i) : [st].filter(n => !isNaN(n));
- }))].sort((a, b) => a - b);
- const parseSectionRange = text => {
- const [_, s, e] = String(text || "").match(/(\d+)\s*(?:-\s*(\d+))?/) || [];
- return s ? { start: +s, end: +(e || s) } : null;
- };
- function parseDayColumns(table) {
- for (const row of table.rows) {
- const days = [...row.children].filter(el => /^(TH|TD)$/.test(el.tagName))
- .map(el => WEEK_MAP[el.textContent.trim().match(/^星期([一二三四五六日天])$/)?.[1]] || null);
- if (days.filter(Boolean).length >= 5) return days.filter(d => d !== null);
- }
- return null;
- }
- function parseCellCourses(cell, selector, day, rowSec) {
- const block = cell.querySelector(selector);
- if (!block?.innerHTML.trim() || block.innerHTML.trim() === " ") return [];
- return block.innerHTML.split(/---------------------?/).flatMap(part => {
- if (!part.trim()) return [];
- const temp = document.createElement("div");
- temp.innerHTML = part;
- let name = "";
- for (const node of temp.childNodes) {
- if (node.tagName === "FONT") break;
- if (node.nodeType === 3) name += node.textContent.replace(/\s+/g, " ");
- }
- if (!(name = name.trim())) return [];
- const textOf = sel => (temp.querySelector(sel)?.textContent || "").trim();
- const teacher = textOf("font[title='老师'], font[title='教师']").replace("任课教师:", "") || "未知教师";
- const position = textOf("font[title='教室']") || "未知地点";
- const weekStr = textOf("font[title='周次(节次)']");
- const nums = (weekStr.match(/\[([\d\-,\s]+?)节\]/)?.[1] || "").split(/[^\d]+/).map(Number).filter(n => n > 0);
- const startSection = nums.length ? Math.min(...nums) : (rowSec?.start || 0);
- const endSection = nums.length ? Math.max(...nums) : (rowSec?.end || 0);
- const weeks = parseWeeks(weekStr);
- return (weeks.length && startSection) ? [{ name, teacher, position, day, startSection, endSection: Math.max(startSection, endSection), weeks }] : [];
- });
- }
- // 合并函数
- function mergeAndDistinctCourses(courses) {
- if (!courses?.length) return [];
- const key1 = c => [c.name, c.teacher, c.position, c.day, c.weeks.join()].join("|");
- const key2 = c => [c.name, c.teacher, c.position, c.day, c.startSection, c.endSection].join("|");
- return courses.map(c => ({...c, weeks: [...c.weeks].sort((a, b) => a - b)}))
- .sort((a, b) => key1(a).localeCompare(key1(b)) || a.startSection - b.startSection)
- .reduce((acc, n) => { // 阶段1:合并连续/重复节次
- const cur = acc[acc.length - 1];
- if (cur && key1(cur) === key1(n)) {
- if (cur.endSection + 1 === n.startSection) cur.endSection = n.endSection;
- else if (cur.startSection !== n.startSection || cur.endSection !== n.endSection) acc.push(n);
- } else acc.push(n);
- return acc;
- }, [])
- .sort((a, b) => key2(a).localeCompare(key2(b)))
- .reduce((acc, n) => { // 阶段2:合并相同节次的周次
- const cur = acc[acc.length - 1];
- if (cur && key2(cur) === key2(n)) cur.weeks = [...new Set([...cur.weeks, ...n.weeks])].sort((a,b) => a - b);
- else acc.push(n);
- return acc;
- }, []);
- }
- const parseTimetableToModel = doc => {
- const table = doc.getElementById("kbtable");
- if (!table) return [];
- const dayMap = parseDayColumns(table) || [1, 2, 3, 4, 5, 6, 7];
- const selector = table.querySelector("div.kbcontent") ? "div.kbcontent" : "div.kbcontent1";
- return mergeAndDistinctCourses([...table.rows].flatMap(row => {
- const cells = [...row.querySelectorAll("td")];
- const dayCells = cells.length === dayMap.length + 1 ? cells.slice(1) : cells;
- if (dayCells.length !== dayMap.length) return [];
- const rowSec = parseSectionRange(row.querySelector("th")?.textContent);
- return dayCells.flatMap((cell, i) => dayMap[i] ? parseCellCourses(cell, selector, dayMap[i], rowSec) : []);
- }));
- };
- // 网络请求与解析
- const parseHtml = html => new DOMParser().parseFromString(html, "text/html");
- const req = async (url, opts) => {
- const res = await fetch(url, { credentials: "include", ...opts });
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
- return res.text();
- };
- const formOpts = body => ({ method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body });
- const requestCoursePage = semId => req(URL_COURSE, formOpts(`jx0404id=&cj0701id=&zc=&demo=&xnxq01id=${semId||""}`));
- const requestCoursePageByGet = semId => req(`${URL_COURSE}?xnxq01id=${encodeURIComponent(semId||"")}&zc=&cj0701id=&demo=`, { method: "GET" });
- const requestCalendarPage = semId => req(URL_CALENDAR, formOpts(`xnxq01id=${semId}`));
- function parseSemesterOptions(html) {
- const select = parseHtml(html).getElementById("xnxq01id");
- if (!select) return { list: [], defaultIndex: -1 };
- const all = [...select.options].map(o => ({ value: o.value, label: o.textContent.trim(), selected: o.selected })).filter(o => o.value);
- const idx = Math.max(0, all.findIndex(o => o.selected));
- const start = Math.max(0, idx - 3);
- return { list: all.slice(start, idx + 4), defaultIndex: idx - start };
- }
- async function fetchCourses(semId) {
- let html = await requestCoursePage(semId).catch(() => null);
- let courses = html ? parseTimetableToModel(parseHtml(html)) : [];
- if (!courses.length) {
- html = await requestCoursePageByGet(semId).catch(() => null);
- if (html) courses = parseTimetableToModel(parseHtml(html));
- }
- return courses;
- }
- async function fetchSemesterInfo(semId) {
- try {
- const doc = parseHtml(await requestCalendarPage(semId));
- const dates = [...doc.querySelectorAll("td[title]")].map(td => {
- const m = td.getAttribute("title")?.match(/(\d{4})年(\d{1,2})月(\d{1,2})日?/);
- return m ? `${m[1]}-${pad2(m[2])}-${pad2(m[3])}` : null;
- }).filter(Boolean).sort();
- const totalWeeks = Math.max(0, ...[...doc.querySelectorAll("#kbtable tr td:first-child")].map(td => parseInt(td.textContent, 10)).filter(n => !isNaN(n)));
- return { startDate: dates[0] || null, totalWeeks: totalWeeks || null };
- } catch { return { startDate: null, totalWeeks: null }; }
- }
- // 全局校验
- window.validateStartDateInput = input => !input?.trim() || /^\d{4}-\d{1,2}-\d{1,2}$/.test(input.trim()) ? false : "请输入形如 2026-09-01 的日期,或留空跳过";
- async function promptSemesterStartDate() {
- const input = await window.shiguangBridgePromise.showPrompt("设置开学日期", "未能自动获取开学日期...\n请输入本学期第一周的星期一日期(如 2026-09-01),留空则跳过:", "", "validateStartDateInput");
- const m = input?.trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
- return m ? `${m[1]}-${pad2(m[2])}-${pad2(m[3])}` : null;
- }
- const computeTotalWeeks = courses => {
- const max = Math.max(0, ...courses.flatMap(c => c.weeks || []));
- return (max > 0 && max <= 40) ? max : 20;
- };
- function buildComboSchedule(semId, label) {
- const [_, y0, y1, term] = String(semId || "").match(/^(\d{4})-(\d{4})-(\d)$/) || [];
- if (!term) return null;
- const Y = term === "2" ? +y1 : +y0;
- const winter = { name: "秋冬季作息", startDate: `${term === "2" ? Y - 1 : Y}-10-01`, endDate: `${term === "2" ? Y : Y + 1}-04-30`, timeSlots: [] };
- return {
- name: `南华大学 ${label || semId} 作息方案`,
- publicSchedules: [ { name: "夏秋季作息", startDate: `${Y}-05-01`, endDate: `${Y}-09-30`, timeSlots: TIME_SLOTS_SUMMER }, winter ].sort((a,b) => a.startDate.localeCompare(b.startDate))
- };
- }
- const saveAppConfig = (startDate, weeks) => window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({ semesterTotalWeeks: weeks || 20, firstDayOfWeek: 7, defaultClassDuration: 45, ...(startDate && { semesterStartDate: startDate }) }));
- const saveAppTimeSlots = () => window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(TIME_SLOTS_WINTER));
- // 主流程
- (async function runImportFlow() {
- const bridge = window.shiguangBridge, pBridge = window.shiguangBridgePromise;
- try {
- if (!await pBridge.showAlert("提示", "请确保已登录教务在线,并已打开「学期理论课表」页面。\n是否开始导入?", "开始")) return;
- bridge.showToast("正在获取学期列表...");
- const { list, defaultIndex } = parseSemesterOptions(await requestCoursePage(""));
- if (!list.length) return bridge.showToast("未获取到学期列表,请检查登录状态。");
- const selIdx = await pBridge.showSingleSelection("选择学期", JSON.stringify(list.map(s => s.label)), defaultIndex);
- if (selIdx === null) return bridge.showToast("已取消导入。");
- bridge.showToast("正在请求课程数据...");
- const sem = list[selIdx], courses = await fetchCourses(sem.value);
- if (!courses.length) return bridge.showToast("未发现课程,请检查学期选择或登录状态。");
- bridge.showToast("正在获取学期信息...");
- const { startDate, totalWeeks } = await fetchSemesterInfo(sem.value);
- const finalStart = startDate || await promptSemesterStartDate();
- await saveAppConfig(finalStart, totalWeeks || computeTotalWeeks(courses));
- await pBridge.saveImportedCourses(JSON.stringify(courses));
- try {
- if (await saveAppTimeSlots()) {
- const combo = buildComboSchedule(sem.value, sem.label);
- if (combo) await pBridge.saveComboSchedule(JSON.stringify(combo));
- }
- } catch { bridge.showToast("时间段或组合作息导入异常,已跳过。"); }
- bridge.showToast(`成功导入 ${courses.length} 条课程`);
- bridge.notifyTaskCompletion();
- } catch (e) { bridge.showToast("异常: " + e.message); }
- })();
|