|
@@ -1,9 +1,19 @@
|
|
|
// 吉利学院(GUC) 拾光课程表适配脚本
|
|
// 吉利学院(GUC) 拾光课程表适配脚本
|
|
|
// YETHAN以专教学信息服务平台
|
|
// YETHAN以专教学信息服务平台
|
|
|
// 该教务系统带图片验证码,需用户在页面内手动登录,脚本基于已登录会话(Cookie)拉取课表
|
|
// 该教务系统带图片验证码,需用户在页面内手动登录,脚本基于已登录会话(Cookie)拉取课表
|
|
|
-// 非该大学学生适配可能无法登录,出现问题请通过issues提交反馈
|
|
|
|
|
// 桥接 API 使用 v2:window.shiguangBridge(同步) / window.shiguangBridgePromise(异步)
|
|
// 桥接 API 使用 v2:window.shiguangBridge(同步) / window.shiguangBridgePromise(异步)
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+//*****吐槽(>-<)*****
|
|
|
|
|
+// ===== YETHAN 系统特性(适配脚本为何做了这么多特判) =====
|
|
|
|
|
+// 1. 页面标题错乱:课表页 <title>="学生个人信息"、重修页="选课日志"(模板没改标题)
|
|
|
|
|
+// 2. 无校历/学期开始日期字段——开学日期只能从"按周查课表"表头偷(且表头只有"03月02日"无年份,年份需页头学年/最近年份推算)
|
|
|
|
|
+// 3. 课程数据脏:编号/课名/括号挤一行(如 "B1001 高等数学(4)"),全角括号混用、教师可能缺失、地点可为 NoRoom
|
|
|
|
|
+// 4. 课表视图分裂:全部周次视图表头无日期,仅按周(weekNo=N)视图表头带日期
|
|
|
|
|
+// 5. 学生年级/学历等字段经接口加密下发、无干净字段(故无法按学号可靠推算"大几",只显示真实学年学期)
|
|
|
|
|
+// 6. 功能集中在单 Action(CourseAction?setAction=...)按字符串分发,无 REST
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
// ==================== 常量 ====================
|
|
// ==================== 常量 ====================
|
|
|
|
|
|
|
|
// 课表页面地址(selectTableType: ThisTerm=本学期, NextTerm=下学期)
|
|
// 课表页面地址(selectTableType: ThisTerm=本学期, NextTerm=下学期)
|
|
@@ -12,7 +22,7 @@ const COURSE_TABLE_URL = "https://jw.guc.edu.cn/yethan/CourseAction?setAction=us
|
|
|
// 课表表单提交地址("按周次查课表"用 POST 提交,weekNo=1 请求用于解析开学日期,复刻页面表单)
|
|
// 课表表单提交地址("按周次查课表"用 POST 提交,weekNo=1 请求用于解析开学日期,复刻页面表单)
|
|
|
const COURSE_ACTION_URL = "https://jw.guc.edu.cn/yethan/CourseAction";
|
|
const COURSE_ACTION_URL = "https://jw.guc.edu.cn/yethan/CourseAction";
|
|
|
|
|
|
|
|
-// 默认导入本学期(学校系统仅支持查看本学期课表);如需下学期请改为 "NextTerm"
|
|
|
|
|
|
|
+// 默认导入学期(用户在选择学期弹窗中取消时回退到本学期)
|
|
|
const SELECT_TABLE_TYPE = "ThisTerm";
|
|
const SELECT_TABLE_TYPE = "ThisTerm";
|
|
|
|
|
|
|
|
// 拉取课表请求超时时间(毫秒)
|
|
// 拉取课表请求超时时间(毫秒)
|
|
@@ -61,7 +71,7 @@ function parseWeeks(weekStr) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 解析一行课程文本,如 "B4708 大学英语(4)(李博)"。
|
|
|
|
|
|
|
+ * 解析一行课程文本,如 "B3001 体育与健康(4)(张睿)"(课名带版本号括号,教师为最后一个全角括号)。
|
|
|
* 返回 { name, teacher },无法解析时返回 null。
|
|
* 返回 { name, teacher },无法解析时返回 null。
|
|
|
* 兼容情况:
|
|
* 兼容情况:
|
|
|
* - 未选课前课程可能无教师(如 "B1001 高等数学"),此时 teacher 为空串,课程保留;
|
|
* - 未选课前课程可能无教师(如 "B1001 高等数学"),此时 teacher 为空串,课程保留;
|
|
@@ -187,15 +197,23 @@ function mergeAndDistinctCourses(courses) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 解析课表 HTML 表格为课程模型数组。
|
|
|
|
|
|
|
+ * 解析课表 HTML,返回表格的所有行(<tr> 数组)。找不到表格返回空数组。
|
|
|
|
|
+ * 供解析课程/作息/开学日期复用,统一课表表格入口。
|
|
|
*/
|
|
*/
|
|
|
-function parseTimetableToCourses(html) {
|
|
|
|
|
|
|
+function getTableRows(html) {
|
|
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
|
const table = doc.querySelector("table.table_border");
|
|
const table = doc.querySelector("table.table_border");
|
|
|
- if (!table) return [];
|
|
|
|
|
|
|
+ return table ? Array.from(table.querySelectorAll("tr")) : [];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 解析课表 HTML 表格为课程模型数组。
|
|
|
|
|
+ */
|
|
|
|
|
+function parseTimetableToCourses(html) {
|
|
|
|
|
+ const rows = getTableRows(html);
|
|
|
|
|
+ if (rows.length === 0) return [];
|
|
|
|
|
|
|
|
const results = [];
|
|
const results = [];
|
|
|
- const rows = Array.from(table.querySelectorAll("tr"));
|
|
|
|
|
|
|
|
|
|
for (const row of rows) {
|
|
for (const row of rows) {
|
|
|
const cells = Array.from(row.querySelectorAll("td"));
|
|
const cells = Array.from(row.querySelectorAll("td"));
|
|
@@ -254,11 +272,9 @@ function parseTimetableToCourses(html) {
|
|
|
*/
|
|
*/
|
|
|
function parseTimeSlots(html) {
|
|
function parseTimeSlots(html) {
|
|
|
try {
|
|
try {
|
|
|
- const doc = new DOMParser().parseFromString(html, "text/html");
|
|
|
|
|
- const table = doc.querySelector("table.table_border");
|
|
|
|
|
- if (!table) return null;
|
|
|
|
|
|
|
+ const rows = getTableRows(html);
|
|
|
|
|
+ if (rows.length === 0) return null;
|
|
|
const slots = [];
|
|
const slots = [];
|
|
|
- const rows = Array.from(table.querySelectorAll("tr"));
|
|
|
|
|
for (const row of rows) {
|
|
for (const row of rows) {
|
|
|
const cells = Array.from(row.querySelectorAll("td"));
|
|
const cells = Array.from(row.querySelectorAll("td"));
|
|
|
if (cells.length < 2) continue;
|
|
if (cells.length < 2) continue;
|
|
@@ -303,9 +319,9 @@ async function fetchWithTimeout(url, options) {
|
|
|
* - { error: "request_failed" } HTTP 请求失败(非 2xx)
|
|
* - { error: "request_failed" } HTTP 请求失败(非 2xx)
|
|
|
* - { error: "no_table" } 已登录但页面无课表
|
|
* - { error: "no_table" } 已登录但页面无课表
|
|
|
*/
|
|
*/
|
|
|
-async function fetchTimetableOnce() {
|
|
|
|
|
|
|
+async function fetchTimetableOnce(semesterType) {
|
|
|
try {
|
|
try {
|
|
|
- const resp = await fetchWithTimeout(COURSE_TABLE_URL + SELECT_TABLE_TYPE + "&queryType=student");
|
|
|
|
|
|
|
+ const resp = await fetchWithTimeout(COURSE_TABLE_URL + semesterType + "&queryType=student");
|
|
|
if (!resp.ok) return { error: "request_failed" };
|
|
if (!resp.ok) return { error: "request_failed" };
|
|
|
const html = await resp.text();
|
|
const html = await resp.text();
|
|
|
if (html.includes('id="ranstring"') || html.includes("LoginForm")) return { error: "not_logged_in" };
|
|
if (html.includes('id="ranstring"') || html.includes("LoginForm")) return { error: "not_logged_in" };
|
|
@@ -322,7 +338,7 @@ async function fetchTimetableOnce() {
|
|
|
* 第 1 周视图的表头带日期(如 "星期一<br>03月02日"),用于解析开学日期。
|
|
* 第 1 周视图的表头带日期(如 "星期一<br>03月02日"),用于解析开学日期。
|
|
|
* 成功返回响应 HTML;任何失败返回 null(不阻断主流程,开学日期缺失时 App 端可手动设置)。
|
|
* 成功返回响应 HTML;任何失败返回 null(不阻断主流程,开学日期缺失时 App 端可手动设置)。
|
|
|
*/
|
|
*/
|
|
|
-async function fetchWeek1Timetable() {
|
|
|
|
|
|
|
+async function fetchWeek1Timetable(semesterType) {
|
|
|
try {
|
|
try {
|
|
|
const resp = await fetchWithTimeout(COURSE_ACTION_URL, {
|
|
const resp = await fetchWithTimeout(COURSE_ACTION_URL, {
|
|
|
method: "POST",
|
|
method: "POST",
|
|
@@ -330,7 +346,7 @@ async function fetchWeek1Timetable() {
|
|
|
body: new URLSearchParams({
|
|
body: new URLSearchParams({
|
|
|
setAction: "userCourseScheduleTable",
|
|
setAction: "userCourseScheduleTable",
|
|
|
viewType: "studentCourseTableWeek",
|
|
viewType: "studentCourseTableWeek",
|
|
|
- selectTableType: SELECT_TABLE_TYPE,
|
|
|
|
|
|
|
+ selectTableType: semesterType,
|
|
|
queryType: "student",
|
|
queryType: "student",
|
|
|
weekNo: "1"
|
|
weekNo: "1"
|
|
|
}).toString()
|
|
}).toString()
|
|
@@ -354,10 +370,9 @@ async function fetchWeek1Timetable() {
|
|
|
*/
|
|
*/
|
|
|
function parseSemesterStartDate(html) {
|
|
function parseSemesterStartDate(html) {
|
|
|
try {
|
|
try {
|
|
|
- const doc = new DOMParser().parseFromString(html, "text/html");
|
|
|
|
|
- const table = doc.querySelector("table.table_border");
|
|
|
|
|
- if (!table) return null;
|
|
|
|
|
- const headerRow = table.querySelector("tr");
|
|
|
|
|
|
|
+ const rows = getTableRows(html);
|
|
|
|
|
+ if (rows.length === 0) return null;
|
|
|
|
|
+ const headerRow = rows[0];
|
|
|
const cells = headerRow ? Array.from(headerRow.querySelectorAll("td")) : [];
|
|
const cells = headerRow ? Array.from(headerRow.querySelectorAll("td")) : [];
|
|
|
// 表头:[0]=节, [1]=上课时间, [2]=星期一 ...
|
|
// 表头:[0]=节, [1]=上课时间, [2]=星期一 ...
|
|
|
if (cells.length < 3) return null;
|
|
if (cells.length < 3) return null;
|
|
@@ -402,6 +417,69 @@ function parseSemesterStartDate(html) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ==================== 学期选择 ====================
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 从选课页 HTML 解析真实学期名(如 "2025-2026第2学期")。
|
|
|
|
|
+ * 页头 userInfo 形如 "学号 姓名 <span>2025-2026第2学期</span> 课表"。
|
|
|
|
|
+ * 解析失败返回 null(由调用方回退使用通用名)。
|
|
|
|
|
+ */
|
|
|
|
|
+function parseSemesterLabel(html) {
|
|
|
|
|
+ const m = html.match(/(\d{4}-\d{4}第\d学期)/);
|
|
|
|
|
+ return m ? m[1] : null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 探测指定学期的真实学期名。请求选课页(userCourseSchedule)拿页头学期标识;
|
|
|
|
|
+ * 任何失败返回 null(调用方回退用通用名"本学期/下学期")。
|
|
|
|
|
+ */
|
|
|
|
|
+async function fetchSemesterLabel(type) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const resp = await fetchWithTimeout(COURSE_ACTION_URL + "?setAction=userCourseSchedule&selectTableType=" + type);
|
|
|
|
|
+ if (!resp.ok) return null;
|
|
|
|
|
+ return parseSemesterLabel(await resp.text());
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error("探测学期名失败(", type, "):", e);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 让用户选择导入学期。用 showSingleSelection 显示两个学期的真实学期名
|
|
|
|
|
+ * (如 "2025-2026第2学期" / "2026-2027第1学期",比"本学期/下学期"更明确);
|
|
|
|
|
+ * 探测不到真实名时回退显示通用名。取消返回默认本学期(ThisTerm)。
|
|
|
|
|
+ */
|
|
|
|
|
+async function selectSemester() {
|
|
|
|
|
+ const kinds = [
|
|
|
|
|
+ { value: "ThisTerm", label: "本学期" },
|
|
|
|
|
+ { value: "NextTerm", label: "下学期" }
|
|
|
|
|
+ ];
|
|
|
|
|
+ const labels = await Promise.all(kinds.map(k => fetchSemesterLabel(k.value)));
|
|
|
|
|
+ const names = kinds.map((k, i) => labels[i] || k.label);
|
|
|
|
|
+ const idx = await window.shiguangBridgePromise.showSingleSelection("选择导入学期", JSON.stringify(names), 0);
|
|
|
|
|
+ if (idx == null || idx === undefined || idx === -1) return SELECT_TABLE_TYPE;
|
|
|
|
|
+ return (kinds[idx] || kinds[0]).value;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 构造课表配置:学期总周数(取课程最大周)+ 开学日期(可选)。
|
|
|
|
|
+ * 开学日期:请求第 1 周课表,取表头"星期一"日期作为第一周第一天("yyyy-MM-dd",官方格式);
|
|
|
|
|
+ * 请求失败或解析失败时不设置该字段(App 端可手动设置),不影响课程导入。
|
|
|
|
|
+ */
|
|
|
|
|
+async function fetchCourseConfig(semesterType, courses) {
|
|
|
|
|
+ const maxWeek = courses.reduce((max, c) => Math.max(max, Math.max(...c.weeks)), 0);
|
|
|
|
|
+ const config = { semesterTotalWeeks: Math.max(maxWeek, 1) };
|
|
|
|
|
+ const week1Html = await fetchWeek1Timetable(semesterType);
|
|
|
|
|
+ if (week1Html) {
|
|
|
|
|
+ const semesterStartDate = parseSemesterStartDate(week1Html);
|
|
|
|
|
+ if (semesterStartDate) {
|
|
|
|
|
+ config.semesterStartDate = semesterStartDate;
|
|
|
|
|
+ console.log("开学日期:", semesterStartDate);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return config;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ==================== 主流程 ====================
|
|
// ==================== 主流程 ====================
|
|
|
|
|
|
|
|
async function runImportFlow() {
|
|
async function runImportFlow() {
|
|
@@ -425,9 +503,10 @@ async function runImportFlow() {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 2. 拉取本学期课表并检测登录状态
|
|
|
|
|
|
|
+ // 2. 选择导入学期(默认本学期),并拉取该学期课表、检测登录状态
|
|
|
|
|
+ const semesterType = await selectSemester();
|
|
|
BRIDGE.showToast("正在获取课表数据...");
|
|
BRIDGE.showToast("正在获取课表数据...");
|
|
|
- const result = await fetchTimetableOnce();
|
|
|
|
|
|
|
+ const result = await fetchTimetableOnce(semesterType);
|
|
|
if (result.error === "not_logged_in") {
|
|
if (result.error === "not_logged_in") {
|
|
|
await BRIDGE_P.showAlert(
|
|
await BRIDGE_P.showAlert(
|
|
|
"未检测到登录状态",
|
|
"未检测到登录状态",
|
|
@@ -447,10 +526,10 @@ async function runImportFlow() {
|
|
|
}
|
|
}
|
|
|
const html = result.html;
|
|
const html = result.html;
|
|
|
|
|
|
|
|
- // 3. 解析课程(默认本学期)
|
|
|
|
|
|
|
+ // 3. 解析课程(所选学期)
|
|
|
const courses = parseTimetableToCourses(html);
|
|
const courses = parseTimetableToCourses(html);
|
|
|
if (courses.length === 0) {
|
|
if (courses.length === 0) {
|
|
|
- BRIDGE.showToast("未解析到课程数据,请确认本学期是否有课程");
|
|
|
|
|
|
|
+ BRIDGE.showToast("未解析到课程数据,请确认所选学期是否有课程");
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
console.log("解析到课程记录数:", courses.length, courses);
|
|
console.log("解析到课程记录数:", courses.length, courses);
|
|
@@ -475,18 +554,7 @@ async function runImportFlow() {
|
|
|
|
|
|
|
|
// 6. 保存课表配置(学期总周数 + 开学日期)
|
|
// 6. 保存课表配置(学期总周数 + 开学日期)
|
|
|
try {
|
|
try {
|
|
|
- const maxWeek = courses.reduce((max, c) => Math.max(max, Math.max(...c.weeks)), 0);
|
|
|
|
|
- const config = { semesterTotalWeeks: Math.max(maxWeek, 1) };
|
|
|
|
|
- // 开学日期:请求第 1 周课表,取表头"星期一"日期作为第一周第一天("yyyy-MM-dd",官方格式);
|
|
|
|
|
- // 请求失败或解析失败时不设置该字段(App 端可手动设置),不影响课程导入
|
|
|
|
|
- const week1Html = await fetchWeek1Timetable();
|
|
|
|
|
- if (week1Html) {
|
|
|
|
|
- const semesterStartDate = parseSemesterStartDate(week1Html);
|
|
|
|
|
- if (semesterStartDate) {
|
|
|
|
|
- config.semesterStartDate = semesterStartDate;
|
|
|
|
|
- console.log("开学日期:", semesterStartDate);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const config = await fetchCourseConfig(semesterType, courses);
|
|
|
await BRIDGE_P.saveCourseConfig(JSON.stringify(config));
|
|
await BRIDGE_P.saveCourseConfig(JSON.stringify(config));
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
BRIDGE.showToast("课表配置保存失败: " + e.message);
|
|
BRIDGE.showToast("课表配置保存失败: " + e.message);
|