yu.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. // 从 HTML 获取版
  2. (function () {
  3. function safeToast(message) {
  4. try {
  5. window.AndroidBridge && AndroidBridge.showToast(message);
  6. } catch (_) {
  7. console.log("[Toast Fallback]", message);
  8. }
  9. }
  10. function firstNonEmpty(...values) {
  11. for (const value of values) {
  12. if (value !== undefined && value !== null && String(value).trim() !== "") {
  13. return String(value).trim();
  14. }
  15. }
  16. return "";
  17. }
  18. function parseValidWeeksBitmap(bitmap) {
  19. if (!bitmap || typeof bitmap !== "string") return [];
  20. const weeks = [];
  21. for (let i = 0; i < bitmap.length; i++) {
  22. if (bitmap[i] === "1") {
  23. if (i >= 1) weeks.push(i);
  24. }
  25. }
  26. return weeks;
  27. }
  28. function parseWeeksExpression(expr) {
  29. const text = (expr || "").trim();
  30. if (!text) return [];
  31. const oddOnly = text.startsWith("单");
  32. const evenOnly = text.startsWith("双");
  33. const raw = text.replace(/^[单双]/, "");
  34. const matchRange = raw.match(/^(\d+)\s*-\s*(\d+)$/);
  35. if (matchRange) {
  36. const start = parseInt(matchRange[1], 10);
  37. const end = parseInt(matchRange[2], 10);
  38. if (Number.isNaN(start) || Number.isNaN(end) || end < start) return [];
  39. const weeks = [];
  40. for (let w = start; w <= end; w++) {
  41. if (oddOnly && w % 2 === 0) continue;
  42. if (evenOnly && w % 2 !== 0) continue;
  43. weeks.push(w);
  44. }
  45. return weeks;
  46. }
  47. const nums = raw
  48. .split(/[,,、\s]+/)
  49. .map((t) => parseInt(t, 10))
  50. .filter((n) => !Number.isNaN(n) && n > 0);
  51. if (!oddOnly && !evenOnly) return nums;
  52. return nums.filter((w) => (oddOnly ? w % 2 === 1 : w % 2 === 0));
  53. }
  54. function normalizeWeeks(weeks) {
  55. const uniq = Array.from(new Set((weeks || []).filter((n) => Number.isInteger(n) && n > 0)));
  56. uniq.sort((a, b) => a - b);
  57. return uniq;
  58. }
  59. function cleanCourseName(name) {
  60. return String(name).replace(/\(\d+\)\s*$/, "").trim();
  61. }
  62. function extractTeacherFromCourse(obj) {
  63. return firstNonEmpty(
  64. obj.teacherName,
  65. obj.teachers,
  66. obj.teacher,
  67. obj.teacherNames,
  68. obj.teachername,
  69. obj.courseteacher
  70. );
  71. }
  72. function extractPositionFromCourse(obj) {
  73. return firstNonEmpty(
  74. obj.room,
  75. obj.roomName,
  76. obj.position,
  77. obj.place,
  78. obj.classroom,
  79. obj.location,
  80. obj.addr
  81. );
  82. }
  83. function extractWeeksFromCourse(obj) {
  84. return normalizeWeeks(parseValidWeeksBitmap(firstNonEmpty(
  85. obj.vaildWeeks,
  86. obj.validWeeks,
  87. obj.weeks,
  88. obj.weekBitmap,
  89. obj.weekString
  90. )));
  91. }
  92. function createCourseObject(name, teacher, position, day, startSection, endSection, weeks) {
  93. return {
  94. name: cleanCourseName(name),
  95. teacher: teacher || "",
  96. position: position || "",
  97. day,
  98. startSection,
  99. endSection,
  100. weeks: normalizeWeeks(weeks)
  101. };
  102. }
  103. function parseCourseNameAndTeacher(courseWithTeacher) {
  104. const text = (courseWithTeacher || "").trim();
  105. if (!text) return { name: "", teacher: "" };
  106. // 去掉课程名称末尾的序号
  107. let cleaned = cleanCourseName(text);
  108. // 匹配末尾教师名
  109. const match = cleaned.match(/^(.*)\s+\(([^()]*)\)\s*$/);
  110. if (match) {
  111. return {
  112. name: match[1].trim(),
  113. teacher: match[2].trim()
  114. };
  115. }
  116. return { name: cleaned, teacher: "" };
  117. }
  118. function parseTitleToCourses(titleText, day, section) {
  119. if (!titleText || !titleText.trim()) return [];
  120. const parts = titleText
  121. .split(";")
  122. .map((p) => p.trim())
  123. .filter((p) => p.length > 0);
  124. const results = [];
  125. for (let i = 0; i < parts.length; i++) {
  126. const current = parts[i];
  127. const next = parts[i + 1] || "";
  128. if (current.startsWith("(")) continue;
  129. const { name, teacher } = parseCourseNameAndTeacher(current);
  130. if (!name) continue;
  131. let weeks = [];
  132. let position = "";
  133. if (next.startsWith("(") && next.endsWith(")")) {
  134. const inner = next.slice(1, -1);
  135. const commaIndex = inner.indexOf(",");
  136. if (commaIndex >= 0) {
  137. const weekExpr = inner.slice(0, commaIndex).trim();
  138. position = inner.slice(commaIndex + 1).trim();
  139. weeks = parseWeeksExpression(weekExpr);
  140. } else {
  141. const isPureWeeks = /^\d+[-,,]|^[单双]\d/.test(inner);
  142. if (isPureWeeks) {
  143. weeks = parseWeeksExpression(inner);
  144. } else {
  145. position = inner;
  146. }
  147. }
  148. }
  149. results.push(createCourseObject(name, teacher, position, day, section, section, weeks));
  150. }
  151. return results;
  152. }
  153. function parseFromCourseTableObjects() {
  154. const candidates = [];
  155. for (const key of Object.keys(window)) {
  156. if (!/^table\d+$/.test(key)) continue;
  157. const obj = window[key];
  158. if (obj && Array.isArray(obj.activities) && Number.isInteger(obj.unitCounts)) {
  159. candidates.push({ name: key, obj });
  160. }
  161. }
  162. const courses = [];
  163. for (const candidate of candidates) {
  164. const table = candidate.obj;
  165. const totalCells = table.activities.length;
  166. let unitCount = table.unitCounts;
  167. // 如果 unitCount > 7,尝试推断为总数,计算单行列数
  168. if (unitCount > 7 && totalCells > 0) {
  169. const deducedUnitCount = Math.floor(totalCells / 7);
  170. if (deducedUnitCount > 0 && deducedUnitCount < totalCells && deducedUnitCount <= 12) {
  171. unitCount = deducedUnitCount;
  172. }
  173. }
  174. console.log(`[Debug] Table ${candidate.name}: unitCounts=${table.unitCounts}, totalCells=${totalCells}, deduced unitCount=${unitCount}`);
  175. if (unitCount < 1 || unitCount >= totalCells) {
  176. console.warn(`[Warn] Invalid unitCount ${unitCount} for table ${candidate.name}, skip`);
  177. continue;
  178. }
  179. for (let index = 0; index < totalCells; index++) {
  180. const activitiesInCell = table.activities[index];
  181. if (!Array.isArray(activitiesInCell) || activitiesInCell.length === 0) continue;
  182. const day = Math.floor(index / unitCount) + 1;
  183. const section = (index % unitCount) + 1;
  184. if (day < 1 || day > 7 || section < 1 || section > 12) continue;
  185. for (const act of activitiesInCell) {
  186. if (!act) continue;
  187. let name = firstNonEmpty(act.courseName, act.name);
  188. if (!name) continue;
  189. const teacher = extractTeacherFromCourse(act);
  190. const position = extractPositionFromCourse(act);
  191. const weeks = extractWeeksFromCourse(act);
  192. courses.push(createCourseObject(name, teacher, position, day, section, section, weeks));
  193. }
  194. }
  195. }
  196. return courses;
  197. }
  198. function parseFromHtmlTableFallback() {
  199. const table = document.querySelector("#manualArrangeCourseTable");
  200. if (!table) return [];
  201. const bodyRows = table.querySelectorAll("tbody tr");
  202. const courses = [];
  203. bodyRows.forEach((row, rowIndex) => {
  204. const cells = row.querySelectorAll("td");
  205. if (cells.length < 8) return;
  206. const section = rowIndex + 1;
  207. for (let col = 1; col <= 7; col++) {
  208. const td = cells[col];
  209. if (!td) continue;
  210. const title = td.getAttribute("title") || "";
  211. if (!title.trim()) continue;
  212. const day = col;
  213. const parsed = parseTitleToCourses(title, day, section);
  214. courses.push(...parsed);
  215. }
  216. });
  217. return courses;
  218. }
  219. function extractPositionFromTitle(title) {
  220. const positionMatch = title.match(/\(([^(),]*)\)\s*$/);
  221. if (!positionMatch) return "";
  222. const potential = positionMatch[1].trim();
  223. // 排除掉是周次表达式的情况
  224. if (!/^\d+[-~]|^[单双]|^\d+$/.test(potential)) {
  225. return potential;
  226. }
  227. return "";
  228. }
  229. function supplementPositionFromHtml(courses) {
  230. const table = document.querySelector("#manualArrangeCourseTable");
  231. if (!table) return courses;
  232. const courseMap = {};
  233. for (const course of courses) {
  234. // 用课程名、教师、日期、时间作为 key
  235. const key = `${course.name}|${course.teacher}|${course.day}|${course.startSection}`;
  236. if (!courseMap[key]) {
  237. courseMap[key] = [];
  238. }
  239. courseMap[key].push(course);
  240. }
  241. const bodyRows = table.querySelectorAll("tbody tr");
  242. bodyRows.forEach((row, rowIndex) => {
  243. const cells = row.querySelectorAll("td");
  244. if (cells.length < 8) return;
  245. const section = rowIndex + 1;
  246. for (let col = 1; col <= 7; col++) {
  247. const td = cells[col];
  248. if (!td) continue;
  249. const title = td.getAttribute("title") || "";
  250. if (!title.trim()) continue;
  251. const day = col;
  252. const position = extractPositionFromTitle(title);
  253. // 从 title 提取课程信息并匹配
  254. const titleParts = title.split(";").map(p => p.trim()).filter(p => p && !p.startsWith("("));
  255. for (const part of titleParts) {
  256. const { name, teacher } = parseCourseNameAndTeacher(part);
  257. if (!name) continue;
  258. const key = `${name}|${teacher}|${day}|${section}`;
  259. if (courseMap[key]) {
  260. for (const course of courseMap[key]) {
  261. if (!course.position) {
  262. course.position = position;
  263. }
  264. }
  265. }
  266. }
  267. }
  268. });
  269. return courses;
  270. }
  271. function mergeContiguousSections(courses) {
  272. const normalized = (courses || [])
  273. .filter((c) => c && c.name && Number.isInteger(c.day) && Number.isInteger(c.startSection) && Number.isInteger(c.endSection))
  274. .map((c) => ({
  275. ...c,
  276. weeks: normalizeWeeks(c.weeks)
  277. }));
  278. normalized.sort((a, b) => {
  279. const ak = `${a.name}|${a.teacher}|${a.position}|${a.day}|${a.weeks.join(",")}`;
  280. const bk = `${b.name}|${b.teacher}|${b.position}|${b.day}|${b.weeks.join(",")}`;
  281. if (ak < bk) return -1;
  282. if (ak > bk) return 1;
  283. return a.startSection - b.startSection;
  284. });
  285. const merged = [];
  286. for (const item of normalized) {
  287. const prev = merged[merged.length - 1];
  288. const isContinuous = prev
  289. && prev.name === item.name
  290. && prev.teacher === item.teacher
  291. && prev.position === item.position
  292. && prev.day === item.day
  293. && prev.weeks.join(",") === item.weeks.join(",")
  294. && prev.endSection + 1 >= item.startSection;
  295. if (isContinuous) {
  296. prev.endSection = Math.max(prev.endSection, item.endSection);
  297. } else {
  298. merged.push({ ...item });
  299. }
  300. }
  301. return merged;
  302. }
  303. async function exportAllCourseData() {
  304. safeToast("开始解析教务课表...");
  305. console.log("[Exporter] 开始解析课表");
  306. let parsedCourses = parseFromCourseTableObjects();
  307. if (parsedCourses.length === 0) {
  308. console.warn("[Exporter] 未从 tableX.activities 取到数据,尝试 HTML 兜底解析");
  309. parsedCourses = parseFromHtmlTableFallback();
  310. } else {
  311. console.log(`[Exporter] 从 table.activities 获取 ${parsedCourses.length} 条课程,尝试补充位置信息...`);
  312. // 尝试从 HTML 补充位置信息
  313. parsedCourses = supplementPositionFromHtml(parsedCourses);
  314. console.log(`[Exporter] 补充位置后 ${parsedCourses.length} 条课程`);
  315. }
  316. parsedCourses = mergeContiguousSections(parsedCourses);
  317. if (parsedCourses.length === 0) {
  318. throw new Error("未在当前页面识别到可导出的课程数据,请确认已打开我的课表页面。");
  319. }
  320. console.log(`[Exporter] 解析完成,课程条目数: ${parsedCourses.length}`);
  321. console.log(`[Exporter] 样本课程:`, JSON.stringify(parsedCourses.slice(0, 2), null, 2));
  322. const presetTimeSlots = [
  323. {
  324. "number": 1,
  325. "startTime": "08:00",
  326. "endTime": "08:45"
  327. },
  328. {
  329. "number": 2,
  330. "startTime": "10:05",
  331. "endTime": "11:40"
  332. },
  333. {
  334. "number": 3,
  335. "startTime": "14:00",
  336. "endTime": "15:35"
  337. },
  338. {
  339. "number": 4,
  340. "startTime": "16:05",
  341. "endTime": "17:40"
  342. },
  343. {
  344. "number": 5,
  345. "startTime": "19:00",
  346. "endTime": "20:35"
  347. },
  348. {
  349. "number": 6,
  350. "startTime": "20:45",
  351. "endTime": "22:20"
  352. }
  353. ];
  354. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
  355. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
  356. safeToast(`导出成功,共 ${parsedCourses.length} 条课程`);
  357. }
  358. (async function run() {
  359. try {
  360. await exportAllCourseData();
  361. } catch (error) {
  362. console.error("[Exporter] 导出失败:", error);
  363. safeToast(`导出失败:${error.message}`);
  364. } finally {
  365. try {
  366. window.AndroidBridge && AndroidBridge.notifyTaskCompletion();
  367. } catch (e) {
  368. console.error("[Exporter] notifyTaskCompletion 调用失败:", e);
  369. }
  370. }
  371. })();
  372. })();