wbu_01.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. // 适配目标:武汉商学院-超星综合教学管理系统
  2. (function () {
  3. function toast(message) {
  4. if (window.AndroidBridge && typeof window.AndroidBridge.showToast === "function") {
  5. window.AndroidBridge.showToast(message);
  6. }
  7. }
  8. function sleep(ms) {
  9. return new Promise((resolve) => setTimeout(resolve, ms));
  10. }
  11. async function getTargetDocument() {
  12. if (location.href.includes("queryKbForXsd")) {
  13. return document;
  14. }
  15. const iframe = document.querySelector("iframe[src*='queryKbForXsd']");
  16. if (!iframe) return null;
  17. for (let i = 0; i < 20; i += 1) {
  18. try {
  19. const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
  20. if (doc && doc.readyState && doc.readyState !== "loading") {
  21. return doc;
  22. }
  23. } catch (e) {
  24. // ignore
  25. }
  26. await sleep(500);
  27. }
  28. return null;
  29. }
  30. async function waitForScheduleData(doc, timeoutMs = 15000) {
  31. const start = Date.now();
  32. while (Date.now() - start < timeoutMs) {
  33. const cells = Array.from(doc.querySelectorAll("td.cell, td[id^='Cell']"));
  34. const filled = cells.filter((cell) => {
  35. const text = (cell.innerText || cell.textContent || "").trim();
  36. return text.length > 0 && /周/.test(text);
  37. });
  38. if (filled.length > 0) {
  39. return true;
  40. }
  41. await sleep(500);
  42. }
  43. return false;
  44. }
  45. function uniqueSortedNumbers(nums) {
  46. const set = new Set(nums.filter((n) => Number.isFinite(n)));
  47. return Array.from(set).sort((a, b) => a - b);
  48. }
  49. function parseWeekText(weekText) {
  50. if (!weekText) return [];
  51. let text = String(weekText).trim();
  52. if (!text) return [];
  53. let oddOnly = false;
  54. let evenOnly = false;
  55. if (text.includes("单")) oddOnly = true;
  56. if (text.includes("双")) evenOnly = true;
  57. text = text.replace(/周/g, "");
  58. text = text.replace(/\s+/g, "");
  59. text = text.replace(/\(.*?\)/g, "");
  60. text = text.replace(/(.*?)/g, "");
  61. const weeks = [];
  62. const segments = text.split(",").map((s) => s.trim()).filter(Boolean);
  63. segments.forEach((seg) => {
  64. if (!seg) return;
  65. const rangeMatch = seg.match(/^(\d+)-(\d+)$/);
  66. if (rangeMatch) {
  67. const start = parseInt(rangeMatch[1], 10);
  68. const end = parseInt(rangeMatch[2], 10);
  69. if (!Number.isFinite(start) || !Number.isFinite(end)) return;
  70. for (let w = start; w <= end; w += 1) {
  71. weeks.push(w);
  72. }
  73. return;
  74. }
  75. const single = parseInt(seg, 10);
  76. if (Number.isFinite(single)) weeks.push(single);
  77. });
  78. let filtered = weeks;
  79. if (oddOnly && !evenOnly) {
  80. filtered = weeks.filter((w) => w % 2 === 1);
  81. } else if (evenOnly && !oddOnly) {
  82. filtered = weeks.filter((w) => w % 2 === 0);
  83. }
  84. return uniqueSortedNumbers(filtered);
  85. }
  86. function splitCourseBlocks(cellText) {
  87. const text = cellText.replace(/\r/g, "").trim();
  88. if (!text) return [];
  89. return text
  90. .split(/\n{2,}/)
  91. .map((block) => block.trim())
  92. .filter(Boolean);
  93. }
  94. function extractWeeksTextFromLine(line) {
  95. if (!line) return { weeksText: "", rest: line || "" };
  96. const match = line.match(/(\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*)\s*(?:\((单|双)\))?\s*周/);
  97. if (!match) return { weeksText: "", rest: line };
  98. const weeksCore = match[1];
  99. const oddEven = match[2] ? `(${match[2]})` : "";
  100. const weeksText = `${weeksCore}${oddEven}周`;
  101. const rest = line.replace(match[0], "").trim();
  102. return { weeksText, rest };
  103. }
  104. function parseCourseBlock(block) {
  105. const lines = block
  106. .split(/\n+/)
  107. .map((l) => l.trim())
  108. .filter(Boolean);
  109. if (!lines.length) return null;
  110. const name = lines[0] || "";
  111. let teacher = "";
  112. let weeksText = "";
  113. let position = "";
  114. const weekLineIndex = lines.findIndex((l) => /周/.test(l));
  115. if (weekLineIndex >= 0) {
  116. const { weeksText: extractedWeeks, rest } = extractWeeksTextFromLine(
  117. lines[weekLineIndex]
  118. );
  119. weeksText = extractedWeeks;
  120. if (weekLineIndex === 1) {
  121. teacher = rest || lines[1];
  122. }
  123. }
  124. if (!teacher && lines.length > 1) {
  125. teacher = lines[1];
  126. const { weeksText: extractedWeeks, rest } = extractWeeksTextFromLine(teacher);
  127. if (extractedWeeks) {
  128. weeksText = weeksText || extractedWeeks;
  129. teacher = rest;
  130. }
  131. }
  132. if (!weeksText) {
  133. for (const line of lines) {
  134. const { weeksText: extractedWeeks } = extractWeeksTextFromLine(line);
  135. if (extractedWeeks) {
  136. weeksText = extractedWeeks;
  137. break;
  138. }
  139. }
  140. }
  141. if (weekLineIndex >= 0 && weekLineIndex + 1 < lines.length) {
  142. position = lines[weekLineIndex + 1];
  143. }
  144. if (!position) {
  145. position =
  146. lines.find((l) => l !== name && l !== teacher && !/周/.test(l)) || "";
  147. }
  148. return {
  149. name: name || "未知课程",
  150. teacher: teacher || "",
  151. weeksText,
  152. position: position || "",
  153. };
  154. }
  155. function padTime(value) {
  156. const text = String(value || "").trim();
  157. const match = text.match(/^(\d{1,2}):(\d{2})$/);
  158. if (!match) return text;
  159. const h = match[1].padStart(2, "0");
  160. return `${h}:${match[2]}`;
  161. }
  162. function randomColor() {
  163. return Math.floor(Math.random() * 12) + 1;
  164. }
  165. function createId() {
  166. if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
  167. return crypto.randomUUID();
  168. }
  169. return `id-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
  170. }
  171. function mergeCourses(courses) {
  172. const byKey = new Map();
  173. courses.forEach((course) => {
  174. const weeksKey = (course.weeks || []).join(",");
  175. const key = [course.name, course.teacher, course.position, course.day, weeksKey].join("|");
  176. if (!byKey.has(key)) byKey.set(key, []);
  177. byKey.get(key).push({ ...course });
  178. });
  179. const merged = [];
  180. byKey.forEach((items) => {
  181. items.sort((a, b) => a.startSection - b.startSection);
  182. let current = null;
  183. items.forEach((item) => {
  184. if (!current) {
  185. current = { ...item };
  186. return;
  187. }
  188. if (item.startSection === current.endSection + 1) {
  189. current.endSection = Math.max(current.endSection, item.endSection);
  190. } else {
  191. merged.push(current);
  192. current = { ...item };
  193. }
  194. });
  195. if (current) merged.push(current);
  196. });
  197. return merged;
  198. }
  199. function parseScheduleFromDocument(doc) {
  200. const cells = Array.from(doc.querySelectorAll("td.cell"));
  201. const fallbackCells = cells.length ? [] : Array.from(doc.querySelectorAll("td[id^='Cell']"));
  202. const targetCells = cells.length ? cells : fallbackCells;
  203. const courses = [];
  204. const seen = new Set();
  205. targetCells.forEach((cell) => {
  206. const id = cell.getAttribute("id") || "";
  207. const match = id.match(/^Cell(\d)(\d{1,2})$/);
  208. if (!match) return;
  209. const day = parseInt(match[1], 10);
  210. const startSection = parseInt(match[2], 10);
  211. const rowspan = parseInt(cell.getAttribute("rowspan") || "1", 10);
  212. const endSection = startSection + Math.max(rowspan, 1) - 1;
  213. const blocks = splitCourseBlocks(cell.innerText || "");
  214. blocks.forEach((blockText) => {
  215. const parsed = parseCourseBlock(blockText);
  216. if (!parsed) return;
  217. const weeks = parseWeekText(parsed.weeksText);
  218. if (!weeks.length) return;
  219. const key = [
  220. parsed.name,
  221. parsed.teacher,
  222. parsed.position,
  223. day,
  224. startSection,
  225. endSection,
  226. weeks.join(","),
  227. ].join("|");
  228. if (seen.has(key)) return;
  229. seen.add(key);
  230. courses.push({
  231. id: createId(),
  232. name: parsed.name,
  233. teacher: parsed.teacher,
  234. position: parsed.position,
  235. day,
  236. startSection,
  237. endSection,
  238. color: randomColor(),
  239. weeks,
  240. });
  241. });
  242. });
  243. return mergeCourses(courses);
  244. }
  245. function parseTimeSlots(doc) {
  246. const slots = [];
  247. const seenNumbers = new Set();
  248. const timeRegex = /(\d{1,2}:\d{2})/g;
  249. const timeCells = Array.from(
  250. doc.querySelectorAll("td[data-jcindex], td[data-jcIndex]")
  251. );
  252. timeCells.forEach((cell) => {
  253. const text = (cell.innerText || cell.textContent || "").trim();
  254. if (!text) return;
  255. const indexAttr = cell.getAttribute("data-jcindex") || cell.getAttribute("data-jcIndex");
  256. const numberMatch = text.match(/^(\d{1,2})/);
  257. const number = parseInt(indexAttr || (numberMatch && numberMatch[1]) || "", 10);
  258. if (!Number.isFinite(number)) return;
  259. const times = text.match(timeRegex) || [];
  260. if (times.length < 2) return;
  261. if (seenNumbers.has(number)) return;
  262. seenNumbers.add(number);
  263. slots.push({
  264. number,
  265. startTime: padTime(times[0]),
  266. endTime: padTime(times[1]),
  267. });
  268. });
  269. return slots.sort((a, b) => a.number - b.number);
  270. }
  271. async function run() {
  272. toast("开始解析课表...");
  273. const doc = await getTargetDocument();
  274. if (!doc) {
  275. toast("未找到课表页面 iframe");
  276. return;
  277. }
  278. await waitForScheduleData(doc);
  279. const courses = parseScheduleFromDocument(doc);
  280. const timeSlots = parseTimeSlots(doc);
  281. if (!courses.length) {
  282. toast("未解析到课程,请确认课表已加载完成");
  283. return;
  284. }
  285. try {
  286. const result = await window.AndroidBridgePromise.saveImportedCourses(
  287. JSON.stringify(courses)
  288. );
  289. if (result === true) {
  290. if (timeSlots.length) {
  291. await window.AndroidBridgePromise.savePresetTimeSlots(
  292. JSON.stringify(timeSlots)
  293. );
  294. }
  295. toast("课表导出成功");
  296. window.AndroidBridge.notifyTaskCompletion();
  297. } else {
  298. toast("课表导出失败,请查看控制台日志");
  299. }
  300. } catch (error) {
  301. toast("导出失败: " + error.message);
  302. }
  303. }
  304. run();
  305. })();