|
|
@@ -0,0 +1,532 @@
|
|
|
+// 肇庆学院(zqu.edu.cn) 拾光课程表适配脚本
|
|
|
+// 教务系统:URP 系(Struts2,形如 xxx!yyy.action)
|
|
|
+// 适用范围:本脚本为【WebVPN 校外访问】版本;校内直连版本另行提交
|
|
|
+// 维护者:NoobLLiu
|
|
|
+//
|
|
|
+// 课表数据直接取自后台 JSON 接口,不做 HTML 表格解析:
|
|
|
+// GET xsgrkbcx!getKbRq.action?xnxqdm=<学年学期>&zc=<周次>&xsdm=
|
|
|
+// zc 传空即不限周次,一次可取回整学期排课记录;每条记录自带 zc 字段
|
|
|
+// 标明该门课的上课周次,因此无需逐周请求,也无需读取页面 DOM。
|
|
|
+//
|
|
|
+// 使用前提:用户已在弹窗打开的网页中登录 WebVPN 并进入教务系统,
|
|
|
+// 停留在教务系统内的任意页面即可,无需打开课表查询页。
|
|
|
+
|
|
|
+// ---------------------------------------------------------------- 常量
|
|
|
+
|
|
|
+// 「我的课表」内容页,本脚本只用它取服务端渲染的学期下拉框 #xnxqdm
|
|
|
+const KB_LIST_PAGE = 'xsgrkbcx!getXsgrbkList.action';
|
|
|
+// 课表数据接口(返回 JSON)
|
|
|
+const KB_DATA = 'xsgrkbcx!getKbRq.action';
|
|
|
+
|
|
|
+// 肇庆学院作息时间(每天 14 节),依据学校实际作息表。
|
|
|
+// 教务系统不提供作息数据,导入时按此预设写入。
|
|
|
+const PRESET_TIME_SLOTS = [
|
|
|
+ { number: 1, startTime: '08:00', endTime: '08:40' },
|
|
|
+ { number: 2, startTime: '08:50', endTime: '09:30' },
|
|
|
+ { number: 3, startTime: '09:50', endTime: '10:30' },
|
|
|
+ { number: 4, startTime: '10:40', endTime: '11:20' },
|
|
|
+ { number: 5, startTime: '11:30', endTime: '12:10' },
|
|
|
+ { number: 6, startTime: '14:30', endTime: '15:10' },
|
|
|
+ { number: 7, startTime: '15:20', endTime: '16:00' },
|
|
|
+ { number: 8, startTime: '16:15', endTime: '16:55' },
|
|
|
+ { number: 9, startTime: '17:05', endTime: '17:45' },
|
|
|
+ { number: 10, startTime: '17:55', endTime: '18:35' },
|
|
|
+ { number: 11, startTime: '19:00', endTime: '19:40' },
|
|
|
+ { number: 12, startTime: '19:50', endTime: '20:30' },
|
|
|
+ { number: 13, startTime: '20:40', endTime: '21:20' },
|
|
|
+ { number: 14, startTime: '21:30', endTime: '22:10' }
|
|
|
+];
|
|
|
+
|
|
|
+// 教室字段为空时的占位文本(教务系统尚未给出教室信息)
|
|
|
+const UNKNOWN_POSITION = '未获取到教室';
|
|
|
+
|
|
|
+// ---------------------------------------------------------------- 工具函数
|
|
|
+
|
|
|
+/**
|
|
|
+ * 推算教务系统的站点根路径。
|
|
|
+ *
|
|
|
+ * WebVPN 下页面地址形如:
|
|
|
+ * https://webvpn.zqu.edu.cn/https/<加密串>/xsgrkbcx!getXsgrbkList.action
|
|
|
+ * 校内直连下形如:
|
|
|
+ * https://jwgl.zqu.edu.cn/xsgrkbcx!getXsgrbkList.action
|
|
|
+ * 取到 <加密串>/ 这一层,后续所有接口都基于它拼接。
|
|
|
+ */
|
|
|
+function siteBase() {
|
|
|
+ try {
|
|
|
+ const u = new URL(window.location.href);
|
|
|
+ const path = u.pathname;
|
|
|
+ // 本脚本只请求 xsgrkbcx! 下两个接口,取该段之前的一段即为站点基址。
|
|
|
+ // 停留在别的页面时匹配不到,退回按最后一段斜杠截断,效果相同。
|
|
|
+ const i = path.indexOf('xsgrkbcx!');
|
|
|
+ if (i > 0) return u.origin + path.slice(0, i);
|
|
|
+ const j = path.lastIndexOf('/');
|
|
|
+ return u.origin + (j > 0 ? path.slice(0, j + 1) : '/');
|
|
|
+ } catch (e) {
|
|
|
+ const href = window.location.href.split(/[?#]/)[0];
|
|
|
+ const i = href.lastIndexOf('/');
|
|
|
+ return i > 0 ? href.slice(0, i + 1) : href;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 读取某个 document 里的下拉框选项(跳过空值项)。
|
|
|
+ * 目前只用于解析学期下拉框 #xnxqdm。
|
|
|
+ * @returns {{cur: string, opts: Array<{value: string, text: string}>}|null}
|
|
|
+ */
|
|
|
+function readSelectOptions(doc, id) {
|
|
|
+ let sel;
|
|
|
+ try {
|
|
|
+ sel = doc.querySelector('#' + id);
|
|
|
+ } catch (e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (!sel || !sel.options) return null;
|
|
|
+
|
|
|
+ const opts = [];
|
|
|
+ for (let i = 0; i < sel.options.length; i++) {
|
|
|
+ const v = String(sel.options[i].value).trim();
|
|
|
+ if (v === '') continue;
|
|
|
+ opts.push({ value: v, text: (sel.options[i].textContent || '').trim() });
|
|
|
+ }
|
|
|
+ if (opts.length === 0) return null;
|
|
|
+ return { cur: String(sel.value).trim(), opts: opts };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 解析节次字段,返回节号数组。
|
|
|
+ * jcdm2 形如 "01,02,03";jcdm 形如 "0102"(连写)。
|
|
|
+ */
|
|
|
+function parseSections(jcdm2, jcdm) {
|
|
|
+ const txt = String(jcdm2 || jcdm || '').trim();
|
|
|
+ const nums = [];
|
|
|
+ const push = n => { if (n > 0 && nums.indexOf(n) === -1) nums.push(n); };
|
|
|
+ for (const raw of txt.split(/[,\s]+/)) {
|
|
|
+ const part = raw.trim();
|
|
|
+ if (!part || !/^\d+$/.test(part)) continue;
|
|
|
+ if (part.length <= 2) {
|
|
|
+ push(parseInt(part, 10));
|
|
|
+ } else {
|
|
|
+ for (let i = 0; i < part.length; i += 2) {
|
|
|
+ push(parseInt(part.slice(i, i + 2), 10));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nums.sort((a, b) => a - b);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 解析课程记录自带的周次字段,返回周号数组。
|
|
|
+ *
|
|
|
+ * 教务系统在不同部署下 zc 字段有多种写法,故一并兼容:
|
|
|
+ * "1-8,10-17周" 区间(URP 系最常见)
|
|
|
+ * "1,3,5周" 逗号分隔
|
|
|
+ * "1111111100000000" 位串(正方形教务 classWeek 写法,第 i 位为 1 表示第 i+1 周)
|
|
|
+ * "1-16(单)" 带单双周后缀
|
|
|
+ * 返回值已去重并升序。
|
|
|
+ */
|
|
|
+function parseWeekText(zc) {
|
|
|
+ const txt = String(zc === null || zc === undefined ? '' : zc).trim();
|
|
|
+ if (!txt) return [];
|
|
|
+
|
|
|
+ // 位串:只由 0/1 组成且长度 >= 4,视为「第 i+1 周」标记
|
|
|
+ if (/^[01]{4,}$/.test(txt)) {
|
|
|
+ const out = [];
|
|
|
+ for (let i = 0; i < txt.length; i++) {
|
|
|
+ if (txt[i] === '1') out.push(i + 1);
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 区间 / 列表:先去掉「周、单、双、(单)、(双)」等修饰再切分
|
|
|
+ const cleaned = txt
|
|
|
+ .replace(/周|星期/g, '')
|
|
|
+ .replace(/[((][^))]*[))]/g, '')
|
|
|
+ .replace(/[单双]/g, '');
|
|
|
+ const out = [];
|
|
|
+ for (const raw of cleaned.split(/[,,\s]+/)) {
|
|
|
+ const part = raw.trim();
|
|
|
+ if (!part) continue;
|
|
|
+ const range = /^(\d+)\s*-\s*(\d+)$/.exec(part);
|
|
|
+ if (range) {
|
|
|
+ const a = parseInt(range[1], 10);
|
|
|
+ const b = parseInt(range[2], 10);
|
|
|
+ if (a > 0 && b >= a) {
|
|
|
+ for (let i = a; i <= b; i++) out.push(i);
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (/^\d+$/.test(part)) {
|
|
|
+ const n = parseInt(part, 10);
|
|
|
+ if (n > 0) out.push(n);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return Array.from(new Set(out)).sort((a, b) => a - b);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 节次与周次合并去重函数(摘自拾光课程表官方 wiki 的参考实现)。
|
|
|
+ */
|
|
|
+function mergeAndDistinctCourses(courses) {
|
|
|
+ if (!Array.isArray(courses) || courses.length <= 1) return courses;
|
|
|
+
|
|
|
+ const list = courses.map(c => ({
|
|
|
+ ...c,
|
|
|
+ name: c.name || '',
|
|
|
+ teacher: c.teacher || '',
|
|
|
+ position: c.position || '',
|
|
|
+ weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 阶段 1:合并连续节次与完全重复记录
|
|
|
+ list.sort((a, b) => {
|
|
|
+ return a.name.localeCompare(b.name) ||
|
|
|
+ a.teacher.localeCompare(b.teacher) ||
|
|
|
+ a.position.localeCompare(b.position) ||
|
|
|
+ (a.day || 0) - (b.day || 0) ||
|
|
|
+ a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
|
|
|
+ (a.startSection || 0) - (b.startSection || 0);
|
|
|
+ });
|
|
|
+
|
|
|
+ const step1Merged = [];
|
|
|
+ let current = list[0];
|
|
|
+
|
|
|
+ for (let i = 1; i < list.length; i++) {
|
|
|
+ const next = list[i];
|
|
|
+
|
|
|
+ const isSameCourseAndWeeks =
|
|
|
+ current.name === next.name &&
|
|
|
+ current.teacher === next.teacher &&
|
|
|
+ current.position === next.position &&
|
|
|
+ current.day === next.day &&
|
|
|
+ current.weeks.join(',') === next.weeks.join(',');
|
|
|
+
|
|
|
+ const isContinuous = current.endSection + 1 === next.startSection;
|
|
|
+ const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
|
|
|
+
|
|
|
+ if (isSameCourseAndWeeks && isContinuous) {
|
|
|
+ current.endSection = next.endSection;
|
|
|
+ } else if (isSameCourseAndWeeks && isDuplicate) {
|
|
|
+ continue;
|
|
|
+ } else {
|
|
|
+ step1Merged.push(current);
|
|
|
+ current = next;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ step1Merged.push(current);
|
|
|
+
|
|
|
+ // 阶段 2:合并同节次的周次
|
|
|
+ step1Merged.sort((a, b) => {
|
|
|
+ return a.name.localeCompare(b.name) ||
|
|
|
+ a.teacher.localeCompare(b.teacher) ||
|
|
|
+ a.position.localeCompare(b.position) ||
|
|
|
+ (a.day || 0) - (b.day || 0) ||
|
|
|
+ (a.startSection || 0) - (b.startSection || 0) ||
|
|
|
+ (a.endSection || 0) - (b.endSection || 0);
|
|
|
+ });
|
|
|
+
|
|
|
+ const step2Merged = [];
|
|
|
+ let cur = step1Merged[0];
|
|
|
+
|
|
|
+ for (let i = 1; i < step1Merged.length; i++) {
|
|
|
+ const nxt = step1Merged[i];
|
|
|
+
|
|
|
+ const isSameCourseAndSection =
|
|
|
+ cur.name === nxt.name &&
|
|
|
+ cur.teacher === nxt.teacher &&
|
|
|
+ cur.position === nxt.position &&
|
|
|
+ cur.day === nxt.day &&
|
|
|
+ cur.startSection === nxt.startSection &&
|
|
|
+ cur.endSection === nxt.endSection;
|
|
|
+
|
|
|
+ if (isSameCourseAndSection) {
|
|
|
+ cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
|
|
|
+ } else {
|
|
|
+ step2Merged.push(cur);
|
|
|
+ cur = nxt;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ step2Merged.push(cur);
|
|
|
+
|
|
|
+ return step2Merged;
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------- 数据获取
|
|
|
+
|
|
|
+/**
|
|
|
+ * 读取学期下拉框。
|
|
|
+ *
|
|
|
+ * 学期下拉框由服务端随页面一起下发,所以请求一次内容页并解析返回的 HTML 即可。
|
|
|
+ */
|
|
|
+async function fetchTermOptions(base) {
|
|
|
+ const resp = await fetch(base + KB_LIST_PAGE, { method: 'GET', credentials: 'include' });
|
|
|
+ if (!resp.ok) throw new Error('学期列表请求失败(HTTP ' + resp.status + ')');
|
|
|
+
|
|
|
+ const html = await resp.text();
|
|
|
+ const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
|
+ const terms = readSelectOptions(doc, 'xnxqdm');
|
|
|
+ if (!terms) throw new Error('未找到学期列表,请确认已登录并进入教务系统');
|
|
|
+ return terms;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 请求整个学期的课表原始数据(一次请求)。
|
|
|
+ *
|
|
|
+ * zc 传空字符串表示不限周次,服务端会返回整学期数据,
|
|
|
+ * 每条记录自带的 zc 字段即该门课上课的周次,无需逐周请求。
|
|
|
+ *
|
|
|
+ * 注意:不限周次时服务端不返回日期数组,学期开始日期需另行获取(见 fetchFirstWeekStart)。
|
|
|
+ * @returns {Array|null} 课程记录数组,取不到返回 null
|
|
|
+ */
|
|
|
+async function fetchTermData(base, termCode) {
|
|
|
+ const url = base + KB_DATA +
|
|
|
+ '?xnxqdm=' + encodeURIComponent(termCode) +
|
|
|
+ '&zc=&xsdm=';
|
|
|
+
|
|
|
+ const resp = await fetch(url, {
|
|
|
+ method: 'GET',
|
|
|
+ credentials: 'include',
|
|
|
+ headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
|
+ });
|
|
|
+ if (!resp.ok) return null;
|
|
|
+
|
|
|
+ const text = await resp.text();
|
|
|
+ let data;
|
|
|
+ try {
|
|
|
+ data = JSON.parse(text);
|
|
|
+ } catch (e) {
|
|
|
+ // 未登录/被拦截时会返回 HTML,交由上层统一提示
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (!Array.isArray(data) || data.length < 2) return null;
|
|
|
+ return data[0] || [];
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 请求第 1 周的课表,取其中的周一日期作为学期开始日期。
|
|
|
+ *
|
|
|
+ * 不限周次的那次请求不返回日期数组,所以这里单独取一次第 1 周 ——
|
|
|
+ * 一次请求即可确定开学日期,无需逐周探测。
|
|
|
+ * @returns {string|null} "YYYY-MM-DD",取不到返回 null
|
|
|
+ */
|
|
|
+async function fetchFirstWeekStart(base, termCode) {
|
|
|
+ const url = base + KB_DATA +
|
|
|
+ '?xnxqdm=' + encodeURIComponent(termCode) +
|
|
|
+ '&zc=1&xsdm=';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const resp = await fetch(url, {
|
|
|
+ method: 'GET',
|
|
|
+ credentials: 'include',
|
|
|
+ headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
|
+ });
|
|
|
+ if (!resp.ok) return null;
|
|
|
+
|
|
|
+ const data = JSON.parse(await resp.text());
|
|
|
+ if (!Array.isArray(data) || data.length < 2) return null;
|
|
|
+
|
|
|
+ // dates 里取星期一的日期
|
|
|
+ for (const d of (data[1] || [])) {
|
|
|
+ if (!d) continue;
|
|
|
+ if (parseInt(d.xqmc, 10) !== 1) continue;
|
|
|
+ const rq = String(d.rq || '').trim();
|
|
|
+ if (/^\d{4}-\d{2}-\d{2}$/.test(rq)) return rq;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ } catch (e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 解析整学期数据,聚合成拾光课程表需要的课程数组。
|
|
|
+ *
|
|
|
+ * 服务端一次返回整学期的排课记录,每条自带 zc(该门课上课的周次),
|
|
|
+ * 因此这里按「课程名+教师+教室+星期+起止节次」聚合出 weeks 数组,
|
|
|
+ * 同一门课的多个周次会归并到同一条记录。
|
|
|
+ */
|
|
|
+function collectCourses(records) {
|
|
|
+ const courses = [];
|
|
|
+
|
|
|
+ for (const c of records) {
|
|
|
+ if (!c || typeof c !== 'object') continue;
|
|
|
+
|
|
|
+ const day = parseInt(c.xq, 10);
|
|
|
+ if (!Number.isInteger(day) || day < 1 || day > 7) continue;
|
|
|
+
|
|
|
+ const name = String(c.kcmc || '').trim();
|
|
|
+ if (!name) continue;
|
|
|
+
|
|
|
+ const sections = parseSections(c.jcdm2, c.jcdm);
|
|
|
+ if (sections.length === 0) continue;
|
|
|
+
|
|
|
+ const weeks = parseWeekText(c.zc);
|
|
|
+ if (weeks.length === 0) continue;
|
|
|
+
|
|
|
+ const teacher = String(c.teaxms || '').trim();
|
|
|
+ const position = String(c.jxcdmc || '').trim() || UNKNOWN_POSITION;
|
|
|
+ const startSection = sections[0];
|
|
|
+ const endSection = sections[sections.length - 1];
|
|
|
+
|
|
|
+ // 同一门课同一时段可能有多条记录(不同周次区间),按周次归并
|
|
|
+ const exist = courses.find(x =>
|
|
|
+ x.name === name && x.teacher === teacher && x.position === position &&
|
|
|
+ x.day === day && x.startSection === startSection && x.endSection === endSection
|
|
|
+ );
|
|
|
+ if (exist) {
|
|
|
+ exist.weeks = Array.from(new Set([...exist.weeks, ...weeks])).sort((a, b) => a - b);
|
|
|
+ } else {
|
|
|
+ courses.push({
|
|
|
+ name: name,
|
|
|
+ teacher: teacher,
|
|
|
+ position: position,
|
|
|
+ day: day,
|
|
|
+ startSection: startSection,
|
|
|
+ endSection: endSection,
|
|
|
+ weeks: weeks
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 总周数取所有课程周次的最大值;一条都取不到时保持 0,交给 App 默认值。
|
|
|
+ // 这里用 reduce 而非 Math.max(...spread),避免周次过多时参数展开爆栈。
|
|
|
+ const totalWeeks = courses.reduce(
|
|
|
+ (max, c) => c.weeks.reduce((m, w) => (w > m ? w : m), max),
|
|
|
+ 0
|
|
|
+ );
|
|
|
+
|
|
|
+ return {
|
|
|
+ courses: mergeAndDistinctCourses(courses),
|
|
|
+ totalWeeks: totalWeeks
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------- 交互
|
|
|
+
|
|
|
+async function promptUserToStart() {
|
|
|
+ return await window.shiguangBridgePromise.showAlert(
|
|
|
+ '肇庆学院课表导入',
|
|
|
+ '导入前请确认:\n1) 已在当前网页登录 WebVPN;\n2) 已进入教务系统(停留在任意页面即可,无需打开课表查询)。',
|
|
|
+ '开始导入'
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 开学日期输入框的校验函数(须挂在全局作用域,供原生侧调用)。
|
|
|
+ * 返回 false 表示通过,返回字符串表示错误提示。
|
|
|
+ */
|
|
|
+function validateDate(input) {
|
|
|
+ if (!input || !input.trim()) return '请输入开学日期';
|
|
|
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(input.trim())) return '格式应为 YYYY-MM-DD,例如 2026-08-31';
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 让用户确认(必要时修正)开学日期。
|
|
|
+ * 自动取到的值作为默认值;若未能取到,则由用户手动输入。
|
|
|
+ */
|
|
|
+async function confirmStartDate(defaultDate) {
|
|
|
+ const tip = defaultDate
|
|
|
+ ? '请确认本学期第 1 周的周一日期(如有误可直接修改):'
|
|
|
+ : '未能自动取到开学日期,请手动输入本学期第 1 周的周一日期:';
|
|
|
+ return await window.shiguangBridgePromise.showPrompt(
|
|
|
+ '确认开学日期',
|
|
|
+ tip,
|
|
|
+ defaultDate || '',
|
|
|
+ 'validateDate'
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+async function selectTerm(terms) {
|
|
|
+ const texts = terms.opts.map(o => o.text);
|
|
|
+ let defIdx = terms.opts.findIndex(o => o.value === terms.cur);
|
|
|
+ if (defIdx < 0) defIdx = 0;
|
|
|
+
|
|
|
+ const idx = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
+ '选择学期', JSON.stringify(texts), defIdx);
|
|
|
+ if (idx === null || idx < 0 || idx >= terms.opts.length) return null;
|
|
|
+ return terms.opts[idx];
|
|
|
+}
|
|
|
+
|
|
|
+async function importPresetTimeSlots(timeSlots) {
|
|
|
+ try {
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
+ } catch (error) {
|
|
|
+ window.shiguangBridge.showToast('导入时间段失败: ' + error.message);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ---------------------------------------------------------------- 主流程
|
|
|
+
|
|
|
+async function runImportFlow() {
|
|
|
+ const confirmed = await promptUserToStart();
|
|
|
+ if (!confirmed) {
|
|
|
+ window.shiguangBridge.showToast('用户取消了导入。');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const base = siteBase();
|
|
|
+
|
|
|
+ let terms;
|
|
|
+ try {
|
|
|
+ window.shiguangBridge.showToast('正在读取学期列表 ...');
|
|
|
+ terms = await fetchTermOptions(base);
|
|
|
+ } catch (error) {
|
|
|
+ window.shiguangBridge.showToast('读取学期失败:' + error.message);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const term = await selectTerm(terms);
|
|
|
+ if (term === null) {
|
|
|
+ window.shiguangBridge.showToast('未选择学期,导入流程终止。');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ window.shiguangBridge.showToast('正在读取课表 ...');
|
|
|
+ const records = await fetchTermData(base, term.value);
|
|
|
+ if (!records) {
|
|
|
+ window.shiguangBridge.showToast('课表接口未返回数据,请检查登录状态或稍后重试。');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = collectCourses(records);
|
|
|
+ if (result.courses.length === 0) {
|
|
|
+ window.shiguangBridge.showToast('未获取到课表数据,请确认该学期已选课。');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 开学日期:额外取一次第 1 周的周一日期,再让用户确认/修正。
|
|
|
+ // 取不到也不阻塞流程,用户可自行输入。
|
|
|
+ window.shiguangBridge.showToast('正在确认开学日期 ...');
|
|
|
+ const startDate = await fetchFirstWeekStart(base, term.value);
|
|
|
+
|
|
|
+ const input = await confirmStartDate(startDate);
|
|
|
+ if (input === null) {
|
|
|
+ window.shiguangBridge.showToast('用户取消了导入。');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const semesterStartDate = String(input).trim();
|
|
|
+
|
|
|
+ try {
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(result.courses));
|
|
|
+ } catch (error) {
|
|
|
+ window.shiguangBridge.showToast('课程保存失败: ' + error.message);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 只在取到值时才写这两个可选字段,否则交给 App 侧沿用已有设置
|
|
|
+ const config = {};
|
|
|
+ if (result.totalWeeks > 0) config.semesterTotalWeeks = result.totalWeeks;
|
|
|
+ if (semesterStartDate) config.semesterStartDate = semesterStartDate;
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
|
|
|
+ } catch (error) {
|
|
|
+ window.shiguangBridge.showToast('课表配置保存失败: ' + error.message);
|
|
|
+ }
|
|
|
+
|
|
|
+ await importPresetTimeSlots(PRESET_TIME_SLOTS);
|
|
|
+
|
|
|
+ window.shiguangBridge.showToast('导入完成,共 ' + result.courses.length + ' 个课程时段。');
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
+}
|
|
|
+
|
|
|
+runImportFlow();
|