wzzy_01.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. // 梧州职业学院(wzzy.edu.cn)拾光适配代码
  2. // 非该大学开发者适配,开发者无法及时发现问题
  3. // 出现问题请提issues或者提交pr更改,这更加快速
  4. /**
  5. * 基础工具函数:Base64 编码
  6. */
  7. function encodeParams(xn, xq) {
  8. const rawStr = `xn=${xn}&xq=${xq}`;
  9. return btoa(rawStr);
  10. }
  11. /**
  12. * 深度解析周数字符串 (支持 1-16, 1-8单, 9-17双, 1,3,5等格式)
  13. */
  14. function parseWeeks(weekStr) {
  15. const weeks = [];
  16. const groups = weekStr.split(',');
  17. groups.forEach(group => {
  18. const isSingle = group.includes('单');
  19. const isDouble = group.includes('双');
  20. const rangeMatch = group.match(/(\d+)-(\d+)/);
  21. if (rangeMatch) {
  22. const start = parseInt(rangeMatch[1]);
  23. const end = parseInt(rangeMatch[2]);
  24. for (let i = start; i <= end; i++) {
  25. if (isSingle && i % 2 === 0) continue;
  26. if (isDouble && i % 2 !== 0) continue;
  27. weeks.push(i);
  28. }
  29. } else {
  30. const num = parseInt(group.replace(/[^\d]/g, ''));
  31. if (!isNaN(num)) weeks.push(num);
  32. }
  33. });
  34. return Array.from(new Set(weeks)).sort((a, b) => a - b);
  35. }
  36. /**
  37. * 数据解析函数
  38. */
  39. function parseAndMergeXmcuData(htmlText) {
  40. const parser = new DOMParser();
  41. const doc = parser.parseFromString(htmlText, 'text/html');
  42. const rawItems = [];
  43. const table = doc.getElementById('mytable');
  44. if (!table) return [];
  45. const rows = table.querySelectorAll('tr');
  46. rows.forEach((row) => {
  47. // 排除表头和特殊行,只处理包含课程内容的行
  48. const cells = row.querySelectorAll('td.td');
  49. if (cells.length === 0) return;
  50. cells.forEach((cell, dayIndex) => {
  51. const day = dayIndex + 1; // 对应 星期一 到 星期五
  52. // 找到所有包含课程信息的 div
  53. const courseDivs = cell.querySelectorAll('div[style*="padding-bottom:5px"]');
  54. courseDivs.forEach(div => {
  55. // 提取文本并过滤空行
  56. const lines = Array.from(div.childNodes)
  57. .map(n => n.textContent.trim())
  58. .filter(t => t.length > 0);
  59. if (lines.length >= 3) {
  60. const name = lines[0];
  61. const teacher = lines[1];
  62. // 匹配格式:周数[节次] -> 例如 "1-8 单 [3-4]"
  63. const timeMatch = lines[2].match(/(.*)\[(.*)\]/);
  64. const position = lines[3] || "未知地点";
  65. if (timeMatch) {
  66. const weeks = parseWeeks(timeMatch[1]);
  67. const sections = timeMatch[2].split('-').map(Number);
  68. rawItems.push({
  69. name,
  70. teacher,
  71. position,
  72. day,
  73. startSection: sections[0],
  74. endSection: sections[sections.length - 1],
  75. weeks
  76. });
  77. }
  78. }
  79. });
  80. });
  81. });
  82. const groupMap = new Map();
  83. rawItems.forEach(item => {
  84. const key = `${item.name}|${item.teacher}|${item.position}|${item.day}`;
  85. if (!groupMap.has(key)) groupMap.set(key, []);
  86. groupMap.get(key).push(item);
  87. });
  88. const finalCourses = [];
  89. groupMap.forEach((items, key) => {
  90. const matrix = {};
  91. items.forEach(item => {
  92. item.weeks.forEach(w => {
  93. if (!matrix[w]) matrix[w] = new Set();
  94. for (let s = item.startSection; s <= item.endSection; s++) matrix[w].add(s);
  95. });
  96. });
  97. const patternMap = new Map();
  98. Object.keys(matrix).forEach(w => {
  99. const week = parseInt(w);
  100. const sections = Array.from(matrix[week]).sort((a, b) => a - b);
  101. let start = sections[0];
  102. for (let i = 0; i < sections.length; i++) {
  103. if (i === sections.length - 1 || sections[i+1] !== sections[i] + 1) {
  104. const pKey = `${start}-${sections[i]}`;
  105. if (!patternMap.has(pKey)) patternMap.set(pKey, []);
  106. patternMap.get(pKey).push(week);
  107. if (i < sections.length - 1) start = sections[i+1];
  108. }
  109. }
  110. });
  111. const [name, teacher, position, day] = key.split('|');
  112. patternMap.forEach((weeks, pKey) => {
  113. const [sStart, sEnd] = pKey.split('-').map(Number);
  114. finalCourses.push({
  115. name, teacher, position,
  116. day: parseInt(day),
  117. startSection: sStart,
  118. endSection: sEnd,
  119. weeks: weeks.sort((a, b) => a - b)
  120. });
  121. });
  122. });
  123. return finalCourses;
  124. }
  125. /**
  126. * 节次与周次合并去重函数(供开发者参考)
  127. * @param {Array<Object>} courses 原始解析课程数组
  128. * @returns {Array<Object>} 合并去重后的课程数组
  129. */
  130. function mergeAndDistinctCourses(courses) {
  131. if (!Array.isArray(courses) || courses.length <= 1) return courses;
  132. // 1. 深拷贝并规范周次数据,过滤无效项
  133. const list = courses.map(c => ({
  134. ...c,
  135. name: c.name || '',
  136. teacher: c.teacher || '',
  137. position: c.position || '',
  138. weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
  139. }));
  140. // 阶段 1:合并连续节次与完全重复记录(前提:名称、教师、地点、星期、周次一致)
  141. list.sort((a, b) => {
  142. return a.name.localeCompare(b.name) ||
  143. a.teacher.localeCompare(b.teacher) ||
  144. a.position.localeCompare(b.position) ||
  145. (a.day || 0) - (b.day || 0) ||
  146. a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
  147. (a.startSection || 0) - (b.startSection || 0);
  148. });
  149. const step1Merged = [];
  150. let current = list[0];
  151. for (let i = 1; i < list.length; i++) {
  152. const next = list[i];
  153. const isSameCourseAndWeeks =
  154. current.name === next.name &&
  155. current.teacher === next.teacher &&
  156. current.position === next.position &&
  157. current.day === next.day &&
  158. current.weeks.join(',') === next.weeks.join(',');
  159. const isContinuous = current.endSection + 1 === next.startSection;
  160. const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
  161. if (isSameCourseAndWeeks && isContinuous) {
  162. // 节次连续:延长结束节次 (如 1-2 节 + 3-4 节 -> 1-4 节)
  163. current.endSection = next.endSection;
  164. } else if (isSameCourseAndWeeks && isDuplicate) {
  165. // 完全重复:跳过
  166. continue;
  167. } else {
  168. step1Merged.push(current);
  169. current = next;
  170. }
  171. }
  172. step1Merged.push(current);
  173. // 阶段 2:合并同节次的周次(前提:名称、教师、地点、星期、开始/结束节次一致)
  174. step1Merged.sort((a, b) => {
  175. return a.name.localeCompare(b.name) ||
  176. a.teacher.localeCompare(b.teacher) ||
  177. a.position.localeCompare(b.position) ||
  178. (a.day || 0) - (b.day || 0) ||
  179. (a.startSection || 0) - (b.startSection || 0) ||
  180. (a.endSection || 0) - (b.endSection || 0);
  181. });
  182. const step2Merged = [];
  183. let cur = step1Merged[0];
  184. for (let i = 1; i < step1Merged.length; i++) {
  185. const nxt = step1Merged[i];
  186. const isSameCourseAndSection =
  187. cur.name === nxt.name &&
  188. cur.teacher === nxt.teacher &&
  189. cur.position === nxt.position &&
  190. cur.day === nxt.day &&
  191. cur.startSection === nxt.startSection &&
  192. cur.endSection === nxt.endSection;
  193. if (isSameCourseAndSection) {
  194. // 周次合并去重 (如 1-8 周 + 9-16 周 -> 1-16 周)
  195. cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
  196. } else {
  197. step2Merged.push(cur);
  198. cur = nxt;
  199. }
  200. }
  201. step2Merged.push(cur);
  202. return step2Merged;
  203. }
  204. /**
  205. * 学期获取函数
  206. */
  207. async function getYearAndSemester() {
  208. try {
  209. window.shiguangBridge.showToast("正在获取学期列表...");
  210. const response = await fetch("http://222.217.195.24:805/wzzyjw/frame/droplist/getDropLists.action", {
  211. method: "POST",
  212. headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
  213. body: "comboBoxName=StMsXnxqDxDesc&paramValue=&isYXB=0&isCDDW=0&isXQ=0&isDJKSLB=0&isZY=0",
  214. credentials: "include"
  215. });
  216. const list = await response.json();
  217. const names = list.map(item => item.name);
  218. const selectedIndex = await window.shiguangBridgePromise.showSingleSelection("选择导入学期", JSON.stringify(names), 0);
  219. if (selectedIndex === null) return null;
  220. const [xn, xq] = list[selectedIndex].code.split('-');
  221. return { xn, xq };
  222. } catch (error) {
  223. window.shiguangBridge.showToast("获取列表失败");
  224. return null;
  225. }
  226. }
  227. /**
  228. * 课表抓取函数
  229. */
  230. async function fetchCourses(xn, xq) {
  231. try {
  232. const paramsBase64 = encodeParams(xn, xq);
  233. const url = `http://222.217.195.24:805/wzzyjw/student/wsxk.xskcb10319.jsp?params=${paramsBase64}`;
  234. window.shiguangBridge.showToast("正在提取数据...");
  235. const response = await fetch(url, { method: "GET", credentials: "include" });
  236. const arrayBuffer = await response.arrayBuffer();
  237. const htmlText = new TextDecoder('gbk').decode(arrayBuffer);
  238. return parseAndMergeXmcuData(htmlText);
  239. } catch (error) {
  240. window.shiguangBridge.showToast("抓取课表失败");
  241. return null;
  242. }
  243. }
  244. /**
  245. * 获取开学日期函数
  246. * 通过第一周课表 HTML 解析出第一周周一的日期
  247. * @param {String} xn 学年(如 "2026")
  248. * @param {String} xq 学期码("0"=第一学期/"1"=第二学期)
  249. * @returns {String|null} 格式 YYYY-MM-DD 的开学日期,失败返回 null
  250. */
  251. async function fetchSemesterStartDate(xn, xq) {
  252. try {
  253. window.shiguangBridge.showToast("正在获取开学日期...");
  254. const url = `http://222.217.195.24:805/wzzyjw/frame/desk/showLessonScheduleInfosV14.action?xn=${xn}&xq=${xq}&jxz=1`;
  255. const response = await fetch(url, {
  256. method: "POST",
  257. headers: { "x-requested-with": "XMLHttpRequest" },
  258. credentials: "include"
  259. });
  260. const arrayBuffer = await response.arrayBuffer();
  261. const htmlText = new TextDecoder('gbk').decode(arrayBuffer);
  262. const match = htmlText.match(/<br\s*\/?>\s*(\d{2})-(\d{2})/);
  263. if (match) {
  264. const month = match[1];
  265. const day = match[2];
  266. const year = xq === "1" ? String(parseInt(xn) + 1) : xn;
  267. return `${year}-${month}-${day}`;
  268. }
  269. return null;
  270. } catch (error) {
  271. window.shiguangBridge.showToast("获取开学日期失败");
  272. return null;
  273. }
  274. }
  275. /**
  276. * 开学日期输入验证函数(供 showPrompt 调用)
  277. * 返回 false 表示验证通过,返回字符串表示错误信息
  278. */
  279. function validateDateInput(input) {
  280. if (/^\d{4}-\d{2}-\d{2}$/.test(input)) {
  281. return false; // 验证通过
  282. }
  283. return "请输入正确格式的日期,如 2026-03-09";
  284. }
  285. /**
  286. * 时间段导入函数
  287. */
  288. async function importPresetTimeSlots() {
  289. const slots = [
  290. { "number": 1, "startTime": "08:00", "endTime": "08:40" },
  291. { "number": 2, "startTime": "08:50", "endTime": "09:30" },
  292. { "number": 3, "startTime": "09:50", "endTime": "10:30" },
  293. { "number": 4, "startTime": "10:40", "endTime": "11:20" },
  294. { "number": 5, "startTime": "11:30", "endTime": "12:10" },
  295. { "number": 6, "startTime": "14:30", "endTime": "15:10" },
  296. { "number": 7, "startTime": "15:20", "endTime": "16:00" },
  297. { "number": 8, "startTime": "16:10", "endTime": "16:50" },
  298. { "number": 9, "startTime": "17:00", "endTime": "17:40" },
  299. { "number": 10, "startTime": "18:45", "endTime": "19:25" },
  300. { "number": 11, "startTime": "19:35", "endTime": "20:15" },
  301. { "number": 12, "startTime": "20:25", "endTime": "21:05" },
  302. { "number": 13, "startTime": "21:15", "endTime": "21:55" }
  303. ];
  304. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(slots)).catch(() => {});
  305. }
  306. /**
  307. * 最终流程控制
  308. */
  309. async function runImportFlow() {
  310. // 弹窗确认
  311. const confirmed = await window.shiguangBridgePromise.showAlert("教务导入", "确认已经登录并进入教务系统", "确定");
  312. if (!confirmed) return;
  313. // 选择学期
  314. const params = await getYearAndSemester();
  315. if (!params) return;
  316. // 获取并解析数据
  317. const courses = await fetchCourses(params.xn, params.xq);
  318. if (!courses || courses.length === 0) {
  319. window.shiguangBridge.showToast("未找到有效课程");
  320. return;
  321. }
  322. // 合并去重
  323. const finalCourses = mergeAndDistinctCourses(courses);
  324. // 获取开学日期并让用户确认
  325. const startDate = await fetchSemesterStartDate(params.xn, params.xq);
  326. const confirmedDate = await window.shiguangBridgePromise.showPrompt(
  327. "确认开学日期",
  328. "请确认本学期第一周周一的日期(格式 YYYY-MM-DD):",
  329. startDate || "",
  330. "validateDateInput"
  331. );
  332. if (confirmedDate === null) {
  333. window.shiguangBridge.showToast("导入已取消。");
  334. return;
  335. }
  336. // 保存课表配置
  337. // 从课程周次中推算本学期总周数(取所有课程 weeks 的最大值)
  338. let maxWeek = 0;
  339. finalCourses.forEach(c => {
  340. c.weeks.forEach(w => { if (w > maxWeek) maxWeek = w; });
  341. });
  342. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
  343. semesterStartDate: confirmedDate,
  344. semesterTotalWeeks: maxWeek > 0 ? maxWeek : 20,
  345. defaultClassDuration: 40,
  346. defaultBreakDuration: 10
  347. })).catch(() => {});
  348. // 存储
  349. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(finalCourses));
  350. await importPresetTimeSlots();
  351. // 完成
  352. window.shiguangBridge.showToast(`导入成功:共 ${finalCourses.length} 门课程`);
  353. window.shiguangBridge.notifyTaskCompletion();
  354. }
  355. // 启动
  356. runImportFlow();