| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- // 西安工业大学(http://jwgl2018.xatu.edu.cn) 拾光课程表适配脚本,基于天津农学院适配脚本
- // 本校开发者适配,出现问题请提issues或者提交pr更改,这更加快速
- //感谢XingHeYuZhuan、aryunm、jursin...等的帮助,感谢trae的辅助
- function powerSplit(paramsRaw) {
- const args = [];
- let current = "";
- let depth = 0;
- let inQuote = false;
- let quoteChar = "";
- for (let i = 0; i < paramsRaw.length; i++) {
- let char = paramsRaw[i];
- if ((char === '"' || char === "'") && (i === 0 || paramsRaw[i - 1] !== '\\')) {
- if (!inQuote) { inQuote = true; quoteChar = char; }
- else if (char === quoteChar) { inQuote = false; }
- }
- if (!inQuote) {
- if (char === '(' || char === '[' || char === '{') depth++;
- if (char === ')' || char === ']' || char === '}') depth--;
- }
- if (char === ',' && depth === 0 && !inQuote) {
- args.push(cleanArg(current));
- current = "";
- } else {
- current += char;
- }
- }
- args.push(cleanArg(current));
- return args;
- }
- function cleanArg(s) {
- s = s.trim();
- return s.replace(/^["']|["']$/g, "");
- }
- /** 按课程、教师、地点和星期合并连续节次,保持原有50周范围。 */
- function mergeContinuousLessons(lessons) {
- const groups = new Map();
- for (const lesson of lessons) {
- const key = JSON.stringify([lesson.courseSequence, lesson.name, lesson.teacher, lesson.position, lesson.day]);
- if (!groups.has(key)) groups.set(key, { lesson, weeks: Array.from({ length: 50 }, () => new Set()) });
- const { weeks } = groups.get(key);
- for (const week of lesson.weeks) {
- if (week >= 50) continue;
- for (let section = lesson.startSection; section <= lesson.endSection; section++) weeks[week].add(section);
- }
- }
- const courses = [];
- for (const { lesson, weeks } of groups.values()) {
- const blocks = new Map();
- weeks.forEach((sections, week) => {
- const sorted = [...sections].sort((a, b) => a - b);
- for (let i = 0; i < sorted.length; i++) {
- const start = sorted[i];
- while (i + 1 < sorted.length && sorted[i + 1] === sorted[i] + 1) i++;
- const key = `${start}-${sorted[i]}`;
- if (!blocks.has(key)) blocks.set(key, []);
- blocks.get(key).push(week);
- }
- });
- for (const [key, courseWeeks] of blocks) {
- const [startSection, endSection] = key.split('-').map(Number);
- courses.push({
- name: lesson.courseSequence ? `${lesson.name} | ${lesson.courseSequence}` : lesson.name,
- teacher: lesson.teacher, position: lesson.position, day: lesson.day,
- startSection, endSection, weeks: courseWeeks,
- credit: lesson.credit,
- ...(/教3/.test(lesson.position) && startSection === 3 && endSection === 4 ? {
- isCustomTime: true, customStartTime: "10:10", customEndTime: "11:40"
- } : {})
- });
- }
- }
- return courses.sort((a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name));
- }
- /** 从同一课表响应的课程列表中读取学分,按课程序号匹配,避免同名课程混淆。 */
- function parseCourseCredits(html) {
- const doc = new DOMParser().parseFromString(html, "text/html");
- const credits = new Map();
- for (const table of doc.querySelectorAll("table")) {
- const header = Array.from(table.rows).find(row =>
- Array.from(row.cells).some(cell => cell.tagName === "TH" && cell.textContent.trim() === "学分"));
- if (!header) continue;
- const headers = Array.from(header.cells).map(cell => cell.textContent.trim());
- const creditIndex = headers.indexOf("学分");
- const sequenceIndex = headers.indexOf("课程序号");
- if (sequenceIndex < 0) continue;
- for (const row of table.rows) {
- const sequence = row.cells[sequenceIndex]?.textContent.trim();
- const text = row.cells[creditIndex]?.textContent.trim();
- if (!sequence || !/^\d+(?:\.\d+)?$/.test(text)) continue;
- credits.set(sequence, Number(text));
- }
- }
- return credits;
- }
- function parseTaskActivities(html) {
- const credits = parseCourseCredits(html);
- const lessons = [];
- for (const block of html.split(/var\s+teachers\s*=/).slice(1)) {
- const activities = [...block.matchAll(/new\s+TaskActivity\(([\s\S]*?)\);/g)];
- let teacher = "未知教师";
- for (let i = 0; i < activities.length; i++) {
- const activity = activities[i];
- const before = block.slice(i ? activities[i - 1].index + activities[i - 1][0].length : 0, activity.index);
- const teachers = before.match(/actTeachers\s*=\s*(\[[\s\S]*?\])/);
- if (teachers) {
- teacher = [...teachers[1].matchAll(/name\s*:\s*["'](.*?)["']/g)].map(m => m[1]).join("、") || "未知教师";
- }
- const args = powerSplit(activity[1]);
- const courseSequence = (args[2] || "").match(/\(([^()]*)\)$/)?.[1] || "";
- const credit = credits.get(courseSequence);
- const position = (args[5] || "未知地点").replace(/\(.*?\)/g, "");
- const weeks = [...(args[6] || "")].flatMap((bit, week) => bit === "1" ? [week] : []);
- const indexes = block.slice(activity.index + activity[0].length, activities[i + 1]?.index)
- .matchAll(/index\s*=\s*(\d+)\s*\*\s*unitCount\s*\+\s*(\d+);/g);
- for (const index of indexes) {
- lessons.push({
- name: (args[3] || "未知课程").split('(')[0], courseSequence,
- teacher, position, day: Number(index[1]) + 1,
- startSection: Number(index[2]) + 1, endSection: Number(index[2]) + 1,
- weeks, credit
- });
- }
- }
- }
- return mergeContinuousLessons(lessons);
- }
- async function request(path, body) {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 15000);
- try {
- const res = await fetch(`http://jwgl2018.xatu.edu.cn/eams/${path}`, {
- credentials: "include", signal: controller.signal,
- ...(body !== undefined ? {
- method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body
- } : {})
- });
- if (!res.ok) throw new Error(`教务请求失败:HTTP ${res.status}`);
- const html = await res.text();
- return { html, serverTime: Date.parse(res.headers.get("Date") || "") };
- } catch (e) {
- if (controller.signal.aborted) throw new Error("教务请求超时,请稍后重试");
- throw e;
- } finally {
- clearTimeout(timer);
- }
- }
- /** 根据教务首页的当前教学周,推算第一教学周的周一(北京时间)。 */
- function inferCurrentSemester(html, referenceTime) {
- const doc = new DOMParser().parseFromString(html, "text/html");
- const label = Array.from(doc.querySelectorAll("strong"))
- .find(el => el.textContent.trim() === "当前教学周");
- const weekText = label?.parentElement?.querySelector("span")?.textContent.trim();
- const totalMatch = label?.parentElement?.querySelector("i")?.textContent.match(/\/\s*(\d+)/);
- if (!weekText || !/^\d+$/.test(weekText) || !totalMatch) return null;
- const week = Number(weekText);
- const totalWeeks = Number(totalMatch[1]);
- if (week < 1 || week > totalWeeks || totalWeeks > 53) return null;
- // 固定按教务的 UTC+8 日期计算第一周周一。
- const start = new Date(referenceTime + 8 * 3600000);
- start.setUTCHours(0, 0, 0, 0);
- start.setUTCDate(start.getUTCDate() - (start.getUTCDay() + 6) % 7 - (week - 1) * 7);
- const isAutumn = start.getUTCMonth() >= 6;
- const year = start.getUTCFullYear() - (isAutumn ? 0 : 1);
- return {
- schoolYear: `${year}-${year + 1}`, term: isAutumn ? "1" : "2",
- semesterStartDate: start.toISOString().slice(0, 10),
- semesterTotalWeeks: totalWeeks
- };
- }
- async function getCurrentSemesterConfig() {
- try {
- const { html, serverTime } = await request("homeExt!main.action?sf_request_type=ajax");
- const current = inferCurrentSemester(html, Number.isFinite(serverTime) ? serverTime : Date.now());
- if (!current) throw new Error("未能识别当前教学周");
- return current;
- } catch (e) {
- console.warn(`[开学日期推算] ${e.message}`);
- window.shiguangBridge.showToast(`${e.message};开学日期请手动设置`);
- return null;
- }
- }
- async function detectParameters() {
- const { html } = await request("courseTableForStd.action?sf_request_type=ajax");
- const idsMatch = html.match(/bg\.form\.addInput\(form,"ids","(\d+)"\)/);
- const tagIdMatch = html.match(/id="(semesterBar\d+Semester)"/);
- if (!idsMatch || !tagIdMatch) return null;
- return { ids: idsMatch[1], tagId: tagIdMatch[1] };
- }
- async function getSelectedSemester({ tagId }, current) {
- const { html } = await request("dataQuery.action?sf_request_type=ajax",
- `tagId=${encodeURIComponent(tagId)}&dataType=semesterCalendar`);
- const data = Function(`return (${html});`)();
- const list = Object.values(data.semesters).flat().map(s => ({
- id: s.id, name: `${s.schoolYear}学年 ${String(s.name) === "1" ? "秋季" : "春季"}学期`,
- isCurrent: s.schoolYear === current?.schoolYear && String(s.name) === current?.term
- }));
- const currentIndex = list.findIndex(s => s.isCurrent);
- const idx = await window.shiguangBridgePromise.showSingleSelection("选择学期",
- JSON.stringify(list.map(s => s.name + (s.isCurrent ? "(当前学期)" : ""))),
- currentIndex);
- return idx === null ? null : list[idx];
- }
- async function fetchAndParseCourses(semesterId, ids) {
- const { html } = await request("courseTableForStd!courseTable.action?sf_request_type=ajax",
- `ignoreHead=1&setting.kind=std&semester.id=${encodeURIComponent(semesterId)}&ids=${encodeURIComponent(ids)}`);
- return parseTaskActivities(html);
- }
- async function applyTimeSlots() {
- const slots = [
- ["08:20", "09:05"], ["09:15", "10:00"],
- ["10:20", "11:05"], ["11:15", "12:00"],
- ["14:00", "14:45"], ["14:55", "15:40"],
- ["16:00", "16:45"], ["16:55", "17:40"],
- ["18:10", "18:55"], ["19:05", "19:50"],
- ["20:00", "20:45"], ["20:55", "21:40"]
- ].map(([startTime, endTime], i) => ({ number: i + 1, startTime, endTime }));
- return await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(slots));
- }
- async function runImportFlow() {
- try {
- window.shiguangBridge.showToast("开始探测教务参数...");
- const params = await detectParameters();
- if (!params) throw new Error("未能识别教务参数,请确认已登录");
- const current = await getCurrentSemesterConfig();
- const semester = await getSelectedSemester(params, current);
- if (!semester) return;
- window.shiguangBridge.showToast("正在同步课表...");
- const courses = await fetchAndParseCourses(semester.id, params.ids);
-
- if (!courses.length) throw new Error("未解析到课程数据");
- if (current && !semester.isCurrent) window.shiguangBridge.showToast("历史或其他学期开学日期请手动设置");
- if (!await applyTimeSlots()) {
- console.error("作息时间保存失败,导入未完成");
- return;
- }
- if (semester.isCurrent && !await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
- semesterStartDate: current.semesterStartDate,
- semesterTotalWeeks: current.semesterTotalWeeks
- }))) {
- throw new Error("学期配置保存失败,导入未完成");
- }
- if (!await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses))) {
- throw new Error("课程保存失败,导入未完成");
- }
- window.shiguangBridge.showToast(`成功导入 ${courses.length} 个课程条目`);
- window.shiguangBridge.notifyTaskCompletion();
- } catch (e) {
- console.error(`[异常] ${e.message}`);
- window.shiguangBridge.showToast(e.message);
- }
- }
- runImportFlow();
|