school.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. // school.js - 教务系统适配
  2. //
  3. // 适配说明:
  4. // 此脚本通过调用教务系统已有的 AJAX 接口获取课程数据,
  5. // 解析 scheduleText 文本字段提取排课信息,
  6. // 然后转换为拾光 App 所需的格式并导出。
  7. //
  8. // 使用方式:
  9. // 1. 登录教务系统,进入课表页面
  10. // 2. 点击扩展图标 → "开始测试"
  11. // 3. 等待数据采集完成,弹出下载
  12. const CONTEXT_PATH = window.CONTEXT_PATH || '/student';
  13. const BIZ_TYPE_ID = window.bizTypeId || 2;
  14. // ============================================================
  15. // 工具函数
  16. // ============================================================
  17. /**
  18. * 从页面全局变量中获取当前学期信息
  19. */
  20. function getCurrentSemesterInfo() {
  21. if (window.currentSemester) {
  22. const cs = window.currentSemester;
  23. const startDate = cs.startDate
  24. ? `${cs.startDate.year}-${String(cs.startDate.monthOfYear).padStart(2, '0')}-${String(cs.startDate.dayOfMonth).padStart(2, '0')}`
  25. : null;
  26. const endDate = cs.endDate
  27. ? `${cs.endDate.year}-${String(cs.endDate.monthOfYear).padStart(2, '0')}-${String(cs.endDate.dayOfMonth).padStart(2, '0')}`
  28. : null;
  29. return {
  30. id: cs.id,
  31. name: cs.nameZh || cs.name,
  32. startDate: startDate,
  33. endDate: endDate,
  34. weekStartOnSunday: cs.weekStartOnSunday || false,
  35. schoolYear: cs.schoolYear,
  36. };
  37. }
  38. if (window.semesters && window.semesters.length > 0) {
  39. const semesterId = document.getElementById('allSemesters')?.value;
  40. const sem = semesterId
  41. ? window.semesters.find(s => s.id == semesterId)
  42. : window.semesters[0];
  43. if (sem) {
  44. return {
  45. id: sem.id,
  46. name: sem.nameZh || sem.name,
  47. startDate: sem.startDate,
  48. endDate: sem.endDate,
  49. weekStartOnSunday: sem.weekStartOnSunday || false,
  50. schoolYear: sem.schoolYear,
  51. };
  52. }
  53. }
  54. return null;
  55. }
  56. /**
  57. * 计算学期总周数
  58. */
  59. function calculateTotalWeeks(startDateStr, endDateStr) {
  60. if (!startDateStr || !endDateStr) return 20;
  61. const start = new Date(startDateStr);
  62. const end = new Date(endDateStr);
  63. const diffDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
  64. return Math.ceil(diffDays / 7);
  65. }
  66. // 星期映射
  67. const DAY_MAP = {
  68. '星期一': 1, '星期二': 2, '星期三': 3, '星期四': 4,
  69. '星期五': 5, '星期六': 6, '星期日': 7,
  70. };
  71. // ============================================================
  72. // 核心:解析 scheduleText 文本
  73. // ============================================================
  74. /**
  75. * 解析周次字符串,如 "2~15" → [2,3,...,15], "1" → [1]
  76. */
  77. function parseWeekRange(weekStr) {
  78. const weeks = [];
  79. if (weekStr.includes('~')) {
  80. const parts = weekStr.split('~');
  81. const start = parseInt(parts[0], 10);
  82. const end = parseInt(parts[1], 10);
  83. for (let i = start; i <= end; i++) {
  84. weeks.push(i);
  85. }
  86. } else {
  87. weeks.push(parseInt(weekStr, 10));
  88. }
  89. return weeks;
  90. }
  91. /**
  92. * 解析单条排课文本行
  93. *
  94. * 格式示例 (有教室):
  95. * "1周 星期三 8~9节 成龙 1-A407"
  96. * "2~15周 星期三 1~2节 成龙 1-B214"
  97. * 格式示例 (无教室,如网课/讲座):
  98. * "11~12周 星期三 3~4节"
  99. */
  100. function parseScheduleLine(line, lesson) {
  101. if (!line || !line.trim()) return [];
  102. // 正则匹配:
  103. // (\d+(?:~\d+)?)周 - 周次 (如 "1" 或 "2~15")
  104. // (星期[一二三四五六日]) - 星期几
  105. // (\d+)~(\d+)节 - 节次范围
  106. // (?:\s+(\S+))?(?:\s+(\S+))? - 可选的校区和教室 (有些课没有)
  107. const pattern = /(\d+(?:~\d+)?)周\s+(星期[一二三四五六日])\s+(\d+)~(\d+)节(?:\s+(\S+))?(?:\s+(\S+))?/;
  108. const match = line.match(pattern);
  109. if (!match) {
  110. console.warn('[教务适配] 无法解析排课行:', line);
  111. return [];
  112. }
  113. const weekStr = match[1];
  114. const dayText = match[2];
  115. const startSection = parseInt(match[3], 10);
  116. const endSection = parseInt(match[4], 10);
  117. const campus = match[5] || ''; // 可选
  118. const classroom = match[6] || ''; // 可选
  119. const weeks = parseWeekRange(weekStr);
  120. const day = DAY_MAP[dayText] || 1;
  121. // 课程名: lesson.course.nameZh
  122. const courseName = (lesson.course && lesson.course.nameZh) || lesson.nameZh || '未知课程';
  123. // 教师: lesson.teacherAssignmentString
  124. const teacher = lesson.teacherAssignmentString || '';
  125. // 教室位置
  126. const position = [campus, classroom].filter(Boolean).join(' ');
  127. return [{
  128. name: courseName,
  129. teacher: teacher,
  130. position: position,
  131. day: day,
  132. startSection: startSection,
  133. endSection: endSection,
  134. weeks: weeks,
  135. isCustomTime: false,
  136. }];
  137. }
  138. /**
  139. * 从 lesson 的 scheduleText 中解析所有排课条目
  140. */
  141. function parseLessonScheduleText(lesson) {
  142. const courses = [];
  143. const scheduleText = lesson.scheduleText;
  144. if (!scheduleText) return courses;
  145. let rawText = '';
  146. if (scheduleText.dateTimePlaceText && scheduleText.dateTimePlaceText.text) {
  147. rawText = scheduleText.dateTimePlaceText.text;
  148. } else if (scheduleText.dateTimePlacePersonText && scheduleText.dateTimePlacePersonText.text) {
  149. rawText = scheduleText.dateTimePlacePersonText.text;
  150. } else if (scheduleText.dateTimeText && scheduleText.dateTimeText.text) {
  151. rawText = scheduleText.dateTimeText.text;
  152. }
  153. if (!rawText) return courses;
  154. // 按 "; \n" 或 ";\n" 拆分各行
  155. const lines = rawText.split(/;\s*\n\s*/).filter(line => line.trim());
  156. for (const line of lines) {
  157. const parsed = parseScheduleLine(line.trim(), lesson);
  158. courses.push(...parsed);
  159. }
  160. return courses;
  161. }
  162. // ============================================================
  163. // 合并相同课程
  164. // ============================================================
  165. /**
  166. * 合并相同课程的周次
  167. */
  168. function mergeCourseWeeks(courses) {
  169. const map = new Map();
  170. for (const course of courses) {
  171. const key = JSON.stringify({
  172. name: course.name,
  173. teacher: course.teacher,
  174. position: course.position,
  175. day: course.day,
  176. startSection: course.startSection,
  177. endSection: course.endSection,
  178. });
  179. if (map.has(key)) {
  180. const existing = map.get(key);
  181. const mergedWeeks = new Set([...existing.weeks, ...course.weeks]);
  182. existing.weeks = Array.from(mergedWeeks).sort((a, b) => a - b);
  183. } else {
  184. map.set(key, { ...course });
  185. }
  186. }
  187. return Array.from(map.values());
  188. }
  189. // ============================================================
  190. // 数据获取
  191. // ============================================================
  192. /**
  193. * 通过 AJAX 获取课程数据
  194. */
  195. function fetchCourseData(semesterId) {
  196. return new Promise((resolve, reject) => {
  197. const url = CONTEXT_PATH + '/for-std/course-table/get-data';
  198. const data = {
  199. bizTypeId: BIZ_TYPE_ID,
  200. semesterId: semesterId,
  201. dataId: window.studentIds ? window.studentIds[0] : window.personId,
  202. };
  203. console.log('[教务适配] 正在请求课程数据...', { url, data });
  204. $.ajax({
  205. url: url,
  206. type: 'GET',
  207. data: data,
  208. success: function (res) {
  209. console.log('[教务适配] 成功获取课程数据,包含 ' + (res.lessons?.length || 0) + ' 门课程');
  210. resolve(res);
  211. },
  212. error: function (jqXHR, textStatus, errorThrown) {
  213. console.error('[教务适配] 获取课程数据失败:', textStatus, errorThrown);
  214. reject(new Error('获取课程数据失败: ' + textStatus));
  215. },
  216. });
  217. });
  218. }
  219. // ============================================================
  220. // 时间段配置 (狮子山|成龙校区课表)
  221. // ============================================================
  222. /**
  223. * 获取上课时间段配置
  224. */
  225. function getDefaultTimeSlots() {
  226. return [
  227. { number: 1, startTime: "08:10", endTime: "08:55" },
  228. { number: 2, startTime: "09:00", endTime: "09:45" },
  229. { number: 3, startTime: "10:10", endTime: "10:55" },
  230. { number: 4, startTime: "11:05", endTime: "11:45" },
  231. { number: 5, startTime: "11:50", endTime: "12:35" },
  232. { number: 6, startTime: "12:40", endTime: "13:25" },
  233. { number: 7, startTime: "13:30", endTime: "14:10" },
  234. { number: 8, startTime: "14:10", endTime: "14:55" },
  235. { number: 9, startTime: "15:00", endTime: "15:45" },
  236. { number: 10, startTime: "16:10", endTime: "16:55" },
  237. { number: 11, startTime: "17:00", endTime: "17:45" },
  238. { number: 12, startTime: "18:00", endTime: "18:45" },
  239. { number: 13, startTime: "19:10", endTime: "19:55" },
  240. { number: 14, startTime: "20:00", endTime: "20:45" },
  241. { number: 15, startTime: "20:50", endTime: "21:35" },
  242. ];
  243. }
  244. // ============================================================
  245. // 课表配置
  246. // ============================================================
  247. function getCourseConfig(semesterInfo) {
  248. const totalWeeks = semesterInfo?.startDate && semesterInfo?.endDate
  249. ? calculateTotalWeeks(semesterInfo.startDate, semesterInfo.endDate)
  250. : 20;
  251. return {
  252. semesterStartDate: semesterInfo?.startDate || null,
  253. semesterTotalWeeks: totalWeeks,
  254. defaultClassDuration: 45,
  255. defaultBreakDuration: 10,
  256. firstDayOfWeek: 1,
  257. };
  258. }
  259. // ============================================================
  260. // 主流程
  261. // ============================================================
  262. async function main() {
  263. try {
  264. console.log('========================================');
  265. console.log('[教务适配] 开始执行 SICNU 教务系统适配');
  266. console.log('========================================');
  267. // 1. 获取学期信息
  268. const semesterInfo = getCurrentSemesterInfo();
  269. if (!semesterInfo) {
  270. throw new Error('无法获取学期信息,请确认已在课表页面');
  271. }
  272. console.log('[教务适配] 当前学期:', semesterInfo.name, '(id=' + semesterInfo.id + ')');
  273. console.log('[教务适配] 学期日期:', semesterInfo.startDate, '~', semesterInfo.endDate);
  274. // 2. 获取课程数据
  275. AndroidBridge.showToast('正在采集课程数据...');
  276. const data = await fetchCourseData(semesterInfo.id);
  277. if (!data || !data.lessons || data.lessons.length === 0) {
  278. throw new Error('未获取到课程数据,请确认已登录并选择了正确的学期');
  279. }
  280. // 3. 解析每门课的 scheduleText
  281. console.log('[教务适配] 开始解析 ' + data.lessons.length + ' 门课程...');
  282. let allCourses = [];
  283. for (const lesson of data.lessons) {
  284. const courses = parseLessonScheduleText(lesson);
  285. if (courses.length > 0) {
  286. const courseName = (lesson.course && lesson.course.nameZh) || lesson.nameZh;
  287. console.log('[教务适配] ' + courseName + ': 解析出 ' + courses.length + ' 条排课记录');
  288. allCourses.push(...courses);
  289. } else {
  290. const courseName = (lesson.course && lesson.course.nameZh) || lesson.nameZh || '(未知)';
  291. console.warn('[教务适配] ' + courseName + ' (id=' + lesson.id + '): 未能解析出排课记录');
  292. // 输出 lesson 的 scheduleText 原始内容供调试
  293. if (lesson.scheduleText) {
  294. console.log(' scheduleText 内容:', JSON.stringify(lesson.scheduleText));
  295. } else {
  296. console.log(' lesson 没有 scheduleText 字段, keys:', Object.keys(lesson));
  297. }
  298. }
  299. }
  300. console.log('[教务适配] 解析得到 ' + allCourses.length + ' 条课程记录');
  301. // 4. 合并相同课程的周次
  302. allCourses = mergeCourseWeeks(allCourses);
  303. console.log('[教务适配] 合并后共 ' + allCourses.length + ' 条课程记录');
  304. if (allCourses.length === 0) {
  305. throw new Error('未能解析出任何课程数据。请查看控制台日志。');
  306. }
  307. // 输出课程样本
  308. console.log('[教务适配] 课程样本 (前5条):');
  309. allCourses.slice(0, 5).forEach((c, i) => {
  310. console.log(` ${i + 1}. ${c.name} | ${c.teacher} | ${c.position} | 周${c.day} | ${c.startSection}-${c.endSection}节 | 周次:${c.weeks.join(',')}`);
  311. });
  312. // 5. 时间段配置
  313. const timeSlots = getDefaultTimeSlots();
  314. // 6. 课表配置
  315. const config = getCourseConfig(semesterInfo);
  316. console.log('[教务适配] 准备导入:');
  317. console.log(' - 课程数:', allCourses.length);
  318. console.log(' - 时间段数:', timeSlots.length);
  319. console.log(' - 学期总周数:', config.semesterTotalWeeks);
  320. console.log(' - 开学日期:', config.semesterStartDate);
  321. // 7. 导入数据
  322. AndroidBridge.showToast(`正在导入 ${allCourses.length} 门课程...`);
  323. const courseResult = await window.AndroidBridgePromise.saveImportedCourses(
  324. JSON.stringify(allCourses)
  325. );
  326. console.log('[教务适配] 课程导入结果:', courseResult);
  327. const timeSlotResult = await window.AndroidBridgePromise.savePresetTimeSlots(
  328. JSON.stringify(timeSlots)
  329. );
  330. console.log('[教务适配] 时间段导入结果:', timeSlotResult);
  331. const configResult = await window.AndroidBridgePromise.saveCourseConfig(
  332. JSON.stringify(config)
  333. );
  334. console.log('[教务适配] 配置导入结果:', configResult);
  335. // 8. 触发下载
  336. console.log('[教务适配] ✅ 所有数据导入完成,触发下载...');
  337. AndroidBridge.showToast('课程数据采集完成!请保存下载的文件。');
  338. AndroidBridge.notifyTaskCompletion();
  339. } catch (error) {
  340. console.error('[教务适配] ❌ 执行失败:', error.message);
  341. console.error(error.stack);
  342. AndroidBridge.showToast('采集失败: ' + error.message);
  343. }
  344. }
  345. // 启动
  346. main();