fosu_01.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. /**
  2. * 佛山大学强智教务系统适配
  3. * @since 2026-5-14
  4. * @description 支持课程表导入,需要校园网访问
  5. * @author e7g
  6. * @version 1.0
  7. */
  8. function parseWeeksString(weekStr) {
  9. const weeks = [];
  10. if (!weekStr) return weeks;
  11. let cleanStr = weekStr;
  12. while (cleanStr.includes('(周)')) {
  13. cleanStr = cleanStr.replace('(周)', '');
  14. }
  15. while (cleanStr.includes('周')) {
  16. cleanStr = cleanStr.replace('周', '');
  17. }
  18. cleanStr = cleanStr.trim();
  19. const parts = cleanStr.split(',');
  20. for (const part of parts) {
  21. const trimmed = part.trim();
  22. if (trimmed.includes('-')) {
  23. const nums = trimmed.split('-').map(s => parseInt(s.trim(), 10));
  24. const start = nums[0];
  25. const end = nums[1];
  26. if (!isNaN(start) && !isNaN(end)) {
  27. for (let w = start; w <= end; w++) {
  28. weeks.push(w);
  29. }
  30. }
  31. } else {
  32. const week = parseInt(trimmed, 10);
  33. if (!isNaN(week)) {
  34. weeks.push(week);
  35. }
  36. }
  37. }
  38. return weeks.sort((a, b) => a - b);
  39. }
  40. function parseSectionFromText(text) {
  41. const startIdx = text.indexOf('[');
  42. const endIdx = text.indexOf(']节');
  43. if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
  44. return null;
  45. }
  46. const sectionStr = text.substring(startIdx + 1, endIdx);
  47. const sections = sectionStr.split('-').map(s => parseInt(s.trim(), 10));
  48. if (sections.some(s => isNaN(s))) {
  49. return null;
  50. }
  51. return {
  52. start: Math.min(...sections),
  53. end: Math.max(...sections)
  54. };
  55. }
  56. function removeSectionFromText(text) {
  57. const startIdx = text.indexOf('[');
  58. const endIdx = text.indexOf(']节');
  59. if (startIdx === -1 || endIdx === -1) {
  60. return text;
  61. }
  62. return text.substring(0, startIdx).trim();
  63. }
  64. function extractFontContent(line) {
  65. const startTag = line.indexOf('<font');
  66. if (startTag === -1) return null;
  67. const closeTag = line.indexOf('>', startTag);
  68. if (closeTag === -1) return null;
  69. const endTag = line.indexOf('</font>', closeTag);
  70. if (endTag === -1) return null;
  71. return line.substring(closeTag + 1, endTag).trim();
  72. }
  73. function removeHtmlTags(text) {
  74. let result = '';
  75. let inTag = false;
  76. for (let i = 0; i < text.length; i++) {
  77. if (text[i] === '<') {
  78. inTag = true;
  79. } else if (text[i] === '>') {
  80. inTag = false;
  81. } else if (!inTag) {
  82. result += text[i];
  83. }
  84. }
  85. return result.trim();
  86. }
  87. function parseCourseFromDiv(divContent, dayIndex, sectionIndex) {
  88. const courses = [];
  89. if (!divContent || divContent.includes('&nbsp;') || divContent.trim() === '') {
  90. return courses;
  91. }
  92. const courseBlocks = [];
  93. let currentBlock = '';
  94. let dashCount = 0;
  95. for (let i = 0; i < divContent.length; i++) {
  96. if (divContent[i] === '-') {
  97. dashCount++;
  98. } else {
  99. if (dashCount >= 10) {
  100. if (currentBlock.trim()) {
  101. courseBlocks.push(currentBlock);
  102. }
  103. currentBlock = '';
  104. } else if (dashCount > 0) {
  105. for (let j = 0; j < dashCount; j++) {
  106. currentBlock += '-';
  107. }
  108. }
  109. currentBlock += divContent[i];
  110. dashCount = 0;
  111. }
  112. }
  113. if (currentBlock.trim()) {
  114. courseBlocks.push(currentBlock);
  115. }
  116. let pendingCourse = null;
  117. for (const block of courseBlocks) {
  118. const trimmedBlock = block.trim();
  119. if (!trimmedBlock || trimmedBlock.includes('&nbsp;')) continue;
  120. const lines = trimmedBlock.split('<br>').map(l => l.trim()).filter(l => l);
  121. if (lines.length === 0) continue;
  122. const courseName = removeHtmlTags(lines[0]);
  123. let teacher = '';
  124. let position = '';
  125. let weeks = [];
  126. let startSection = sectionIndex * 2 - 1;
  127. let endSection = sectionIndex * 2;
  128. for (let i = 1; i < lines.length; i++) {
  129. const line = lines[i];
  130. if (line.includes('title="老师"')) {
  131. const content = extractFontContent(line);
  132. if (content) {
  133. teacher = content;
  134. }
  135. } else if (line.includes('title="周次(节次)"')) {
  136. const content = extractFontContent(line);
  137. if (content) {
  138. weeks = parseWeeksString(content);
  139. }
  140. } else if (line.includes('title="教室"')) {
  141. const content = extractFontContent(line);
  142. if (content) {
  143. position = content;
  144. const sectionMatch = parseSectionFromText(content);
  145. if (sectionMatch) {
  146. startSection = sectionMatch.start;
  147. endSection = sectionMatch.end;
  148. position = removeSectionFromText(content);
  149. }
  150. }
  151. }
  152. }
  153. if (teacher && !weeks.length) {
  154. pendingCourse = {
  155. name: courseName,
  156. teacher: teacher,
  157. position: '',
  158. day: dayIndex,
  159. startSection: startSection,
  160. endSection: endSection,
  161. weeks: []
  162. };
  163. } else if (weeks.length > 0) {
  164. if (pendingCourse && pendingCourse.name === courseName) {
  165. pendingCourse.position = position;
  166. pendingCourse.startSection = startSection;
  167. pendingCourse.endSection = endSection;
  168. pendingCourse.weeks = weeks;
  169. courses.push(pendingCourse);
  170. pendingCourse = null;
  171. } else {
  172. if (courseName && weeks.length > 0) {
  173. courses.push({
  174. name: courseName,
  175. teacher: teacher,
  176. position: position,
  177. day: dayIndex,
  178. startSection: startSection,
  179. endSection: endSection,
  180. weeks: weeks
  181. });
  182. }
  183. }
  184. }
  185. }
  186. return courses;
  187. }
  188. function findTagContent(html, tagName, startFrom) {
  189. const openTag = '<' + tagName;
  190. const closeTag = '</' + tagName + '>';
  191. let start = html.indexOf(openTag, startFrom || 0);
  192. if (start === -1) return null;
  193. const tagEnd = html.indexOf('>', start);
  194. if (tagEnd === -1) return null;
  195. let depth = 1;
  196. let pos = tagEnd + 1;
  197. while (depth > 0 && pos < html.length) {
  198. const nextOpen = html.indexOf(openTag, pos);
  199. const nextClose = html.indexOf(closeTag, pos);
  200. if (nextClose === -1) return null;
  201. if (nextOpen !== -1 && nextOpen < nextClose) {
  202. depth++;
  203. pos = html.indexOf('>', nextOpen) + 1;
  204. } else {
  205. depth--;
  206. if (depth === 0) {
  207. return {
  208. content: html.substring(tagEnd + 1, nextClose),
  209. endPos: nextClose + closeTag.length
  210. };
  211. }
  212. pos = nextClose + closeTag.length;
  213. }
  214. }
  215. return null;
  216. }
  217. function findAllTags(html, tagName) {
  218. const results = [];
  219. let pos = 0;
  220. while (true) {
  221. const result = findTagContent(html, tagName, pos);
  222. if (!result) break;
  223. results.push(result.content);
  224. pos = result.endPos;
  225. }
  226. return results;
  227. }
  228. function findTagWithAttr(html, tagName, attrName, attrValue) {
  229. const openTag = '<' + tagName;
  230. const closeTag = '</' + tagName + '>';
  231. let pos = 0;
  232. while (true) {
  233. let start = html.indexOf(openTag, pos);
  234. if (start === -1) return null;
  235. const tagEnd = html.indexOf('>', start);
  236. if (tagEnd === -1) return null;
  237. const tagDecl = html.substring(start, tagEnd + 1);
  238. const attrPattern = attrName + '="';
  239. const attrStart = tagDecl.indexOf(attrPattern);
  240. if (attrStart !== -1) {
  241. const attrValueStart = attrStart + attrPattern.length;
  242. const attrValueEnd = tagDecl.indexOf('"', attrValueStart);
  243. if (attrValueEnd !== -1) {
  244. const foundValue = tagDecl.substring(attrValueStart, attrValueEnd);
  245. if (foundValue === attrValue || foundValue.includes(attrValue)) {
  246. let depth = 1;
  247. let searchPos = tagEnd + 1;
  248. while (depth > 0 && searchPos < html.length) {
  249. const nextOpen = html.indexOf(openTag, searchPos);
  250. const nextClose = html.indexOf(closeTag, searchPos);
  251. if (nextClose === -1) return null;
  252. if (nextOpen !== -1 && nextOpen < nextClose) {
  253. depth++;
  254. searchPos = html.indexOf('>', nextOpen) + 1;
  255. } else {
  256. depth--;
  257. if (depth === 0) {
  258. return html.substring(tagEnd + 1, nextClose);
  259. }
  260. searchPos = nextClose + closeTag.length;
  261. }
  262. }
  263. }
  264. }
  265. }
  266. pos = tagEnd + 1;
  267. }
  268. return null;
  269. }
  270. function parseSemesterValue(semesterValue) {
  271. const parts = semesterValue.split('-');
  272. if (parts.length !== 3) return null;
  273. const startYear = parts[0];
  274. const endYear = parts[1];
  275. const semesterNum = parts[2];
  276. if (startYear.length !== 4 || endYear.length !== 4 || semesterNum.length !== 1) {
  277. return null;
  278. }
  279. return {
  280. startYear: parseInt(startYear, 10),
  281. endYear: parseInt(endYear, 10),
  282. semesterNum: semesterNum
  283. };
  284. }
  285. function parseHtmlTable(htmlContent) {
  286. const courses = [];
  287. const tableContent = findTagWithAttr(htmlContent, 'table', 'id', 'kbtable');
  288. if (!tableContent) {
  289. console.error('未找到课程表格');
  290. return courses;
  291. }
  292. const rows = findAllTags(tableContent, 'tr');
  293. let sectionIndex = 0;
  294. for (const row of rows) {
  295. if (row.includes('星期一') || row.includes('备注')) {
  296. continue;
  297. }
  298. const thContent = findTagContent(row, 'th', 0);
  299. if (thContent) {
  300. const thText = thContent.content;
  301. const sectionNames = ['一', '二', '三', '四', '五', '六'];
  302. for (let i = 0; i < sectionNames.length; i++) {
  303. if (thText.includes('第' + sectionNames[i] + '大节')) {
  304. sectionIndex = i + 1;
  305. break;
  306. }
  307. }
  308. }
  309. if (sectionIndex === 0) continue;
  310. const cells = findAllTags(row, 'td');
  311. let dayIndex = 1;
  312. for (const cell of cells) {
  313. let kbcontentDivs = [];
  314. let divPos = 0;
  315. while (true) {
  316. const divStart = cell.indexOf('<div', divPos);
  317. if (divStart === -1) break;
  318. const divDeclEnd = cell.indexOf('>', divStart);
  319. if (divDeclEnd === -1) break;
  320. const divDecl = cell.substring(divStart, divDeclEnd + 1);
  321. if (divDecl.includes('class="kbcontent"') && !divDecl.includes('class="kbcontent1"')) {
  322. const divContent = findTagContent(cell.substring(divStart), 'div', 0);
  323. if (divContent) {
  324. kbcontentDivs.push(divContent.content);
  325. }
  326. }
  327. divPos = divDeclEnd + 1;
  328. }
  329. for (const divContent of kbcontentDivs) {
  330. const parsedCourses = parseCourseFromDiv(divContent, dayIndex, sectionIndex);
  331. courses.push(...parsedCourses);
  332. }
  333. dayIndex++;
  334. }
  335. }
  336. return courses;
  337. }
  338. function mergeSameCourses(courses) {
  339. const courseMap = new Map();
  340. for (const course of courses) {
  341. const key = `${course.name}-${course.teacher}-${course.position}-${course.day}-${course.startSection}-${course.endSection}`;
  342. if (courseMap.has(key)) {
  343. const existing = courseMap.get(key);
  344. for (const week of course.weeks) {
  345. if (!existing.weeks.includes(week)) {
  346. existing.weeks.push(week);
  347. }
  348. }
  349. } else {
  350. courseMap.set(key, { ...course, weeks: [...course.weeks] });
  351. }
  352. }
  353. return Array.from(courseMap.values()).map(c => ({
  354. ...c,
  355. weeks: c.weeks.sort((a, b) => a - b)
  356. }));
  357. }
  358. async function parseAndImportCourses() {
  359. const tableElement = document.querySelector('table#kbtable');
  360. if (!tableElement) {
  361. console.error('未找到课程表格元素 #kbtable');
  362. AndroidBridge.showToast('未找到课程表格,请确保在正确的页面!');
  363. return false;
  364. }
  365. const htmlContent = tableElement.outerHTML;
  366. console.log('找到课程表格,开始解析...');
  367. let courses = parseHtmlTable(htmlContent);
  368. console.log(`解析到 ${courses.length} 条课程记录`);
  369. courses = mergeSameCourses(courses);
  370. console.log(`合并后 ${courses.length} 条课程记录`);
  371. console.log('解析结果:', JSON.stringify(courses, null, 2));
  372. try {
  373. const result = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  374. if (result === true) {
  375. console.log('课程导入成功!');
  376. AndroidBridge.showToast(`成功导入 ${courses.length} 门课程!`);
  377. return true;
  378. } else {
  379. console.log('课程导入失败,结果:' + result);
  380. AndroidBridge.showToast('课程导入失败,请查看日志。');
  381. return false;
  382. }
  383. } catch (error) {
  384. console.error('导入课程时发生错误:', error);
  385. AndroidBridge.showToast('导入课程失败: ' + error.message);
  386. return false;
  387. }
  388. }
  389. async function importPresetTimeSlots() {
  390. console.log("正在准备预设时间段数据...");
  391. const presetTimeSlots = [
  392. { "number": 1, "startTime": "08:00", "endTime": "08:40" },
  393. { "number": 2, "startTime": "08:45", "endTime": "09:25" },
  394. { "number": 3, "startTime": "09:40", "endTime": "10:20" },
  395. { "number": 4, "startTime": "10:25", "endTime": "11:05" },
  396. { "number": 5, "startTime": "11:10", "endTime": "11:50" },
  397. { "number": 6, "startTime": "13:30", "endTime": "14:10" },
  398. { "number": 7, "startTime": "14:15", "endTime": "14:55" },
  399. { "number": 8, "startTime": "15:10", "endTime": "15:50" },
  400. { "number": 9, "startTime": "15:55", "endTime": "16:35" },
  401. { "number": 10, "startTime": "16:40", "endTime": "17:20" },
  402. { "number": 11, "startTime": "18:30", "endTime": "19:10" },
  403. { "number": 12, "startTime": "19:15", "endTime": "19:55" },
  404. { "number": 13, "startTime": "20:05", "endTime": "20:45" },
  405. { "number": 14, "startTime": "20:50", "endTime": "21:30" }
  406. ];
  407. try {
  408. console.log("正在尝试导入预设时间段...");
  409. const result = await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
  410. if (result === true) {
  411. console.log("预设时间段导入成功!");
  412. window.AndroidBridge.showToast("时间段导入成功!");
  413. return true;
  414. } else {
  415. console.log("预设时间段导入未成功,结果:" + result);
  416. window.AndroidBridge.showToast("时间段导入失败,请查看日志。");
  417. return false;
  418. }
  419. } catch (error) {
  420. console.error("导入时间段时发生错误:", error);
  421. window.AndroidBridge.showToast("导入时间段失败: " + error.message);
  422. return false;
  423. }
  424. }
  425. function parseSemesterInfo() {
  426. const semesterSelect = document.querySelector('select#xnxq01id');
  427. if (!semesterSelect) {
  428. console.warn('未找到学期选择框');
  429. return { semester: '2025-2026-2', totalWeeks: 19 };
  430. }
  431. const selectedOption = semesterSelect.querySelector('option[selected]');
  432. const semesterValue = selectedOption ? selectedOption.value : semesterSelect.value;
  433. console.log('当前学期:', semesterValue);
  434. const parsed = parseSemesterValue(semesterValue);
  435. if (!parsed) {
  436. return { semester: semesterValue, totalWeeks: 19 };
  437. }
  438. const { startYear, endYear, semesterNum } = parsed;
  439. let startDate;
  440. function getSecondWeekMonday(year, month) {
  441. let firstDay = new Date(year, month, 1);
  442. let dayOfWeek = firstDay.getDay();
  443. if (dayOfWeek === 0) dayOfWeek = 7;
  444. let firstMonday = new Date(year, month, 1);
  445. if (dayOfWeek === 1) {
  446. // 1号就是周一,第一周周一就是1号
  447. } else {
  448. // 1号不是周一,第一周周一是下周一
  449. firstMonday.setDate(1 + (8 - dayOfWeek));
  450. }
  451. let secondMonday = new Date(firstMonday);
  452. secondMonday.setDate(firstMonday.getDate() + 7);
  453. return secondMonday;
  454. }
  455. if (semesterNum === '1') {
  456. startDate = getSecondWeekMonday(startYear, 8);
  457. } else {
  458. startDate = getSecondWeekMonday(endYear, 2);
  459. }
  460. const year = startDate.getFullYear();
  461. const month = String(startDate.getMonth() + 1).padStart(2, '0');
  462. const day = String(startDate.getDate()).padStart(2, '0');
  463. const formattedDate = `${year}-${month}-${day}`;
  464. return {
  465. semester: semesterValue,
  466. startDate: formattedDate,
  467. totalWeeks: 19
  468. };
  469. }
  470. async function saveCourseConfig() {
  471. console.log("正在准备配置数据...");
  472. const semesterInfo = parseSemesterInfo();
  473. console.log('学期信息:', semesterInfo);
  474. const courseConfigData = {
  475. "semesterStartDate": semesterInfo.startDate || "2026-02-24",
  476. "semesterTotalWeeks": semesterInfo.totalWeeks,
  477. "defaultClassDuration": 40,
  478. "defaultBreakDuration": 5,
  479. "firstDayOfWeek": 1
  480. };
  481. try {
  482. console.log("正在尝试导入课表配置...");
  483. console.log("配置数据:", JSON.stringify(courseConfigData, null, 2));
  484. const configJsonString = JSON.stringify(courseConfigData);
  485. const result = await window.AndroidBridgePromise.saveCourseConfig(configJsonString);
  486. if (result === true) {
  487. console.log("课表配置导入成功!");
  488. AndroidBridge.showToast(`配置导入成功!学期: ${semesterInfo.semester}, 开学: ${courseConfigData.semesterStartDate}`);
  489. return true;
  490. } else {
  491. console.log("课表配置导入未成功,结果:" + result);
  492. AndroidBridge.showToast("配置导入失败,请查看日志。");
  493. return false;
  494. }
  495. } catch (error) {
  496. console.error("导入配置时发生错误:", error);
  497. AndroidBridge.showToast("导入配置失败: " + error.message);
  498. return false;
  499. }
  500. }
  501. async function runImportFlow() {
  502. const alertConfirmed = await window.AndroidBridgePromise.showAlert(
  503. "佛山大学教务系统课表导入",
  504. "【重要】本系统需要使用校园网访问,请确保已连接校园网后再操作。\n\n导入步骤:\n1. 登录教务系统\n2. 导航到【培养管理】→【学期理论课表】\n3. 确认课表已加载显示\n4. 点击确定开始导入",
  505. "好的,开始导入"
  506. );
  507. if (!alertConfirmed) {
  508. AndroidBridge.showToast("用户取消了导入。");
  509. return;
  510. }
  511. AndroidBridge.showToast("开始解析课程表...");
  512. console.log("=== 开始课程表解析和导入流程 ===");
  513. const importResult = await parseAndImportCourses();
  514. if (!importResult) {
  515. console.log("课程导入失败或用户取消。");
  516. return;
  517. }
  518. console.log("课程导入完成。");
  519. AndroidBridge.showToast("课程导入完成!");
  520. await importPresetTimeSlots();
  521. await saveCourseConfig();
  522. console.log("=== 所有任务完成 ===");
  523. AndroidBridge.notifyTaskCompletion();
  524. }
  525. if (typeof AndroidBridge !== 'undefined' && AndroidBridge) {
  526. runImportFlow();
  527. }