dlut_01.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. // 大连理工大学本科教务系统适配脚本
  2. // 适配人: dffzcqs78694
  3. //
  4. // 接口链路(与页面实际请求一致):
  5. // 1. GET /student/for-std/course-table → 解析学期下拉框 + 提取 stdPersonId
  6. // 2. GET /student/ws/schedule-table/get-data?bizTypeId=2&semesterId={id} → lessonIds / timeTableLayoutId / weekIndices
  7. // 3. POST /student/ws/schedule-table/datum → lessonList(课程) + scheduleList(排课明细)
  8. // 4. POST /student/ws/schedule-table/timetable-layout → 节次时间定义
  9. // 5. GET /student/ws/semester/get/{semesterId} → 学期起止日期(用于推算开学日期, 失败则回退)
  10. // ---- 节次时间表兜底数据(接口失败时使用, 与大工 timetable-layout 一致) ----
  11. const FALLBACK_TIME_SLOTS = [
  12. { "number": 1, "startTime": "08:00", "endTime": "08:45" },
  13. { "number": 2, "startTime": "08:50", "endTime": "09:35" },
  14. { "number": 3, "startTime": "10:05", "endTime": "10:50" },
  15. { "number": 4, "startTime": "10:55", "endTime": "11:40" },
  16. { "number": 5, "startTime": "13:30", "endTime": "14:15" },
  17. { "number": 6, "startTime": "14:20", "endTime": "15:05" },
  18. { "number": 7, "startTime": "15:35", "endTime": "16:20" },
  19. { "number": 8, "startTime": "16:25", "endTime": "17:10" },
  20. { "number": 9, "startTime": "18:00", "endTime": "18:45" },
  21. { "number": 10, "startTime": "18:50", "endTime": "19:35" },
  22. { "number": 11, "startTime": "19:40", "endTime": "20:25" },
  23. { "number": 12, "startTime": "20:30", "endTime": "21:15" }
  24. ];
  25. // 时间数字格式化: 1800 -> "18:00"
  26. function formatTimeNum(timeNum) {
  27. if (timeNum === null || timeNum === undefined) return "";
  28. const str = String(timeNum).padStart(4, '0');
  29. return `${str.slice(0, 2)}:${str.slice(2, 4)}`;
  30. }
  31. // 判断时间数字是否在 [start, end] 区间内(用于节次映射)
  32. function timeInRange(timeNum, startNum, endNum) {
  33. return timeNum >= startNum && timeNum <= endNum;
  34. }
  35. // 构建节次列表(数字格式): 优先用接口返回的 courseUnitList, 失败时用内置兜底作息表
  36. function buildCourseUnits(layout) {
  37. if (layout && Array.isArray(layout.courseUnitList) && layout.courseUnitList.length > 0) {
  38. return layout.courseUnitList;
  39. }
  40. // 兜底: 内置节次表(与大工 timetable-layout 实际数据一致)
  41. return FALLBACK_TIME_SLOTS.map(slot => ({
  42. indexNo: slot.number,
  43. startTime: parseInt(slot.startTime.replace(':', ''), 10),
  44. endTime: parseInt(slot.endTime.replace(':', ''), 10)
  45. }));
  46. }
  47. // ---- 纯函数: 根据课程原始排课明细解析为拾光课程格式 ----
  48. // printData: datum 响应中的 result 对象
  49. // layout: timetable-layout 响应中的 result 对象
  50. // 返回课程数组
  51. function parseCourses(printData, layout) {
  52. const lessonList = printData.lessonList || [];
  53. const scheduleList = printData.scheduleList || [];
  54. const lessonById = {};
  55. lessonList.forEach(lesson => { lessonById[lesson.id] = lesson; });
  56. // 从 layout 中构建节次列表 (用于 startTime/endTime -> 节次号)
  57. const units = buildCourseUnits(layout);
  58. // 按 (lessonId, weekday, startTime, endTime) 分组, 收集周次
  59. const groups = {};
  60. scheduleList.forEach(schedule => {
  61. const key = `${schedule.lessonId}|${schedule.weekday}|${schedule.startTime}|${schedule.endTime}`;
  62. if (!groups[key]) {
  63. groups[key] = { schedule: schedule, weeks: [] };
  64. }
  65. if (schedule.weekIndex !== null && schedule.weekIndex !== undefined) {
  66. groups[key].weeks.push(schedule.weekIndex);
  67. }
  68. });
  69. const courses = [];
  70. Object.keys(groups).forEach(key => {
  71. const group = groups[key];
  72. const schedule = group.schedule;
  73. const lesson = lessonById[schedule.lessonId] || {};
  74. // 星期几
  75. const day = schedule.weekday;
  76. // 节次映射: 找到所有 startTime>=调度开始 && endTime<=调度结束 的节次
  77. let startSection = null;
  78. let endSection = null;
  79. if (units.length > 0 && schedule.startTime !== null && schedule.endTime !== null) {
  80. const matched = units.filter(unit =>
  81. timeInRange(unit.startTime, schedule.startTime, schedule.endTime) &&
  82. timeInRange(unit.endTime, schedule.startTime, schedule.endTime)
  83. );
  84. if (matched.length > 0) {
  85. startSection = matched[0].indexNo;
  86. endSection = matched[matched.length - 1].indexNo;
  87. }
  88. }
  89. // 教师: 优先取排课明细中的教师, 否则取课程下的教师列表
  90. let teacher = schedule.personName || "";
  91. if (!teacher && lesson.teacherAssignmentList) {
  92. teacher = lesson.teacherAssignmentList.map(t => t.name).filter(Boolean).join(" ");
  93. }
  94. // 教室
  95. let position = "";
  96. if (schedule.room && schedule.room.nameZh) {
  97. position = schedule.room.nameZh;
  98. } else if (schedule.customPlace) {
  99. position = schedule.customPlace;
  100. }
  101. // 周次去重排序
  102. const weeks = Array.from(new Set(group.weeks)).sort((a, b) => a - b);
  103. const course = {
  104. name: lesson.courseName || "",
  105. teacher: teacher,
  106. position: position,
  107. day: day,
  108. startSection: startSection,
  109. endSection: endSection,
  110. weeks: weeks
  111. };
  112. courses.push(course);
  113. });
  114. return courses;
  115. }
  116. // ---- 纯函数: 生成预设时间段 ----
  117. // layout: timetable-layout 响应中的 result 对象
  118. function buildTimeSlots(layout) {
  119. if (layout && Array.isArray(layout.courseUnitList) && layout.courseUnitList.length > 0) {
  120. return layout.courseUnitList.map(unit => ({
  121. number: unit.indexNo,
  122. startTime: formatTimeNum(unit.startTime),
  123. endTime: formatTimeNum(unit.endTime)
  124. }));
  125. }
  126. return FALLBACK_TIME_SLOTS;
  127. }
  128. // ---- 纯函数: 推算学期开始日期(第1周周一) ----
  129. // 排课明细的 date + weekIndex 已知, 用最小的 date 算出其所在周周一,
  130. // 再按 weekIndex 差值回退到第 1 周周一。
  131. // 例: 第3周周一=2026-09-14, 则第1周周一 = 09-14 - 2周 = 2026-08-31
  132. function calcSemesterStartDate(printData) {
  133. let minDate = null;
  134. let minWeekIndex = null;
  135. (printData.scheduleList || []).forEach(s => {
  136. if (s.date && s.date.length >= 10 && s.weekIndex !== null && s.weekIndex !== undefined) {
  137. if (!minDate || s.date < minDate) {
  138. minDate = s.date;
  139. minWeekIndex = s.weekIndex;
  140. }
  141. }
  142. });
  143. if (!minDate || !minWeekIndex) return null;
  144. // 该日期所在周的周一
  145. const dt = new Date(minDate);
  146. const day = dt.getDay(); // 0=周日
  147. const diff = (day === 0 ? -6 : 1 - day);
  148. dt.setDate(dt.getDate() + diff);
  149. // 回退到第1周
  150. dt.setDate(dt.getDate() - (minWeekIndex - 1) * 7);
  151. return dt.toISOString().split('T')[0];
  152. }
  153. // ---- 桥接封装 ----
  154. async function bridgeSaveCourses(courses) {
  155. const result = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  156. if (result === true) {
  157. window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
  158. return true;
  159. }
  160. window.shiguangBridge.showToast("课程导入未成功,请查看日志。");
  161. return false;
  162. }
  163. async function bridgeSaveTimeSlots(timeSlots) {
  164. const result = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  165. if (result === true) {
  166. window.shiguangBridge.showToast(`成功导入 ${timeSlots.length} 个时间段!`);
  167. return true;
  168. }
  169. window.shiguangBridge.showToast("时间段导入失败,请查看日志。");
  170. return false;
  171. }
  172. async function bridgeSaveConfig(config) {
  173. const result = await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  174. if (result === true) {
  175. window.shiguangBridge.showToast("课表配置导入成功!");
  176. return true;
  177. }
  178. window.shiguangBridge.showToast("课表配置导入失败,请查看日志。");
  179. return false;
  180. }
  181. // ---- 1. 公告 ----
  182. async function promptUserToStart() {
  183. try {
  184. const confirmed = await window.shiguangBridgePromise.showAlert(
  185. "导入提醒",
  186. "请确保您已登录教务系统,导入前请确认当前学期正确。",
  187. "好的,开始导入"
  188. );
  189. return confirmed === true;
  190. } catch (e) {
  191. console.error("公告弹窗出错:", e);
  192. window.shiguangBridge.showToast("显示公告出错: " + e.message);
  193. return false;
  194. }
  195. }
  196. // ---- 2. 获取学期列表 ----
  197. async function getSemesterOptions() {
  198. try {
  199. const response = await fetch(`/student/for-std/course-table`);
  200. const htmlString = await response.text();
  201. const parser = new DOMParser();
  202. const dom = parser.parseFromString(htmlString, 'text/html');
  203. const selectElement = dom.getElementById('semesters') || dom.getElementById('allSemesters');
  204. if (!selectElement) throw new Error("页面中未找到学期选择框");
  205. const options = Array.from(selectElement.options);
  206. const validOptions = options.filter(opt => opt.value && opt.value !== "all");
  207. if (validOptions.length === 0) throw new Error("未解析到有效的学期列表");
  208. const semesterTexts = validOptions.map(opt => opt.text.trim());
  209. const semesterValues = validOptions.map(opt => opt.value);
  210. return { semesterTexts, semesterValues };
  211. } catch (e) {
  212. console.error("获取学期列表失败:", e);
  213. window.shiguangBridge.showToast("获取学期列表失败: " + e.message);
  214. return null;
  215. }
  216. }
  217. // ---- 3. 获取 stdPersonId (从页面 JS 变量中提取) ----
  218. async function fetchStdPersonId() {
  219. try {
  220. const response = await fetch(`/student/for-std/course-table`);
  221. const htmlString = await response.text();
  222. // 常见模式: stdPersonId = 3284653 或 stdPersonId: 3284653 或 stdPersonId":"3284653"
  223. const patterns = [
  224. /stdPersonId\s*[:=]\s*["']?(\d+)["']?/,
  225. /std_person_id\s*[:=]\s*["']?(\d+)["']?/,
  226. /stdPersonId["']?\s*[:=]\s*["']?(\d+)["']?/
  227. ];
  228. for (const pattern of patterns) {
  229. const match = htmlString.match(pattern);
  230. if (match) return match[1];
  231. }
  232. return null;
  233. } catch (e) {
  234. console.error("提取 stdPersonId 失败:", e);
  235. return null;
  236. }
  237. }
  238. // ---- 4. 获取课程基础数据 (get-data) ----
  239. async function fetchGetData(semesterId) {
  240. const url = `/student/for-std/course-table/get-data?bizTypeId=2&semesterId=${semesterId}`;
  241. const response = await fetch(url);
  242. if (!response.ok) throw new Error(`get-data 请求失败, 状态码: ${response.status}`);
  243. return await response.json();
  244. }
  245. // ---- 5. 获取排课明细 (datum) ----
  246. async function fetchDatum(lessonIds, stdPersonId) {
  247. const body = {
  248. lessonIds: lessonIds,
  249. studentId: null,
  250. stdPersonId: stdPersonId,
  251. weekIndex: null
  252. };
  253. const response = await fetch(`/student/ws/schedule-table/datum`, {
  254. method: "POST",
  255. headers: { "Content-Type": "application/json" },
  256. body: JSON.stringify(body)
  257. });
  258. if (!response.ok) throw new Error(`datum 请求失败, 状态码: ${response.status}`);
  259. return await response.json();
  260. }
  261. // ---- 6. 获取节次时间表 (timetable-layout) ----
  262. async function fetchTimeTableLayout(timeTableLayoutId) {
  263. try {
  264. const response = await fetch(`/student/ws/schedule-table/timetable-layout`, {
  265. method: "POST",
  266. headers: { "Content-Type": "application/json" },
  267. body: JSON.stringify({ id: timeTableLayoutId })
  268. });
  269. if (!response.ok) throw new Error(`timetable-layout 请求失败, 状态码: ${response.status}`);
  270. const json = await response.json();
  271. if (!json || !json.result || !json.result.courseUnitList) throw new Error("timetable-layout 响应结构异常");
  272. return json.result;
  273. } catch (e) {
  274. console.warn("获取节次时间表失败, 使用内置兜底数据:", e);
  275. return { courseUnitList: null };
  276. }
  277. }
  278. // ---- 7. 获取学期日期信息 (用于推算开学日期, 可选) ----
  279. async function fetchSemesterInfo(semesterId) {
  280. try {
  281. const response = await fetch(`/student/ws/semester/get/${semesterId}`);
  282. if (!response.ok) return null;
  283. return await response.json();
  284. } catch (e) {
  285. return null;
  286. }
  287. }
  288. // ---- 主流程 ----
  289. async function runImportFlow() {
  290. window.shiguangBridge.showToast("课程导入流程即将开始...");
  291. // 1. 公告确认
  292. const alertConfirmed = await promptUserToStart();
  293. if (!alertConfirmed) {
  294. return;
  295. }
  296. // 2. 选择学期
  297. const semesterOptions = await getSemesterOptions();
  298. if (!semesterOptions) return;
  299. const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
  300. "选择学期",
  301. JSON.stringify(semesterOptions.semesterTexts),
  302. 0
  303. );
  304. if (selectedIndex === null || selectedIndex < 0) {
  305. window.shiguangBridge.showToast("导入已取消。");
  306. return;
  307. }
  308. const semesterId = semesterOptions.semesterValues[selectedIndex];
  309. window.shiguangBridge.showToast(`已选择: ${semesterOptions.semesterTexts[selectedIndex]}`);
  310. try {
  311. // 3. 获取课程基础数据
  312. window.shiguangBridge.showToast("正在获取课程数据...");
  313. const getData = await fetchGetData(semesterId);
  314. const lessonIds = getData.lessonIds || [];
  315. const timeTableLayoutId = getData.timeTableLayoutId;
  316. // 4. 获取 stdPersonId
  317. let stdPersonId = await fetchStdPersonId();
  318. // 5. 获取排课明细
  319. window.shiguangBridge.showToast("正在获取排课明细...");
  320. const datum = await fetchDatum(lessonIds, stdPersonId);
  321. const result = datum.result;
  322. if (!result || !result.lessonList || !result.scheduleList) {
  323. throw new Error("datum 响应结构异常, 请检查是否已登录");
  324. }
  325. // 6. 获取节次时间表
  326. const layout = await fetchTimeTableLayout(timeTableLayoutId);
  327. // 7. 解析课程
  328. const courses = parseCourses(result, layout);
  329. if (courses.length === 0) {
  330. window.shiguangBridge.showToast("未解析到课程数据。");
  331. return;
  332. }
  333. // 8. 生成时间段
  334. const timeSlots = buildTimeSlots(layout);
  335. // 9. 生成课表配置
  336. const config = {
  337. semesterStartDate: null,
  338. semesterTotalWeeks: 20,
  339. defaultClassDuration: 45,
  340. defaultBreakDuration: 5,
  341. firstDayOfWeek: 1
  342. };
  343. // 优先尝试从学期接口获取开学日期与周数
  344. const semesterInfo = await fetchSemesterInfo(semesterId);
  345. if (semesterInfo && semesterInfo.startDate) {
  346. config.semesterStartDate = semesterInfo.startDate;
  347. if (semesterInfo.endDate) {
  348. const start = new Date(semesterInfo.startDate);
  349. const end = new Date(semesterInfo.endDate);
  350. const diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
  351. const weeks = Math.ceil(diffDays / 7);
  352. if (weeks > 0) config.semesterTotalWeeks = weeks;
  353. }
  354. } else {
  355. // 回退: 从排课明细推算开学日期
  356. const startDate = calcSemesterStartDate(result);
  357. if (startDate) config.semesterStartDate = startDate;
  358. // 回退: 从课程实际最大周次推算总周数(避免出现大量空白周)
  359. if (courses.length > 0) {
  360. let maxWeek = 0;
  361. courses.forEach(c => c.weeks.forEach(w => { if (w > maxWeek) maxWeek = w; }));
  362. if (maxWeek > 0) config.semesterTotalWeeks = maxWeek;
  363. }
  364. }
  365. // 10. 依次保存
  366. const timeSlotOk = await bridgeSaveTimeSlots(timeSlots);
  367. if (!timeSlotOk) return;
  368. const configOk = await bridgeSaveConfig(config);
  369. if (!configOk) return;
  370. const coursesOk = await bridgeSaveCourses(courses);
  371. if (!coursesOk) return;
  372. // 11. 完成
  373. window.shiguangBridge.showToast("课程导入完成!");
  374. window.shiguangBridge.notifyTaskCompletion();
  375. } catch (e) {
  376. console.error("导入流程发生错误:", e);
  377. window.shiguangBridge.showToast("导入失败: " + e.message);
  378. }
  379. }
  380. // 启动
  381. runImportFlow();