hhvt.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. // 怀化职业技术学院教务系统(jwgl.hhvtc.com.cn)拾光课表导入适配脚本
  2. // 青果教务:课表表格单元格 .have-content 只有空内容,课程详情在隐藏 div [id^="weekly0"] 内
  3. // 每个详情 div 含 ul>li:课程名称 / 任课教师 / 上课时间([周次]星期[节次])/ 上课地点 / 合班信息
  4. // 作息分夏秋(5.1起)与冬春(10.1起)两套,导入时通过选择器让用户选择
  5. // 夏秋作息(5.1 起)
  6. const HHVT_TIME_SLOTS_SUMMER = [
  7. { number: 1, startTime: "08:10", endTime: "08:55" },
  8. { number: 2, startTime: "09:05", endTime: "09:50" },
  9. { number: 3, startTime: "10:10", endTime: "10:55" },
  10. { number: 4, startTime: "11:05", endTime: "11:50" },
  11. { number: 5, startTime: "14:30", endTime: "15:15" },
  12. { number: 6, startTime: "15:25", endTime: "16:10" },
  13. { number: 7, startTime: "16:20", endTime: "17:05" },
  14. { number: 8, startTime: "17:15", endTime: "18:00" }
  15. ];
  16. // 冬春作息(10.1 起)
  17. const HHVT_TIME_SLOTS_WINTER = [
  18. { number: 1, startTime: "08:20", endTime: "09:05" },
  19. { number: 2, startTime: "09:15", endTime: "10:00" },
  20. { number: 3, startTime: "10:20", endTime: "11:05" },
  21. { number: 4, startTime: "11:15", endTime: "12:00" },
  22. { number: 5, startTime: "14:00", endTime: "14:45" },
  23. { number: 6, startTime: "14:55", endTime: "15:40" },
  24. { number: 7, startTime: "15:50", endTime: "16:35" },
  25. { number: 8, startTime: "16:45", endTime: "17:30" }
  26. ];
  27. function getErrorMessage(error) {
  28. if (error && typeof error.message === "string" && error.message.trim()) return error.message;
  29. if (typeof error === "string" && error.trim()) return error;
  30. try {
  31. const serialized = JSON.stringify(error);
  32. if (serialized && serialized !== "{}") return serialized;
  33. } catch (_) {
  34. // Ignore serialization failures.
  35. }
  36. return "未知错误";
  37. }
  38. // 收集可访问的 document(frameset 用 <frame>,普通页面用 <iframe>)
  39. function collectDocs() {
  40. const docs = [document];
  41. const frames = document.querySelectorAll("iframe, frame");
  42. for (const f of frames) {
  43. try {
  44. if (f.contentDocument) docs.push(f.contentDocument);
  45. } catch (_) {
  46. // 跨域 frame 无法访问,忽略。
  47. }
  48. }
  49. return docs;
  50. }
  51. // 从各 frame 中查找课表详情 div([id^="weekly0"])
  52. // 注入时机早于页面加载完成,故轮询等待(最长 6 秒)
  53. async function findDetailDivs() {
  54. const deadline = Date.now() + 6000;
  55. while (Date.now() < deadline) {
  56. for (const doc of collectDocs()) {
  57. const divs = doc.querySelectorAll('[id^="weekly0"]');
  58. if (divs.length > 0) {
  59. return { doc, divs };
  60. }
  61. }
  62. await new Promise(resolve => setTimeout(resolve, 300));
  63. }
  64. return null;
  65. }
  66. // 解析周次文本:"3周"→[3];"1-18周"→[1..18];"1-8,10-18周"→[1..8,10..18]
  67. function parseWeeksText(weekStr) {
  68. const text = String(weekStr || "").replace(/\s+/g, "");
  69. const m = text.match(/\[(.*?)\]/);
  70. if (!m) return [];
  71. const body = m[1].replace(/周/g, "");
  72. const weeks = [];
  73. const parts = body.split(",");
  74. for (const part of parts) {
  75. const range = part.split("-");
  76. const start = Number(range[0]);
  77. const end = range.length > 1 ? Number(range[1]) : start;
  78. if (!start) continue;
  79. for (let w = start; w <= end; w++) weeks.push(w);
  80. }
  81. return [...new Set(weeks)].sort((a, b) => a - b);
  82. }
  83. // 星期汉字 → 拾光 day(一=1 … 日=7)
  84. const DAY_MAP = { 一: 1, 二: 2, 三: 3, 四: 4, 五: 5, 六: 6, 日: 7 };
  85. // 解析单个详情 div(ul>li 结构)
  86. function parseDetailDiv(div) {
  87. const lis = div.querySelectorAll("li");
  88. if (lis.length === 0) return null;
  89. let name = "";
  90. let teacher = "";
  91. let position = "";
  92. let timeText = "";
  93. for (const li of lis) {
  94. const text = (li.textContent || "").trim();
  95. if (text.indexOf("课程名称") !== -1) {
  96. name = text.replace(/^.*?[::]/, "").trim();
  97. } else if (text.indexOf("任课教师") !== -1) {
  98. teacher = text.replace(/^.*?[::]/, "").trim();
  99. } else if (text.indexOf("上课时间") !== -1) {
  100. timeText = text.replace(/^.*?[::]/, "").trim();
  101. } else if (text.indexOf("上课地点") !== -1) {
  102. position = text.replace(/^.*?[::]/, "").trim();
  103. }
  104. }
  105. if (!name || !timeText) return null;
  106. // 上课时间:[3周]一[1-2节]
  107. const dayMatch = timeText.match(/[一二三四五六日]/);
  108. if (!dayMatch) return null;
  109. const day = DAY_MAP[dayMatch[0]];
  110. const sectionMatch = timeText.match(/\[(\d+)-(\d+)节\]/);
  111. if (!sectionMatch) return null;
  112. const weeks = parseWeeksText(timeText);
  113. if (weeks.length === 0) return null;
  114. return {
  115. name,
  116. teacher: teacher || "未知",
  117. position: position || "待定",
  118. day,
  119. startSection: Number(sectionMatch[1]),
  120. endSection: Number(sectionMatch[2]),
  121. weeks
  122. };
  123. }
  124. // 解析所有详情 div
  125. function parseCourses(divs) {
  126. const courses = [];
  127. for (const div of divs) {
  128. const course = parseDetailDiv(div);
  129. if (course) courses.push(course);
  130. }
  131. return courses;
  132. }
  133. // 合并同课程同时间同教室的条目
  134. function mergeCourses(courses) {
  135. const merged = new Map();
  136. for (const c of courses) {
  137. const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.startSection}|${c.endSection}`;
  138. const holder = merged.get(key);
  139. if (holder) {
  140. holder.weeks = Array.from(new Set([...holder.weeks, ...c.weeks])).sort((a, b) => a - b);
  141. } else {
  142. merged.set(key, { ...c, weeks: [...c.weeks].sort((a, b) => a - b) });
  143. }
  144. }
  145. return Array.from(merged.values()).sort(
  146. (a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name)
  147. );
  148. }
  149. // 保存作息时间
  150. async function saveTimeSlots(timeSlots) {
  151. try {
  152. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  153. } catch (error) {
  154. console.error("JS: 作息时间保存失败", error);
  155. }
  156. }
  157. async function runImportFlow() {
  158. const alertConfirmed = await window.shiguangBridgePromise.showAlert(
  159. "课表导入",
  160. "导入前请确保已登录并打开课表查询页面。",
  161. "好的,开始导入"
  162. );
  163. if (!alertConfirmed) {
  164. window.shiguangBridge.showToast("用户取消了导入。");
  165. return;
  166. }
  167. // 作息选择:按当前日期给默认(5.1-9.30 夏秋,10.1-4.30 冬春)
  168. const now = new Date();
  169. const defaultIdx = (now.getMonth() + 1 >= 5 && now.getMonth() + 1 <= 9) ? 0 : 1;
  170. const picked = await window.shiguangBridgePromise.showSingleSelection(
  171. "选择作息时间",
  172. JSON.stringify(["夏秋作息(5月1日起)", "冬春作息(10月1日起)"]),
  173. defaultIdx
  174. );
  175. if (picked === null || picked < 0) {
  176. window.shiguangBridge.showToast("用户取消了导入。");
  177. return;
  178. }
  179. const timeSlots = picked === 0 ? HHVT_TIME_SLOTS_SUMMER : HHVT_TIME_SLOTS_WINTER;
  180. window.shiguangBridge.showToast("正在获取课表数据...");
  181. try {
  182. const found = await findDetailDivs();
  183. if (!found) throw new Error("未找到课表详情,请确认已登录并打开课表查询页面。");
  184. const courses = parseCourses(found.divs);
  185. if (courses.length === 0) throw new Error("课表中未解析到有效课程,请确认当前学期有课。");
  186. const merged = mergeCourses(courses);
  187. window.shiguangBridge.showToast(`正在保存 ${merged.length} 门课程...`);
  188. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(merged, null, 2));
  189. await saveTimeSlots(timeSlots);
  190. window.shiguangBridge.showToast(`课程导入成功,共导入 ${merged.length} 门课程!`);
  191. window.shiguangBridge.notifyTaskCompletion();
  192. } catch (error) {
  193. window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
  194. console.error("JS: Import Error", error);
  195. }
  196. }
  197. runImportFlow();