hunau_01.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. // 湖南农业大学(hunau.edu.cn)拾光课程表适配脚本
  2. // 教务系统:强智 jsxsd(湖南强智科技教务系统)
  3. // 校外经 WebVPN(webvpn.hunau.edu.cn)访问,校内可直连;脚本按当前页面自动推导 /jsxsd 前缀,两种形态通用
  4. // 使用流程:在软件内置浏览器登录学校统一身份认证/WebVPN,进入教务系统任意页面后执行导入
  5. // 数据来源:GET {前缀}/jsxsd/xskb/xskb_list.do?xnxq01id=学期 → 课表页(table#timetable)
  6. // POST {前缀}/jsxsd/jxzl/jxzl_query → 教学周历(首列为周次,第二列 title 为该周星期一日期)
  7. // 维护者:salt-fishes
  8. var HUNAU_KB_PATH = '/jsxsd/xskb/xskb_list.do';
  9. var HUNAU_CALENDAR_PATH = '/jsxsd/jxzl/jxzl_query';
  10. var HUNAU_CLASS_DURATION = 45;
  11. var HUNAU_BREAK_DURATION = 10;
  12. var HUNAU_DEFAULT_TOTAL_WEEKS = 20;
  13. var HUNAU_POST_OPTIONS = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } };
  14. // 兜底作息:当课表页未提供节次时间时使用(按湖南农大 6 个大节、每节 45 分钟、课间 10 分钟推得)
  15. var HUNAU_FALLBACK_TIME_SLOTS = [
  16. { number: 1, startTime: '08:00', endTime: '08:45' },
  17. { number: 2, startTime: '08:55', endTime: '09:40' },
  18. { number: 3, startTime: '10:05', endTime: '10:50' },
  19. { number: 4, startTime: '11:00', endTime: '11:45' },
  20. { number: 5, startTime: '14:30', endTime: '15:15' },
  21. { number: 6, startTime: '15:25', endTime: '16:10' },
  22. { number: 7, startTime: '16:35', endTime: '17:20' },
  23. { number: 8, startTime: '17:30', endTime: '18:15' },
  24. { number: 9, startTime: '19:30', endTime: '20:15' },
  25. { number: 10, startTime: '20:25', endTime: '21:10' },
  26. { number: 11, startTime: '21:20', endTime: '22:05' },
  27. { number: 12, startTime: '22:15', endTime: '23:00' }
  28. ];
  29. var HUNAU_CN_NUMBER = { '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '十': 10 };
  30. // ==================== 桥接封装 ====================
  31. function toast(message) {
  32. if (window.shiguangBridge && typeof window.shiguangBridge.showToast === 'function') {
  33. window.shiguangBridge.showToast(message);
  34. } else {
  35. console.log('[HUNAU] ' + message);
  36. }
  37. }
  38. async function alertUser(title, message) {
  39. if (window.shiguangBridgePromise && typeof window.shiguangBridgePromise.showAlert === 'function') {
  40. const confirmed = await window.shiguangBridgePromise.showAlert(title, message, '知道了');
  41. return confirmed === true || confirmed === 'true';
  42. }
  43. alert(title + '\n' + message);
  44. return true;
  45. }
  46. // 原生以字符串序号或 "null" 回传,统一为数字序号,-1 表示取消
  47. async function selectFromList(title, items, defaultIndex) {
  48. if (!(window.shiguangBridgePromise && typeof window.shiguangBridgePromise.showSingleSelection === 'function')) {
  49. return defaultIndex;
  50. }
  51. const result = await window.shiguangBridgePromise.showSingleSelection(title, JSON.stringify(items), defaultIndex);
  52. if (result === null || result === undefined || result === 'null') return -1;
  53. const index = typeof result === 'number' ? result : parseInt(result, 10);
  54. return isNaN(index) ? -1 : index;
  55. }
  56. // ==================== 请求 ====================
  57. // 兼容直连与 WebVPN 形态的地址,取当前路径中 /jsxsd/ 之前的部分作为前缀
  58. function jsxsdUrl(path) {
  59. const currentPath = window.location.pathname || '';
  60. const index = currentPath.indexOf('/jsxsd/');
  61. return window.location.origin + (index < 0 ? '' : currentPath.slice(0, index)) + path;
  62. }
  63. async function requestText(url, options) {
  64. const response = await fetch(url, Object.assign({ credentials: 'include' }, options || {}));
  65. return await response.text();
  66. }
  67. // 不同强智版本分别使用 #timetable 与 #kbtable
  68. function findTimetableTable(doc) {
  69. return doc.getElementById('timetable') || doc.getElementById('kbtable');
  70. }
  71. // 优先 GET 带学期参数;失败再退回 POST 表单
  72. async function fetchTimetableDoc(semesterValue) {
  73. const url = jsxsdUrl(HUNAU_KB_PATH);
  74. const got = new DOMParser().parseFromString(
  75. await requestText(url + '?xnxq01id=' + encodeURIComponent(semesterValue)), 'text/html');
  76. if (findTimetableTable(got)) return got;
  77. const body = 'xnxq01id=' + encodeURIComponent(semesterValue) + '&sfFD=1';
  78. const posted = new DOMParser().parseFromString(
  79. await requestText(url, Object.assign({ method: 'POST', body: body }, HUNAU_POST_OPTIONS)), 'text/html');
  80. return findTimetableTable(posted) ? posted : null;
  81. }
  82. // ==================== 学期 ====================
  83. // 学期列表:[{ value: "2025-2026-2", label: "2025-2026 学年第 2 学期", selected: true }]
  84. function parseSemesters(doc) {
  85. const select = doc.querySelector('select[name="xnxq01id"], select#xnxq01id');
  86. if (!select) return [];
  87. const list = [];
  88. select.querySelectorAll('option').forEach(function (option) {
  89. const value = String(option.getAttribute('value') || '').trim();
  90. if (!value) return;
  91. list.push({
  92. value: value,
  93. label: value.replace(/^(\d{4})-(\d{4})-(\d)$/, '$1-$2 学年第 $3 学期'),
  94. selected: option.hasAttribute('selected')
  95. });
  96. });
  97. return list;
  98. }
  99. // ==================== 节次时间 ====================
  100. function timeToMinutes(text) {
  101. const match = String(text || '').match(/(\d{1,2}):(\d{2})/);
  102. return match ? parseInt(match[1], 10) * 60 + parseInt(match[2], 10) : null;
  103. }
  104. function minutesToTime(minutes) {
  105. const value = Math.round(minutes);
  106. return String(Math.floor(value / 60)).padStart(2, '0') + ':' + String(value % 60).padStart(2, '0');
  107. }
  108. // 从课表行标题(如“第一大节 08:00-09:40”)推导每一小节的上课时间。
  109. // 大节内的小节数由时长反推:n 个小节的时长 = n*45 + (n-1)*课间,
  110. // 大节内的剩余时间再均分为课间。这样即使某门课跨大节,也不会污染小节的节次编号。
  111. function parseTimeSlots(doc) {
  112. const table = findTimetableTable(doc);
  113. if (!table) return [];
  114. const slots = [];
  115. Array.from(table.querySelectorAll('tr')).forEach(function (row) {
  116. const label = row.querySelector('th');
  117. if (!label) return;
  118. const labelText = String(label.textContent).replace(/\s+/g, ' ').trim();
  119. const match = labelText.match(/第([一二三四五六七八九十]+)大节\s*(\d{1,2}:\d{2})\s*[-~—]\s*(\d{1,2}:\d{2})/);
  120. if (!match) return;
  121. const bigIndex = HUNAU_CN_NUMBER[match[1]];
  122. const startMin = timeToMinutes(match[2]);
  123. const endMin = timeToMinutes(match[3]);
  124. if (!bigIndex || startMin === null || endMin === null || endMin <= startMin) return;
  125. const span = endMin - startMin;
  126. const unit = HUNAU_CLASS_DURATION + HUNAU_BREAK_DURATION;
  127. const count = Math.max(1, Math.round((span + HUNAU_BREAK_DURATION) / unit));
  128. const gap = count > 1 ? (span - count * HUNAU_CLASS_DURATION) / (count - 1) : 0;
  129. for (let i = 0; i < count; i++) {
  130. const start = startMin + i * (HUNAU_CLASS_DURATION + gap);
  131. slots.push({
  132. number: (bigIndex - 1) * count + i + 1,
  133. startTime: minutesToTime(start),
  134. endTime: minutesToTime(start + HUNAU_CLASS_DURATION)
  135. });
  136. }
  137. });
  138. // 去重并按节次排序;数据不足时回退到内置作息
  139. const byNumber = {};
  140. slots.forEach(function (slot) { if (!byNumber[slot.number]) byNumber[slot.number] = slot; });
  141. const result = Object.keys(byNumber).map(function (key) { return byNumber[key]; })
  142. .sort(function (a, b) { return a.number - b.number; });
  143. return result.length >= 6 ? result : HUNAU_FALLBACK_TIME_SLOTS;
  144. }
  145. // ==================== 课表解析 ====================
  146. // 周次与节次:兼容 1-16(周)[01-02节]、1-8,10-16(周)、1-15周(单)、2-16(双) 等写法
  147. function parseWeeksAndSections(text) {
  148. const result = { weeks: [], sections: [] };
  149. const content = String(text || '').trim();
  150. if (!content) return result;
  151. const parity = /双/.test(content) ? 2 : (/单/.test(content) ? 1 : 0);
  152. const bracketIndex = content.indexOf('[');
  153. const cleaned = (bracketIndex >= 0 ? content.slice(0, bracketIndex) : content)
  154. .replace(/第/g, '')
  155. .replace(/至|到|~/g, '-')
  156. .replace(/[((][^()()]*[))]/g, '')
  157. .replace(/周/g, '');
  158. cleaned.split(/[,,、;;\s]+/).forEach(function (segment) {
  159. const item = segment.trim();
  160. const range = item.match(/^(\d+)\s*[-–—]\s*(\d+)$/);
  161. if (range) {
  162. for (let week = parseInt(range[1], 10); week <= parseInt(range[2], 10); week++) result.weeks.push(week);
  163. } else if (/^\d+$/.test(item)) {
  164. result.weeks.push(parseInt(item, 10));
  165. }
  166. });
  167. if (parity) {
  168. result.weeks = result.weeks.filter(function (week) { return week % 2 === (parity === 1 ? 1 : 0); });
  169. }
  170. result.weeks = Array.from(new Set(result.weeks)).sort(function (a, b) { return a - b; });
  171. const sectionMatch = content.match(/\[([^\]]*)\]/);
  172. if (sectionMatch) {
  173. (sectionMatch[1].match(/\d+/g) || []).forEach(function (number) {
  174. const section = parseInt(number, 10);
  175. if (result.sections.indexOf(section) < 0) result.sections.push(section);
  176. });
  177. result.sections.sort(function (a, b) { return a - b; });
  178. }
  179. return result;
  180. }
  181. // 教师姓名在教师字体里与职称连写(如“何龙姣讲师”“周小红其他”),且多人以逗号分隔,逐个去掉常见职称后缀
  182. function cleanTeacher(text) {
  183. const raw = String(text || '').trim();
  184. if (!raw) return '';
  185. const titles = ['其他正高级', '其他副高级', '其他中级', '其他初级', '副教授', '教授', '副研究员', '研究员',
  186. '高级实验师', '实验师', '高级工程师', '工程师', '高级技师', '技师', '讲师', '助教', '其他'];
  187. const strip = function (name) {
  188. const value = name.trim();
  189. for (let i = 0; i < titles.length; i++) {
  190. if (value.length > titles[i].length && value.slice(-titles[i].length) === titles[i]) {
  191. return value.slice(0, value.length - titles[i].length).trim();
  192. }
  193. }
  194. return value;
  195. };
  196. return raw.split(/[,,、;;\/]+/).map(strip).filter(function (name) { return name; }).join(',');
  197. }
  198. // 收集单个课表单元格内的全部课程候选。强智在格内同时输出简表与详表,
  199. // 简表缺少教师与节次方括号,此处一并解析,交由归并阶段择优。
  200. function collectCellCandidates(cellDiv, day, output, rowSections) {
  201. const html = String(cellDiv.innerHTML || '').trim();
  202. if (!html || html.replace(/&nbsp;/gi, '').trim() === '') return;
  203. html.split(/[-—–]{6,}\s*(?:<br\s*\/?>)?/i).forEach(function (blockHtml) {
  204. if (!blockHtml.trim()) return;
  205. const holder = document.createElement('div');
  206. holder.innerHTML = blockHtml;
  207. let name = '';
  208. for (let i = 0; i < holder.childNodes.length; i++) {
  209. const node = holder.childNodes[i];
  210. if (node.nodeType === 3 && /[\u4e00-\u9fa5]/.test(node.textContent)) {
  211. name = node.textContent.trim();
  212. break;
  213. }
  214. }
  215. if (!name) name = String(holder.textContent || '').split('\n')[0].trim();
  216. if (!name) return;
  217. const pickText = function (titles) {
  218. for (let i = 0; i < titles.length; i++) {
  219. const element = holder.querySelector('font[title*="' + titles[i] + '"]');
  220. if (element && element.textContent.trim()) return element.textContent.trim();
  221. }
  222. return '';
  223. };
  224. let timeText = pickText(['周次', '节次']);
  225. if (!/\[[^\]]*\]/.test(timeText)) {
  226. const bracket = String(holder.textContent || '').match(/\[[^\]]*\]/);
  227. if (bracket) timeText += bracket[0];
  228. }
  229. const parsed = parseWeeksAndSections(timeText || holder.textContent);
  230. if (parsed.weeks.length === 0) return;
  231. if (parsed.sections.length === 0 && rowSections) {
  232. for (let section = rowSections[0]; section <= rowSections[1]; section++) parsed.sections.push(section);
  233. }
  234. const teacher = cleanTeacher(pickText(['教师', '老师']));
  235. const position = pickText(['教室', '地点']).replace(/\[\s*\d+\s*-\s*\d+\s*\]\s*节?$/, '').trim();
  236. output.push({
  237. name: name,
  238. teacher: teacher,
  239. position: position,
  240. day: day,
  241. weeks: parsed.weeks,
  242. sections: parsed.sections,
  243. score: (teacher ? 4 : 0) + (/\[[^\]]*\]/.test(timeText) ? 3 : 0) +
  244. (position ? 1 : 0) + (parsed.sections.length > 1 ? 1 : 0)
  245. });
  246. });
  247. }
  248. // 按课程名、周次、教室归并候选,保留信息更完整的一条
  249. function mergeCellCandidates(candidates) {
  250. const groups = {};
  251. const order = [];
  252. candidates.forEach(function (item) {
  253. const key = item.name + '|' + item.weeks.join(',') + '|' + item.position;
  254. if (groups[key] === undefined) {
  255. groups[key] = item;
  256. order.push(key);
  257. } else if (item.score > groups[key].score) {
  258. groups[key] = item;
  259. }
  260. });
  261. const courses = [];
  262. order.forEach(function (key) {
  263. const item = groups[key];
  264. if (item.sections.length === 0) return;
  265. courses.push({
  266. name: item.name,
  267. teacher: item.teacher || '未知教师',
  268. position: item.position || '未知地点',
  269. day: item.day,
  270. weeks: item.weeks,
  271. startSection: item.sections[0],
  272. endSection: item.sections[item.sections.length - 1]
  273. });
  274. });
  275. return courses;
  276. }
  277. function parseTimetable(doc) {
  278. const table = findTimetableTable(doc);
  279. if (!table) return [];
  280. const courses = [];
  281. let bigIndex = 0;
  282. Array.from(table.querySelectorAll('tr')).forEach(function (row) {
  283. const label = row.querySelector('th');
  284. const labelText = label ? String(label.textContent).replace(/\s+/g, ' ').trim() : '';
  285. if (/^第[一二三四五六七八九十]+大节/.test(labelText)) bigIndex++;
  286. const rowSections = bigIndex > 0 ? [2 * bigIndex - 1, 2 * bigIndex] : null;
  287. Array.from(row.querySelectorAll('td')).forEach(function (cell, index) {
  288. const blocks = cell.querySelectorAll('div.kbcontent, div.kbcontent1');
  289. if (blocks.length === 0) return;
  290. const candidates = [];
  291. Array.from(blocks).forEach(function (block) {
  292. collectCellCandidates(block, index + 1, candidates, rowSections);
  293. });
  294. mergeCellCandidates(candidates).forEach(function (course) {
  295. courses.push(course);
  296. });
  297. });
  298. });
  299. return courses;
  300. }
  301. // ==================== 教学周历 ====================
  302. // 教学周历:首列为周次序号,第二列 title 为该周星期一日期(如 2026年08月31)
  303. function parseCalendar(doc) {
  304. const result = { semesterStartDate: null, totalWeeks: null };
  305. const table = doc.querySelector('table');
  306. if (!table) return result;
  307. const toDate = function (text) {
  308. const match = String(text || '').match(/(\d{4})\s*[-年]\s*(\d{1,2})\s*[-月]\s*(\d{1,2})/);
  309. return match ? match[1] + '-' + match[2].padStart(2, '0') + '-' + match[3].padStart(2, '0') : null;
  310. };
  311. Array.from(table.querySelectorAll('tr')).forEach(function (row) {
  312. const cells = Array.from(row.children);
  313. const first = cells.length > 0 ? String(cells[0].textContent).trim() : '';
  314. if (!/^\d+$/.test(first)) return;
  315. const week = parseInt(first, 10);
  316. result.totalWeeks = result.totalWeeks === null ? week : Math.max(result.totalWeeks, week);
  317. if (week === 1 && !result.semesterStartDate && cells.length > 1) {
  318. result.semesterStartDate = toDate(cells[1].getAttribute('title')) || toDate(cells[1].textContent);
  319. }
  320. });
  321. return result;
  322. }
  323. async function fetchCalendar(semesterValue) {
  324. try {
  325. const text = await requestText(jsxsdUrl(HUNAU_CALENDAR_PATH), Object.assign(
  326. { method: 'POST', body: 'xnxq01id=' + encodeURIComponent(semesterValue) }, HUNAU_POST_OPTIONS));
  327. return parseCalendar(new DOMParser().parseFromString(text, 'text/html'));
  328. } catch (error) {
  329. console.warn('[HUNAU] 教学周历读取失败:', error);
  330. return { semesterStartDate: null, totalWeeks: null };
  331. }
  332. }
  333. // ==================== 合并去重 ====================
  334. // 节次与周次合并去重函数
  335. // 来源:官方 Wiki《课程合并与去重函数》
  336. // https://github.com/XingHeYuZhuan/shiguangschedule/wiki/课程合并与去重函数
  337. function mergeAndDistinctCourses(courses) {
  338. if (!Array.isArray(courses) || courses.length <= 1) return courses;
  339. const list = courses.map(c => ({
  340. ...c,
  341. name: c.name || '',
  342. teacher: c.teacher || '',
  343. position: c.position || '',
  344. weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
  345. }));
  346. list.sort((a, b) => {
  347. return a.name.localeCompare(b.name) ||
  348. a.teacher.localeCompare(b.teacher) ||
  349. a.position.localeCompare(b.position) ||
  350. (a.day || 0) - (b.day || 0) ||
  351. a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
  352. (a.startSection || 0) - (b.startSection || 0);
  353. });
  354. const step1Merged = [];
  355. let current = list[0];
  356. for (let i = 1; i < list.length; i++) {
  357. const next = list[i];
  358. const isSameCourseAndWeeks =
  359. current.name === next.name &&
  360. current.teacher === next.teacher &&
  361. current.position === next.position &&
  362. current.day === next.day &&
  363. current.weeks.join(',') === next.weeks.join(',');
  364. const isContinuous = current.endSection + 1 === next.startSection;
  365. const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
  366. if (isSameCourseAndWeeks && isContinuous) {
  367. current.endSection = next.endSection;
  368. } else if (isSameCourseAndWeeks && isDuplicate) {
  369. continue;
  370. } else {
  371. step1Merged.push(current);
  372. current = next;
  373. }
  374. }
  375. step1Merged.push(current);
  376. step1Merged.sort((a, b) => {
  377. return a.name.localeCompare(b.name) ||
  378. a.teacher.localeCompare(b.teacher) ||
  379. a.position.localeCompare(b.position) ||
  380. (a.day || 0) - (b.day || 0) ||
  381. (a.startSection || 0) - (b.startSection || 0) ||
  382. (a.endSection || 0) - (b.endSection || 0);
  383. });
  384. const step2Merged = [];
  385. let merged = step1Merged[0];
  386. for (let i = 1; i < step1Merged.length; i++) {
  387. const next = step1Merged[i];
  388. const isSameCourseAndSection =
  389. merged.name === next.name &&
  390. merged.teacher === next.teacher &&
  391. merged.position === next.position &&
  392. merged.day === next.day &&
  393. merged.startSection === next.startSection &&
  394. merged.endSection === next.endSection;
  395. if (isSameCourseAndSection) {
  396. merged.weeks = Array.from(new Set([...merged.weeks, ...next.weeks])).sort((a, b) => a - b);
  397. } else {
  398. step2Merged.push(merged);
  399. merged = next;
  400. }
  401. }
  402. step2Merged.push(merged);
  403. return step2Merged;
  404. }
  405. // ==================== 流程编排 ====================
  406. async function runImportFlow() {
  407. try {
  408. if ((window.location.pathname || '').indexOf('/jsxsd/') < 0) {
  409. await alertUser('请先进入教务系统', '请登录教务系统并停留在任意教务功能页面后重新执行导入。');
  410. return;
  411. }
  412. toast('正在获取学期信息…');
  413. const listDoc = new DOMParser().parseFromString(await requestText(jsxsdUrl(HUNAU_KB_PATH)), 'text/html');
  414. const semesters = parseSemesters(listDoc);
  415. if (semesters.length === 0) {
  416. await alertUser('未获取到学期列表', '课表页返回异常,请确认登录状态有效后重试。');
  417. return;
  418. }
  419. let defaultIndex = 0;
  420. semesters.forEach(function (item, index) { if (item.selected) defaultIndex = index; });
  421. const picked = semesters.length > 1
  422. ? await selectFromList('选择学期', semesters.map(function (item) { return item.label; }), defaultIndex)
  423. : defaultIndex;
  424. if (picked < 0 || picked >= semesters.length) {
  425. toast('导入已取消');
  426. return;
  427. }
  428. const semester = semesters[picked];
  429. toast('正在获取 ' + semester.label + ' 课表…');
  430. const tableDoc = await fetchTimetableDoc(semester.value);
  431. if (!tableDoc) {
  432. await alertUser('未获取到课表数据', '请确认已登录教务系统后重试。');
  433. return;
  434. }
  435. const courses = mergeAndDistinctCourses(parseTimetable(tableDoc));
  436. if (courses.length === 0) {
  437. await alertUser('未解析到课程', semester.label + ' 页面中没有课程内容,该学期可能尚未发布课表。');
  438. return;
  439. }
  440. const calendar = await fetchCalendar(semester.value);
  441. const weeks = courses.reduce(function (all, course) { return all.concat(course.weeks); }, []);
  442. const config = {
  443. semesterTotalWeeks: Math.max(HUNAU_DEFAULT_TOTAL_WEEKS, Math.max.apply(null, weeks)),
  444. defaultClassDuration: HUNAU_CLASS_DURATION,
  445. defaultBreakDuration: HUNAU_BREAK_DURATION
  446. };
  447. if (calendar.semesterStartDate) {
  448. config.semesterStartDate = calendar.semesterStartDate;
  449. } else {
  450. toast('未读取到教学周历,开学日期可在软件内手动校准');
  451. }
  452. // 课表配置与节次时间属于可选数据,失败不影响课程导入
  453. try {
  454. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  455. } catch (error) {
  456. console.warn('[HUNAU] 课表配置保存失败:', error);
  457. }
  458. try {
  459. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(parseTimeSlots(tableDoc)));
  460. } catch (error) {
  461. console.warn('[HUNAU] 节次时间保存失败:', error);
  462. }
  463. if (!await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses))) {
  464. toast('课程保存失败,请重试');
  465. return;
  466. }
  467. toast('导入成功:' + semester.label + ' 共 ' + courses.length + ' 条课程时段');
  468. if (window.shiguangBridge && typeof window.shiguangBridge.notifyTaskCompletion === 'function') {
  469. window.shiguangBridge.notifyTaskCompletion();
  470. }
  471. } catch (error) {
  472. console.error('[HUNAU] 导入流程异常:', error);
  473. await alertUser('导入失败', error && error.message ? error.message : String(error));
  474. }
  475. }
  476. // 启动导入流程
  477. runImportFlow();