|
@@ -0,0 +1,445 @@
|
|
|
|
|
+// 北京师范大学珠海校区(金智/青果 KINGOSOFT 教务)拾光课程表适配脚本
|
|
|
|
|
+//
|
|
|
|
|
+// 方案参考:wiki《常见教务系统适配案例参考》青果教务案例(resources/WZZY),
|
|
|
|
|
+// 全部数据通过带会话的网络请求获取,不依赖课表页面 DOM,支持开学日期导入。
|
|
|
|
|
+//
|
|
|
|
|
+// 本校实测接口:
|
|
|
|
|
+// 学期列表 POST /frame/droplist/getDropLists.action
|
|
|
|
|
+// body: comboBoxName=Ms_KBBP_FBXKJGXNXQ&...(参数见请求体)
|
|
|
|
|
+// → JSON [{"code":"2026,0","name":"2026-2027学年秋季学期"},...]
|
|
|
|
|
+// 注:本校 code 以逗号分隔(案例为 "-"),解析时已区分
|
|
|
|
|
+// 课表数据 GET /wsxk/xkjg.ckdgxsxdkchj_data10319.jsp?params=<base64(xn=..&xq=..)>
|
|
|
|
|
+// → GBK 编码 HTML 列表表格(网络数据源,非课表页面)
|
|
|
|
|
+// 注:案例使用 student/wsxk.xskcb10319.jsp(周×节矩阵视图),
|
|
|
|
|
+// 本校同族接口的列表视图字段更稳定,故用本接口,数据同源
|
|
|
|
|
+// 开学日期 POST /frame/desk/showLessonScheduleInfosV14.action?xn=..&xq=..&jxz=1
|
|
|
|
|
+// → 第一周课表 HTML,表头形如 <label>一</label><br>09-07,
|
|
|
|
|
+// 正则与青果案例一致;年份按学期推算(春季学期为 xn+1)
|
|
|
|
|
+// 作息时间 GET /public/SchoolTimetable.show.jsp → 制表符分隔节次表
|
|
|
|
|
+// (案例为写死作息,本校教务提供公布接口,优先动态获取)
|
|
|
|
|
+// 当前学期 POST /jw/common/showYearTerm.action(getDropLists 失败时的回退默认值)
|
|
|
|
|
+//
|
|
|
|
|
+// 学年学期编码:xn=学年前年份(2026 即 2026-2027 学年),xq=0 秋季 / 1 春季
|
|
|
|
|
+
|
|
|
|
|
+const HOST = 'https://jwxt.bnuzh.edu.cn';
|
|
|
|
|
+const DROPLIST_URL = HOST + '/frame/droplist/getDropLists.action';
|
|
|
|
|
+const YEARTERM_URL = HOST + '/jw/common/showYearTerm.action';
|
|
|
|
|
+const KB_DATA_URL = HOST + '/wsxk/xkjg.ckdgxsxdkchj_data10319.jsp';
|
|
|
|
|
+const LESSON_INFO_URL = HOST + '/frame/desk/showLessonScheduleInfosV14.action';
|
|
|
|
|
+const TIMETABLE_URL = HOST + '/public/SchoolTimetable.show.jsp';
|
|
|
|
|
+
|
|
|
|
|
+const DROPLIST_BODY =
|
|
|
|
|
+ 'comboBoxName=Ms_KBBP_FBXKJGXNXQ¶mValue=&isYXB=0&isCDDW=0&isPYCC=0' +
|
|
|
|
|
+ '&isPYCCNJ=0&zyPyccFiled=&isKBLB=0&isXQ=0&isBJ=0&isSXJD=0&isDJKSLB=0';
|
|
|
|
|
+
|
|
|
|
|
+const DAY_MAP = { '一': 1, '二': 2, '三': 3, '四': 4, '五': 5, '六': 6, '日': 7, '天': 7 };
|
|
|
|
|
+
|
|
|
|
|
+// 教务未公布作息时的兜底(2026-2027 学年秋季学期)
|
|
|
|
|
+const FALLBACK_TIME_SLOTS = [
|
|
|
|
|
+ { number: 1, startTime: '08:00', endTime: '08:45' },
|
|
|
|
|
+ { number: 2, startTime: '08:55', endTime: '09:40' },
|
|
|
|
|
+ { number: 3, startTime: '10:00', endTime: '10:45' },
|
|
|
|
|
+ { number: 4, startTime: '10:55', endTime: '11:40' },
|
|
|
|
|
+ { number: 5, startTime: '13:30', endTime: '14:15' },
|
|
|
|
|
+ { number: 6, startTime: '14:25', endTime: '15:10' },
|
|
|
|
|
+ { number: 7, startTime: '15:30', endTime: '16:15' },
|
|
|
|
|
+ { number: 8, startTime: '16:25', endTime: '17:10' },
|
|
|
|
|
+ { number: 9, startTime: '18:00', endTime: '18:45' },
|
|
|
|
|
+ { number: 10, startTime: '18:55', endTime: '19:40' },
|
|
|
|
|
+ { number: 11, startTime: '19:50', endTime: '20:35' },
|
|
|
|
|
+ { number: 12, startTime: '20:45', endTime: '21:30' }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+function validateYearInput(input) {
|
|
|
|
|
+ if (/^\d{4}$/.test(String(input).trim())) return false;
|
|
|
|
|
+ return '请输入四位数字学年,如 2026';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function validateDateInput(input) {
|
|
|
|
|
+ if (/^\d{4}-\d{2}-\d{2}$/.test(String(input).trim())) return false;
|
|
|
|
|
+ return '请输入正确格式的日期,如 2026-09-07';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function fetchGbk(url, options) {
|
|
|
|
|
+ const resp = await fetch(url, options);
|
|
|
|
|
+ return new TextDecoder('gbk').decode(await resp.arrayBuffer());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 学期 ----------
|
|
|
|
|
+
|
|
|
|
|
+// 案例同款:网络请求获取学期列表(含默认值逻辑,与课表页面解耦)
|
|
|
|
|
+async function fetchSemesterList() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const resp = await fetch(DROPLIST_URL, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
|
|
|
+ 'X-Requested-With': 'XMLHttpRequest'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: DROPLIST_BODY,
|
|
|
|
|
+ credentials: 'include'
|
|
|
|
|
+ });
|
|
|
|
|
+ const list = await resp.json();
|
|
|
|
|
+ if (Array.isArray(list) && list.length > 0 && list[0].code) return list;
|
|
|
|
|
+ } catch (e) { /* 回退到默认值方案 */ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// getDropLists 不可用时:以教务返回的当前学期为默认值生成候选列表
|
|
|
|
|
+function buildSemesterList(current) {
|
|
|
|
|
+ const list = [];
|
|
|
|
|
+ let xn = parseInt(current.xn, 10);
|
|
|
|
|
+ let xq = current.xq === '1' ? 1 : 0;
|
|
|
|
|
+ for (let i = 0; i < 8; i++) {
|
|
|
|
|
+ list.push({
|
|
|
|
|
+ code: xn + ',' + xq,
|
|
|
|
|
+ name: xn + '-' + (xn + 1) + '学年' + (xq === 0 ? '秋季' : '春季') + '学期'
|
|
|
|
|
+ });
|
|
|
|
|
+ if (xq === 0) { xn -= 1; xq = 1; } else { xq = 0; }
|
|
|
|
|
+ }
|
|
|
|
|
+ return list;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function selectSemester() {
|
|
|
|
|
+ // 当前学期作为默认选中值(案例 dqxnxq.do 的默认值逻辑,本校由 showYearTerm 提供)
|
|
|
|
|
+ let current = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const resp = await fetch(YEARTERM_URL, { method: 'POST' });
|
|
|
|
|
+ const data = JSON.parse(await resp.text());
|
|
|
|
|
+ if (data && data.xn) current = { xn: data.xn, xq: String(data.xqM) };
|
|
|
|
|
+ } catch (e) { /* 列表仍可用时不影响主流程 */ }
|
|
|
|
|
+
|
|
|
|
|
+ let list = await fetchSemesterList();
|
|
|
|
|
+ if (!list && current) list = buildSemesterList(current);
|
|
|
|
|
+
|
|
|
|
|
+ if (!list) {
|
|
|
|
|
+ // 列表与当前学期均不可得时,手动输入
|
|
|
|
|
+ const year = await window.shiguangBridgePromise.showPrompt(
|
|
|
|
|
+ '选择学年', '请输入学年的前年份,如 2026 表示 2026-2027 学年:', '2026', 'validateYearInput'
|
|
|
|
|
+ );
|
|
|
|
|
+ if (year === null) return null;
|
|
|
|
|
+ const xq = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
|
|
+ '选择学期', JSON.stringify(['秋季学期', '春季学期']), 0
|
|
|
|
|
+ );
|
|
|
|
|
+ if (xq === null) return null;
|
|
|
|
|
+ return { xn: String(year).trim(), xq: String(xq) };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let defaultIndex = 0;
|
|
|
|
|
+ if (current) {
|
|
|
|
|
+ const idx = list.findIndex(
|
|
|
|
|
+ item => String(item.code).replace(/\s/g, '') === current.xn + ',' + current.xq
|
|
|
|
|
+ );
|
|
|
|
|
+ if (idx >= 0) defaultIndex = idx;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const names = list.slice(0, 10).map(item => item.name);
|
|
|
|
|
+ const index = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
|
|
+ '选择导入学期', JSON.stringify(names), defaultIndex
|
|
|
|
|
+ );
|
|
|
|
|
+ if (index === null || index < 0 || index >= list.length) return null;
|
|
|
|
|
+ // 本校 code 形如 "2026,0"(逗号),案例为 "2026-0",两种分隔符都兼容
|
|
|
|
|
+ const parts = String(list[index].code).split(/[,\-]/);
|
|
|
|
|
+ const xn = parts[0].trim();
|
|
|
|
|
+ const xq = (parts[1] || '0').trim();
|
|
|
|
|
+ if (!/^\d{4}$/.test(xn)) return null;
|
|
|
|
|
+ return { xn, xq };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 课表数据 ----------
|
|
|
|
|
+
|
|
|
|
|
+async function fetchCourseHtml(xn, xq) {
|
|
|
|
|
+ const params = btoa('xn=' + xn + '&xq=' + xq);
|
|
|
|
|
+ const html = await fetchGbk(KB_DATA_URL + '?params=' + params, { credentials: 'include' });
|
|
|
|
|
+ if (!html || html.includes('凭证已失效')) return null;
|
|
|
|
|
+ return html;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function expandWeeks(start, end, parity) {
|
|
|
|
|
+ const weeks = [];
|
|
|
|
|
+ for (let w = start; w <= end; w++) {
|
|
|
|
|
+ if (parity === '单' && w % 2 === 0) continue;
|
|
|
|
|
+ if (parity === '双' && w % 2 === 1) continue;
|
|
|
|
|
+ weeks.push(w);
|
|
|
|
|
+ }
|
|
|
|
|
+ return weeks;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 单个时间段,如 "1-16周 三[7-8] 丽泽楼C202(140)"
|
|
|
|
|
+function parseTimeBlock(block) {
|
|
|
|
|
+ const m = block.match(
|
|
|
|
|
+ /^(\d+)\s*[-—~-]\s*(\d+)周(?:\((单|双)\))?\s*([一二三四五六日天])\s*\[\s*(\d{1,2})(?:\s*[-—~-]\s*(\d{1,2}))?\s*\]\s*(.+)$/
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!m) return null;
|
|
|
|
|
+ const day = DAY_MAP[m[4]];
|
|
|
|
|
+ if (!day) return null;
|
|
|
|
|
+ const startSection = parseInt(m[5], 10);
|
|
|
|
|
+ const endSection = m[6] ? parseInt(m[6], 10) : startSection;
|
|
|
|
|
+ const weeks = expandWeeks(parseInt(m[1], 10), parseInt(m[2], 10), m[3]);
|
|
|
|
|
+ if (weeks.length === 0 || endSection < startSection) return null;
|
|
|
|
|
+ // 去掉地点末尾的教室容量,如 "丽泽楼C202(140)" → "丽泽楼C202"
|
|
|
|
|
+ const position = m[7].trim().replace(/\(\d+\)$/, '');
|
|
|
|
|
+ return { day, startSection, endSection, weeks, position };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseCourses(html) {
|
|
|
|
|
+ const courses = [];
|
|
|
|
|
+ const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
|
|
|
+ const rows = doc.querySelectorAll('tbody tr');
|
|
|
|
|
+ for (const row of rows) {
|
|
|
|
|
+ const cells = row.querySelectorAll('td');
|
|
|
|
|
+ if (cells.length < 6) continue;
|
|
|
|
|
+ const nameMatch = cells[0].textContent.trim().match(/^\[[^\]]+\]\s*(.+)$/);
|
|
|
|
|
+ if (!nameMatch) continue;
|
|
|
|
|
+ const name = nameMatch[1].trim();
|
|
|
|
|
+ const teacher = cells[4].textContent.trim().replace(/;/g, '、');
|
|
|
|
|
+ const timeText = cells[5].textContent.trim();
|
|
|
|
|
+ if (!timeText) continue;
|
|
|
|
|
+
|
|
|
|
|
+ for (const block of timeText.split(',')) {
|
|
|
|
|
+ const parsed = parseTimeBlock(block.trim());
|
|
|
|
|
+ if (!parsed) continue;
|
|
|
|
|
+ const course = {
|
|
|
|
|
+ name: name,
|
|
|
|
|
+ teacher: teacher,
|
|
|
|
|
+ position: parsed.position,
|
|
|
|
|
+ day: parsed.day,
|
|
|
|
|
+ startSection: parsed.startSection,
|
|
|
|
|
+ endSection: parsed.endSection,
|
|
|
|
|
+ weeks: parsed.weeks
|
|
|
|
|
+ };
|
|
|
|
|
+ courses.push(course);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return courses;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 节次与周次两阶段合并去重(与青果案例 mergeAndDistinctCourses 同逻辑):
|
|
|
|
|
+// 阶段 1 合并同课程同周次的连续节次、去除完全重复记录;
|
|
|
|
|
+// 阶段 2 合并同课程同节次的周次(如 1-8 周 + 9-16 周 → 1-16 周)
|
|
|
|
|
+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) ? [...new Set(c.weeks)].sort((a, b) => a - b) : []
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ const byKey = (a, b) =>
|
|
|
|
|
+ a.name.localeCompare(b.name) ||
|
|
|
|
|
+ a.teacher.localeCompare(b.teacher) ||
|
|
|
|
|
+ a.position.localeCompare(b.position) ||
|
|
|
|
|
+ (a.day || 0) - (b.day || 0);
|
|
|
|
|
+
|
|
|
|
|
+ list.sort((a, b) =>
|
|
|
|
|
+ byKey(a, b) ||
|
|
|
|
|
+ a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
|
|
|
|
|
+ (a.startSection || 0) - (b.startSection || 0));
|
|
|
|
|
+
|
|
|
|
|
+ const step1 = [];
|
|
|
|
|
+ let current = list[0];
|
|
|
|
|
+ for (let i = 1; i < list.length; i++) {
|
|
|
|
|
+ const next = list[i];
|
|
|
|
|
+ const sameGroup =
|
|
|
|
|
+ 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 (sameGroup && isContinuous) {
|
|
|
|
|
+ current.endSection = next.endSection;
|
|
|
|
|
+ } else if (sameGroup && isDuplicate) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ step1.push(current);
|
|
|
|
|
+ current = next;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ step1.push(current);
|
|
|
|
|
+
|
|
|
|
|
+ step1.sort((a, b) =>
|
|
|
|
|
+ byKey(a, b) ||
|
|
|
|
|
+ (a.startSection || 0) - (b.startSection || 0) ||
|
|
|
|
|
+ (a.endSection || 0) - (b.endSection || 0));
|
|
|
|
|
+
|
|
|
|
|
+ const step2 = [];
|
|
|
|
|
+ let cur = step1[0];
|
|
|
|
|
+ for (let i = 1; i < step1.length; i++) {
|
|
|
|
|
+ const nxt = step1[i];
|
|
|
|
|
+ const sameGroup =
|
|
|
|
|
+ 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 (sameGroup) {
|
|
|
|
|
+ cur.weeks = [...new Set([...cur.weeks, ...nxt.weeks])].sort((a, b) => a - b);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ step2.push(cur);
|
|
|
|
|
+ cur = nxt;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ step2.push(cur);
|
|
|
|
|
+ return step2;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 开学日期 ----------
|
|
|
|
|
+
|
|
|
|
|
+// 取第一周课表表头的 MM-DD(形如 <label>一</label><br>09-07),推算完整日期
|
|
|
|
|
+async function fetchSemesterStartDate(xn, xq) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const html = await fetchGbk(
|
|
|
|
|
+ LESSON_INFO_URL + '?xn=' + xn + '&xq=' + xq + '&jxz=1',
|
|
|
|
|
+ {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
|
|
|
+ credentials: 'include'
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+ const match = html.match(/<br\s*\/?>\s*(\d{2})-(\d{2})/);
|
|
|
|
|
+ if (match) {
|
|
|
|
|
+ const year = xq === '1' ? String(parseInt(xn, 10) + 1) : xn;
|
|
|
|
|
+ return year + '-' + match[1] + '-' + match[2];
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) { /* 返回 null 由用户手动填写 */ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 作息时间 ----------
|
|
|
|
|
+
|
|
|
|
|
+async function fetchTimeSlots() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const text = await fetchGbk(TIMETABLE_URL, { credentials: 'include' });
|
|
|
|
|
+ const slots = [];
|
|
|
|
|
+ const re = /(\d{1,2})\t(\d{2}:\d{2})\t(\d{2}:\d{2})/g;
|
|
|
|
|
+ let m;
|
|
|
|
|
+ while ((m = re.exec(text)) !== null) {
|
|
|
|
|
+ const number = parseInt(m[1], 10);
|
|
|
|
|
+ if (number >= 1) {
|
|
|
|
|
+ slots.push({ number: number, startTime: m[2], endTime: m[3] });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ slots.sort((a, b) => a.number - b.number);
|
|
|
|
|
+ if (slots.length > 0 && slots[0].number === 1) return slots;
|
|
|
|
|
+ } catch (e) { /* 回退内置 */ }
|
|
|
|
|
+ return FALLBACK_TIME_SLOTS;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 导入流程 ----------
|
|
|
|
|
+
|
|
|
|
|
+async function runImportFlow() {
|
|
|
|
|
+ // 脚本必须在教务站点域内执行,登录会话才会随请求携带
|
|
|
|
|
+ if (!window.location.hostname.endsWith('bnuzh.edu.cn')) {
|
|
|
|
|
+ await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ '无法导入',
|
|
|
|
|
+ '当前不在教务系统页面内,请先完成教务系统登录再执行导入。',
|
|
|
|
|
+ '我知道了'
|
|
|
|
|
+ );
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const confirmed = await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ '导入课表',
|
|
|
|
|
+ '即将从教务系统获取课表、作息与开学日期。请确保当前页面已登录教务系统。',
|
|
|
|
|
+ '开始导入'
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!confirmed) {
|
|
|
|
|
+ window.shiguangBridge.showToast('已取消导入。');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 选择学期(列表来自网络请求,默认选中最新学期)
|
|
|
|
|
+ const semester = await selectSemester();
|
|
|
|
|
+ if (!semester) {
|
|
|
|
|
+ window.shiguangBridge.showToast('已取消导入。');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const { xn, xq } = semester;
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 网络请求获取课表数据
|
|
|
|
|
+ window.shiguangBridge.showToast('正在获取课表数据...');
|
|
|
|
|
+ let html = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ html = await fetchCourseHtml(xn, xq);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ html = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!html) {
|
|
|
|
|
+ await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ '获取失败',
|
|
|
|
|
+ '未能获取课表数据,可能尚未登录或会话已过期,请确认登录后重试。',
|
|
|
|
|
+ '确定'
|
|
|
|
|
+ );
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const courses = mergeAndDistinctCourses(parseCourses(html));
|
|
|
|
|
+ if (courses.length === 0) {
|
|
|
|
|
+ await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ '没有课程',
|
|
|
|
|
+ '已连接教务系统,但该学期没有解析到课程记录。',
|
|
|
|
|
+ '确定'
|
|
|
|
|
+ );
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 开学日期(教务第一周课表)并让用户确认
|
|
|
|
|
+ window.shiguangBridge.showToast('正在获取开学日期...');
|
|
|
|
|
+ const startDate = await fetchSemesterStartDate(xn, xq);
|
|
|
|
|
+ const confirmedDate = await window.shiguangBridgePromise.showPrompt(
|
|
|
|
|
+ '确认开学日期',
|
|
|
|
|
+ '请确认本学期第一周周一的日期(格式 YYYY-MM-DD):',
|
|
|
|
|
+ startDate || '',
|
|
|
|
|
+ 'validateDateInput'
|
|
|
|
|
+ );
|
|
|
|
|
+ if (confirmedDate === null) {
|
|
|
|
|
+ window.shiguangBridge.showToast('导入已取消。');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 总周数按课程周次最大值推算
|
|
|
|
|
+ let maxWeek = 0;
|
|
|
|
|
+ for (const c of courses) {
|
|
|
|
|
+ for (const w of c.weeks) if (w > maxWeek) maxWeek = w;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
|
|
|
|
|
+ semesterStartDate: String(confirmedDate).trim(),
|
|
|
|
|
+ semesterTotalWeeks: maxWeek > 0 ? maxWeek : 20,
|
|
|
|
|
+ defaultClassDuration: 45,
|
|
|
|
|
+ defaultBreakDuration: 10
|
|
|
|
|
+ }));
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ window.shiguangBridge.showToast('课表配置保存失败:' + e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 作息时间(教务公布优先,失败回退内置)
|
|
|
|
|
+ const timeSlots = await fetchTimeSlots();
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ window.shiguangBridge.showToast('作息时间保存失败:' + e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 提交课程
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ await window.shiguangBridgePromise.showAlert('导入失败', '课程数据保存失败:' + e.message, '确定');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ '导入成功',
|
|
|
|
|
+ '已导入 ' + courses.length + ' 条课程记录、作息时间与开学日期 ' + String(confirmedDate).trim() + '。',
|
|
|
|
|
+ '确定'
|
|
|
|
|
+ );
|
|
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+runImportFlow();
|