sustech.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  1. // 南方科技大学 TIS 教学管理与服务平台 课表导入适配器
  2. //
  3. // 依赖的 TIS 接口(均为登录后同域请求,凭证由 WebView 自动携带):
  4. // POST /component/querydangqianxnxq 当前学年学期 { XN, XQ, XNXQ }
  5. // POST /cjgl/grcjcx/grcjcx 历年课程记录(用于生成可选学期列表)
  6. // POST /xszykb/queryxszykbzong 整学期课表(xn, xq)
  7. // POST /component/queryKbjg 作息时间(xn, xq, zc)
  8. // POST /component/querydangqianzc 当前教学周(纯数字文本)
  9. //
  10. // 课表条目关键字段(2026 秋实测):
  11. // KEY "xq1_jc5" -> 星期 1(周一),jc 为课表网格行号(非节次)
  12. // KSJC / JSJC 起始 / 结束节次(真正的节次)
  13. // ZC 0/1 位图,下标即周次(下标 0 未使用)
  14. // SKSJ 多行文本:课程名 / [教师] / [班级] / [周次][地点][节次]
  15. // KCWZSM / SKFS 等 实测为 null,仅作兜底
  16. // ========================================
  17. // 常量
  18. // ========================================
  19. // 教务系统未返回作息时间时的兜底节次时间(南科大常规作息)
  20. const FALLBACK_TIME_SLOTS = [
  21. { number: 1, startTime: "08:00", endTime: "08:50" },
  22. { number: 2, startTime: "09:00", endTime: "09:50" },
  23. { number: 3, startTime: "10:20", endTime: "11:10" },
  24. { number: 4, startTime: "11:20", endTime: "12:10" },
  25. { number: 5, startTime: "14:00", endTime: "14:50" },
  26. { number: 6, startTime: "15:00", endTime: "15:50" },
  27. { number: 7, startTime: "16:20", endTime: "17:10" },
  28. { number: 8, startTime: "17:20", endTime: "18:10" },
  29. { number: 9, startTime: "19:00", endTime: "19:50" },
  30. { number: 10, startTime: "20:00", endTime: "20:50" },
  31. { number: 11, startTime: "21:20", endTime: "22:10" },
  32. { number: 12, startTime: "22:20", endTime: "23:10" }
  33. ];
  34. const CONFIG_BASE = {
  35. semesterTotalWeeks: 18,
  36. defaultClassDuration: 50,
  37. defaultBreakDuration: 10
  38. };
  39. // ========================================
  40. // 网络请求
  41. // ========================================
  42. async function postFormRaw(path, params) {
  43. const body = new URLSearchParams();
  44. Object.keys(params || {}).forEach(function (key) {
  45. const value = params[key];
  46. body.append(key, value === null || value === undefined ? "" : String(value));
  47. });
  48. const response = await fetch(path, {
  49. method: "POST",
  50. credentials: "include",
  51. headers: {
  52. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  53. "X-Requested-With": "XMLHttpRequest"
  54. },
  55. body: body.toString()
  56. });
  57. if (!response.ok) {
  58. throw new Error(`请求 ${path} 失败:HTTP ${response.status}`);
  59. }
  60. return await response.text();
  61. }
  62. async function postForm(path, params) {
  63. const text = await postFormRaw(path, params);
  64. return parseJsonSafely(text, path);
  65. }
  66. async function postJson(path, payload) {
  67. const response = await fetch(path, {
  68. method: "POST",
  69. credentials: "include",
  70. headers: {
  71. "Content-Type": "application/json",
  72. "X-Requested-With": "XMLHttpRequest"
  73. },
  74. body: JSON.stringify(payload || {})
  75. });
  76. if (!response.ok) {
  77. throw new Error(`请求 ${path} 失败:HTTP ${response.status}`);
  78. }
  79. return parseJsonSafely(await response.text(), path);
  80. }
  81. function parseJsonSafely(text, path) {
  82. const trimmed = (text || "").trim();
  83. if (!trimmed) {
  84. return null;
  85. }
  86. if (trimmed.charAt(0) === "<") {
  87. throw new Error("登录状态已失效,请重新登录本科教务系统后再导入。");
  88. }
  89. // 会话失效时 TIS 会返回登录提示页(HTML 或带"登录"字样的 JSON)
  90. if (/重新登录|请登录|用户认证|登录已过期/.test(trimmed)) {
  91. throw new Error("登录状态已失效,请重新登录教务系统后再导入。");
  92. }
  93. try {
  94. return JSON.parse(trimmed);
  95. } catch (error) {
  96. throw new Error(
  97. `教务系统返回了非 JSON 内容,请确认已登录本科教务系统(${path})。`
  98. );
  99. }
  100. }
  101. // 取数组型数据:接口可能直接返回数组,也可能包在 content / data 里
  102. function toArray(data) {
  103. if (Array.isArray(data)) {
  104. return data;
  105. }
  106. if (data && Array.isArray(data.content)) {
  107. return data.content;
  108. }
  109. if (data && Array.isArray(data.data)) {
  110. return data.data;
  111. }
  112. return [];
  113. }
  114. // ========================================
  115. // 学期信息
  116. // ========================================
  117. async function fetchCurrentSemester() {
  118. const data = await postForm("/component/querydangqianxnxq", {});
  119. if (!data || !data.XN || !data.XQ) {
  120. throw new Error("未能获取学年学期,请确认已登录本科教务系统。");
  121. }
  122. return {
  123. xn: String(data.XN),
  124. xq: String(data.XQ),
  125. label: data.XNXQ || `${data.XN}学年第${data.XQ}学期`
  126. };
  127. }
  128. // 通过成绩/选课接口拿到该学生有记录的所有学期,作为可选列表
  129. async function fetchSemesterOptions() {
  130. const options = [];
  131. const seen = {};
  132. try {
  133. const data = await postJson("/cjgl/grcjcx/grcjcx", {
  134. xn: null,
  135. xq: null,
  136. kcmc: null,
  137. cxbj: "-1",
  138. pylx: "1",
  139. current: 1,
  140. pageSize: 500
  141. });
  142. const rows = (data && data.content && data.content.list) || [];
  143. rows.forEach(function (row) {
  144. addSemesterOption(options, seen, row && row.xnxq, row && row.xnxqmc);
  145. });
  146. } catch (error) {
  147. // 拿不到历史学期列表时不阻塞流程,调用方会退回"仅当前学期"
  148. return options;
  149. }
  150. return options;
  151. }
  152. function addSemesterOption(options, seen, xnxq, label) {
  153. const matched = String(xnxq || "").match(/^(\d{4}-\d{4})(\d)$/);
  154. if (!matched) {
  155. return;
  156. }
  157. const xn = matched[1];
  158. const xq = matched[2];
  159. const key = `${xn}-${xq}`;
  160. if (seen[key]) {
  161. return;
  162. }
  163. seen[key] = true;
  164. options.push({ xn: xn, xq: xq, key: key, label: label || key });
  165. }
  166. function mergeCurrentSemester(options, current) {
  167. const key = `${current.xn}-${current.xq}`;
  168. const exists = options.some(function (item) {
  169. return item.key === key;
  170. });
  171. if (!exists) {
  172. options.push({
  173. xn: current.xn,
  174. xq: current.xq,
  175. key: key,
  176. label: current.label
  177. });
  178. }
  179. options.sort(function (a, b) {
  180. return b.key < a.key ? -1 : 1;
  181. });
  182. return options;
  183. }
  184. // ========================================
  185. // 用户交互
  186. // ========================================
  187. function validateDateInput(input) {
  188. const text = String(input || "").trim();
  189. if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) {
  190. return "请输入 YYYY-MM-DD 格式的日期!";
  191. }
  192. const date = new Date(`${text}T00:00:00`);
  193. if (isNaN(date.getTime())) {
  194. return "请输入有效的日期!";
  195. }
  196. return false;
  197. }
  198. async function confirmStart() {
  199. return await window.shiguangBridgePromise.showAlert(
  200. "导入说明",
  201. "本适配用于导入南方科技大学 TIS 本科教务系统的课表。\n请先完成 CAS 登录并停留在教务系统页面内,再点击确认开始导入。",
  202. "开始导入"
  203. );
  204. }
  205. async function selectSemester(options, current) {
  206. if (!options.length) {
  207. return { xn: current.xn, xq: current.xq, key: `${current.xn}-${current.xq}` };
  208. }
  209. const labels = options.map(function (item) {
  210. return `${item.label}(${item.xn}学年第${item.xq}学期)`;
  211. });
  212. const defaultIndex = options.findIndex(function (item) {
  213. return item.key === `${current.xn}-${current.xq}`;
  214. });
  215. const index = await window.shiguangBridgePromise.showSingleSelection(
  216. "选择学期",
  217. JSON.stringify(labels),
  218. defaultIndex >= 0 ? defaultIndex : 0
  219. );
  220. if (index === null || index < 0 || index >= options.length) {
  221. return null;
  222. }
  223. return options[index];
  224. }
  225. async function askSemesterStartDate(defaultValue) {
  226. const input = await window.shiguangBridgePromise.showPrompt(
  227. "学期开始日期",
  228. "该学期不是当前学期,无法自动推算周次。\n请输入该学期第 1 周星期一的日期:",
  229. defaultValue,
  230. "validateDateInput"
  231. );
  232. if (input === null) {
  233. return null;
  234. }
  235. return String(input).trim();
  236. }
  237. // ========================================
  238. // 课表数据与作息时间
  239. // ========================================
  240. async function fetchTimeSlots(xn, xq) {
  241. try {
  242. const data = await postForm("/component/queryKbjg", { xn: xn, xq: xq, zc: "1" });
  243. const slots = toArray(data).map(function (item) {
  244. const number = toNumber(item.xj);
  245. const startTime = normalizeTime(item.kssj);
  246. const endTime = normalizeTime(item.jssj);
  247. if (!number || !startTime || !endTime) {
  248. return null;
  249. }
  250. return { number: number, startTime: startTime, endTime: endTime };
  251. }).filter(Boolean);
  252. if (slots.length) {
  253. slots.sort(function (a, b) {
  254. return a.number - b.number;
  255. });
  256. return slots;
  257. }
  258. } catch (error) {
  259. // 忽略,使用兜底作息
  260. }
  261. return FALLBACK_TIME_SLOTS.slice();
  262. }
  263. async function fetchCourses(xn, xq) {
  264. const data = await postForm("/xszykb/queryxszykbzong", { xn: xn, xq: xq });
  265. const rows = toArray(data);
  266. if (!rows.length) {
  267. throw new Error("教务系统未返回课表数据,请确认该学期已选课。");
  268. }
  269. return parseCourses(rows);
  270. }
  271. function parseCourses(rows) {
  272. const courses = [];
  273. const seen = {};
  274. rows.forEach(function (row) {
  275. const course = parseCourse(row);
  276. if (!course) {
  277. return;
  278. }
  279. const identity = [
  280. course.name,
  281. course.day,
  282. course.startSection,
  283. course.endSection,
  284. course.weeks.join(","),
  285. course.teacher,
  286. course.position
  287. ].join("|");
  288. // 同一节课会在课表网格中占多行(KEY 的 jc 不同),需要去重
  289. if (seen[identity]) {
  290. return;
  291. }
  292. seen[identity] = true;
  293. courses.push(course);
  294. });
  295. return courses;
  296. }
  297. function parseCourse(row) {
  298. if (!row || typeof row !== "object") {
  299. return null;
  300. }
  301. const keyMatch = String(row.KEY || "").match(/xq(\d+)_jc(\d+)/i);
  302. const day = keyMatch ? toNumber(keyMatch[1]) : null;
  303. if (!day || day < 1 || day > 7) {
  304. return null;
  305. }
  306. const detail = parseScheduleText(row.SKSJ || row.SKSJ_EN);
  307. // 真实数据中 KCWZSM 为 null,课程信息以 SKSJ 为准
  308. const name = firstText(detail.name, row.KCMC, row.KCWZSM);
  309. if (!name) {
  310. return null;
  311. }
  312. // jc 只是网格行号,真实节次用 KSJC / JSJC
  313. const slotIndex = keyMatch ? toNumber(keyMatch[2]) : null;
  314. const startSection = toNumber(row.KSJC) || slotIndex;
  315. const endSection = toNumber(row.JSJC) || startSection;
  316. const weeks = parseWeekBitmap(row.ZC, detail.declaredWeeks);
  317. if (!startSection || !endSection || endSection < startSection || !weeks.length) {
  318. return null;
  319. }
  320. return {
  321. name: name,
  322. teacher: firstText(detail.teacher, row.SKJS, row.JSXM, row.DGJSMC),
  323. position: firstText(detail.position, row.SKDD, row.JXCDMC, row.SKDDMC),
  324. day: day,
  325. startSection: startSection,
  326. endSection: endSection,
  327. weeks: weeks
  328. };
  329. }
  330. // ZC 是 0/1 位图,实测下标即周次(下标 0 未使用)。
  331. // 为兼容不同学期的差异,同时算出两种解释,用 SKSJ 里声明的周次(如 [1-16周])校准。
  332. function parseWeekBitmap(value, declaredWeeks) {
  333. const bitmap = String(value || "");
  334. if (!/^[01]+$/.test(bitmap)) {
  335. return declaredWeeks || [];
  336. }
  337. function build(shift) {
  338. const weeks = [];
  339. for (let index = 0; index < bitmap.length; index += 1) {
  340. if (bitmap[index] === "1") {
  341. const week = index + 1 - shift;
  342. if (week > 0) {
  343. weeks.push(week);
  344. }
  345. }
  346. }
  347. return weeks;
  348. }
  349. // shift=1:周次 = 下标(教务实际使用的口径);shift=0:周次 = 下标 + 1
  350. const candidates = [build(1), build(0)];
  351. if (!declaredWeeks || !declaredWeeks.length) {
  352. return candidates[0];
  353. }
  354. let best = candidates[0];
  355. let bestScore = -1;
  356. candidates.forEach(function (weeks) {
  357. const set = {};
  358. weeks.forEach(function (week) {
  359. set[week] = true;
  360. });
  361. const score = declaredWeeks.filter(function (week) {
  362. return set[week];
  363. }).length;
  364. if (score > bestScore) {
  365. bestScore = score;
  366. best = weeks;
  367. }
  368. });
  369. return best;
  370. }
  371. const WEEK_PATTERN = /\d[\d,\-]*(单|双)?周/; // 1-16周 / 1-15单周 / 1-9,11周
  372. const SECTION_PATTERN = /\d+\s*(-\s*\d+)?\s*节/; // 9-10节 / 3节
  373. // 详情行:含周次或节次的那些行,如 [1-16周][一教110][1-2节]、[3-16周]、[教室][1-4节]
  374. function isDetailLine(line) {
  375. return WEEK_PATTERN.test(line) || SECTION_PATTERN.test(line);
  376. }
  377. // 解析 "1-16周" / "1-9,11,13-15周" / "1-15单周" 形式的周次声明
  378. function parseWeekExpression(text) {
  379. const matched = String(text).match(WEEK_PATTERN);
  380. if (!matched) {
  381. return [];
  382. }
  383. // 去掉末尾的 "单周" / "双周" / "周",剩下的就是周次表达式
  384. const expression = matched[0].replace(/(单|双)?周$/, "");
  385. const parity = /单周$/.test(matched[0]) ? "单" : (/双周$/.test(matched[0]) ? "双" : "");
  386. const weeks = [];
  387. expression.split(",").forEach(function (part) {
  388. const range = part.match(/^(\d+)(?:-(\d+))?$/);
  389. if (!range) {
  390. return;
  391. }
  392. const start = Number(range[1]);
  393. const end = range[2] ? Number(range[2]) : start;
  394. for (let week = start; week <= end && week <= 36; week += 1) {
  395. weeks.push(week);
  396. }
  397. });
  398. const filtered = weeks.filter(function (week) {
  399. if (parity === "单") {
  400. return week % 2 === 1;
  401. }
  402. if (parity === "双") {
  403. return week % 2 === 0;
  404. }
  405. return true;
  406. });
  407. return uniqueSorted(filtered);
  408. }
  409. // SKSJ 形如:
  410. // 思想道德与法治
  411. // [兰美荣]
  412. // [思想道德与法治-09班-中文]
  413. // [1-16周][智华楼207][9-10节]
  414. function parseScheduleText(value) {
  415. const lines = String(value || "")
  416. .replace(/<br\s*\/?>/gi, "\n")
  417. .replace(/<[^>]+>/g, "\n")
  418. .split("\n")
  419. .map(function (line) {
  420. return line.trim();
  421. })
  422. .filter(Boolean);
  423. const result = { name: "", teacher: "", position: "", declaredWeeks: [] };
  424. if (!lines.length) {
  425. return result;
  426. }
  427. result.name = unwrap(lines[0]);
  428. lines.slice(1).forEach(function (line) {
  429. const groups = bracketGroups(line);
  430. // 详情行:[1-16周][智华楼207][9-10节]
  431. // 注意:不能用 "含周/含节" 判断,课程名或教师名里可能有"周"字(如"某单周课")
  432. if (isDetailLine(line)) {
  433. if (!result.declaredWeeks.length) {
  434. result.declaredWeeks = parseWeekExpression(line);
  435. }
  436. if (!result.position) {
  437. const place = firstMatch(groups, function (part) {
  438. return part && !WEEK_PATTERN.test(part) && !SECTION_PATTERN.test(part) &&
  439. !/班/.test(part) && part !== result.name &&
  440. part.indexOf(result.name) < 0 && result.name.indexOf(part) < 0;
  441. });
  442. if (place) {
  443. result.position = unwrap(place);
  444. }
  445. }
  446. return;
  447. }
  448. const text = unwrap(line);
  449. // 教师行:课程名的下一行,排除班级行(含"班")与课程名本身
  450. if (!result.teacher && text && text !== result.name && !/班/.test(text)) {
  451. result.teacher = text;
  452. }
  453. });
  454. return result;
  455. }
  456. // ========================================
  457. // 课表配置(学期开始日期 / 总周数)
  458. // ========================================
  459. async function fetchCurrentWeek() {
  460. try {
  461. const text = (await postFormRaw("/component/querydangqianzc", {})).trim();
  462. if (/^\d+$/.test(text)) {
  463. return Number(text);
  464. }
  465. } catch (error) {
  466. // 学期未开始时该接口返回空内容
  467. }
  468. return null;
  469. }
  470. // 本周周一往前推 (当前周 - 1) 周,即为第 1 周周一
  471. function computeSemesterStart(currentWeek) {
  472. if (!currentWeek || currentWeek < 1) {
  473. return "";
  474. }
  475. const now = new Date();
  476. const monday = new Date(
  477. now.getFullYear(),
  478. now.getMonth(),
  479. now.getDate() - ((now.getDay() + 6) % 7)
  480. );
  481. monday.setDate(monday.getDate() - (currentWeek - 1) * 7);
  482. return formatDate(monday);
  483. }
  484. async function buildCourseConfig(semester, current, courses) {
  485. const maxWeek = courses.reduce(function (max, course) {
  486. return Math.max(max, course.weeks[course.weeks.length - 1] || 0);
  487. }, 0);
  488. const config = Object.assign({}, CONFIG_BASE, {
  489. semesterTotalWeeks: Math.max(CONFIG_BASE.semesterTotalWeeks, maxWeek)
  490. });
  491. const isCurrent = semester && current && semester.key === `${current.xn}-${current.xq}`;
  492. let startDate = isCurrent ? computeSemesterStart(await fetchCurrentWeek()) : "";
  493. if (!startDate) {
  494. startDate = await askSemesterStartDate(computeSemesterStart(1));
  495. if (!startDate) {
  496. return null;
  497. }
  498. }
  499. config.semesterStartDate = startDate;
  500. return config;
  501. }
  502. // ========================================
  503. // 工具函数
  504. // ========================================
  505. function firstText() {
  506. for (let index = 0; index < arguments.length; index += 1) {
  507. const value = arguments[index];
  508. if (value !== null && value !== undefined && String(value).trim() !== "") {
  509. return String(value).trim();
  510. }
  511. }
  512. return "";
  513. }
  514. // 去掉一对包裹的中括号:[兰美荣] -> 兰美荣
  515. function unwrap(value) {
  516. return String(value || "").trim()
  517. .replace(/^\[/, "")
  518. .replace(/\]$/, "")
  519. .trim();
  520. }
  521. // 提取一行里所有 [xxx] 的内容
  522. function bracketGroups(line) {
  523. const groups = [];
  524. const pattern = /\[([^\]]*)\]/g;
  525. let matched = pattern.exec(String(line));
  526. while (matched !== null) {
  527. groups.push(matched[1].trim());
  528. matched = pattern.exec(String(line));
  529. }
  530. return groups;
  531. }
  532. function firstMatch(list, predicate) {
  533. for (let index = 0; index < list.length; index += 1) {
  534. if (predicate(list[index])) {
  535. return list[index];
  536. }
  537. }
  538. return null;
  539. }
  540. function uniqueSorted(numbers) {
  541. const seen = {};
  542. const result = [];
  543. numbers.forEach(function (number) {
  544. if (!seen[number]) {
  545. seen[number] = true;
  546. result.push(number);
  547. }
  548. });
  549. return result.sort(function (a, b) {
  550. return a - b;
  551. });
  552. }
  553. function toNumber(value) {
  554. if (value === null || value === undefined || value === "") {
  555. return null;
  556. }
  557. const number = Number(value);
  558. return Number.isFinite(number) && number > 0 ? number : null;
  559. }
  560. function normalizeTime(value) {
  561. const text = String(value || "").trim();
  562. return /^\d{1,2}:\d{2}$/.test(text) ? text : "";
  563. }
  564. function formatDate(date) {
  565. const month = String(date.getMonth() + 1).padStart(2, "0");
  566. const day = String(date.getDate()).padStart(2, "0");
  567. return `${date.getFullYear()}-${month}-${day}`;
  568. }
  569. function toast(message) {
  570. if (window.shiguangBridge && typeof window.shiguangBridge.showToast === "function") {
  571. window.shiguangBridge.showToast(message);
  572. }
  573. }
  574. // ========================================
  575. // 主流程
  576. // ========================================
  577. async function runImportFlow() {
  578. try {
  579. if (!window.shiguangBridgePromise) {
  580. throw new Error("当前环境不支持导入,请在时光课程表 App 内运行。");
  581. }
  582. const confirmed = await confirmStart();
  583. if (!confirmed) {
  584. toast("已取消导入。");
  585. return;
  586. }
  587. toast("正在获取学期信息...");
  588. const current = await fetchCurrentSemester();
  589. const options = mergeCurrentSemester(await fetchSemesterOptions(), current);
  590. const semester = await selectSemester(options, current);
  591. if (!semester) {
  592. toast("已取消导入。");
  593. return;
  594. }
  595. toast("正在获取课表...");
  596. const courses = await fetchCourses(semester.xn, semester.xq);
  597. if (!courses.length) {
  598. throw new Error("未解析到任何课程,请确认该学期课表已发布。");
  599. }
  600. const timeSlots = await fetchTimeSlots(semester.xn, semester.xq);
  601. const config = await buildCourseConfig(semester, current, courses);
  602. if (!config) {
  603. toast("已取消导入。");
  604. return;
  605. }
  606. toast(`正在导入 ${courses.length} 门课程...`);
  607. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  608. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  609. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  610. toast(`成功导入 ${courses.length} 门课程!`);
  611. if (window.shiguangBridge && typeof window.shiguangBridge.notifyTaskCompletion === "function") {
  612. window.shiguangBridge.notifyTaskCompletion();
  613. }
  614. } catch (error) {
  615. toast(`导入失败:${error.message}`);
  616. }
  617. }
  618. runImportFlow();