| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778 |
- // 文件: nwafu.js
- // ---------------------------------------------------------------------------
- // 学校:西北农林科技大学(NWAFU)
- // 系统:ehall 新版教务(金智 jwapp)→「我的本研课表」
- // 入口:https://newehall.nwafu.edu.cn/(登录后进入「我的本研课表」再执行导入)
- //
- // 规范:拾光课程表《适配脚本开发指南 v2》
- // · 数据:courses / timeSlots / config / comboSchedule
- // · 桥接:window.shiguangBridgePromise(异步)/ window.shiguangBridge(同步)
- // · 流程:runImportFlow() 编排,成功后才 notifyTaskCompletion()
- // 作息:按学校作息时间表硬编码(第 1-4 节冬夏相同,第 5 节起不同)
- // 夏令:当年 05.01 — 当年 10.01;冬令:当年 10.01 — 次年 05.01
- // ---------------------------------------------------------------------------
- // ==================== 常量 ====================
- const APP_CODE = 'wdkbby';
- const URL_TERM = '/modules/jshkcb/dqxnxq.do';
- const URL_TERM_LIST = '/modules/jshkcb/xnxqcx.do';
- const URL_COURSE = '/modules/xskcb/cxxszhxqkb.do';
- const URL_CALENDAR = '/modules/xskcb/cxxljc.do';
- const URL_SECTIONS = '/modules/jshkcb/cxjcs.do';
- const ACTION_TERM = 'dqxnxq';
- const ACTION_TERM_LIST = 'xnxqcx';
- const ACTION_COURSE = 'cxxszhxqkb';
- const ACTION_CALENDAR = 'cxxljc';
- const ACTION_SECTIONS = 'cxjcs';
- const PORTAL_HINT = 'newehall.nwafu.edu.cn 新门户 →「我的本研课表」';
- const DEFAULT_TOTAL_WEEKS = 20;
- const MAX_TOTAL_WEEKS = 30;
- // ==================== 作息(学校作息时间表) ====================
- const SLOTS_WINTER = [
- { number: 1, startTime: '08:00', endTime: '08:45' },
- { number: 2, startTime: '08:55', endTime: '09:40' },
- { number: 3, startTime: '10:10', endTime: '10:55' },
- { number: 4, startTime: '11:05', endTime: '11:50' },
- { number: 5, startTime: '14:00', endTime: '14:45' },
- { number: 6, startTime: '14:55', endTime: '15:40' },
- { number: 7, startTime: '16:00', endTime: '16:45' },
- { number: 8, startTime: '16:55', endTime: '17:40' },
- { number: 9, startTime: '19:00', endTime: '19:45' },
- { number: 10, startTime: '19:50', endTime: '20:35' },
- { number: 11, startTime: '20:40', endTime: '21:25' }
- ];
- const SLOTS_SUMMER = [
- { number: 1, startTime: '08:00', endTime: '08:45' },
- { number: 2, startTime: '08:55', endTime: '09:40' },
- { number: 3, startTime: '10:10', endTime: '10:55' },
- { number: 4, startTime: '11:05', endTime: '11:50' },
- { number: 5, startTime: '14:30', endTime: '15:15' },
- { number: 6, startTime: '15:25', endTime: '16:10' },
- { number: 7, startTime: '16:30', endTime: '17:15' },
- { number: 8, startTime: '17:25', endTime: '18:10' },
- { number: 9, startTime: '19:30', endTime: '20:15' },
- { number: 10, startTime: '20:20', endTime: '21:05' },
- { number: 11, startTime: '21:10', endTime: '21:55' }
- ];
- // ==================== 工具 ====================
- function toast(message) {
- try {
- window.shiguangBridge.showToast(String(message));
- } catch (e) {
- console.warn('[NWAFU] showToast 不可用:', e && e.message);
- }
- }
- function log(message) {
- try {
- console.log('[NWAFU 课表适配] ' + message);
- } catch (e) { /* ignore */ }
- }
- function textOf(value) {
- if (value === null || value === undefined) return '';
- const result = String(value).replace(/\u00a0/g, ' ').trim();
- if (result === 'null' || result === 'undefined') return '';
- return result;
- }
- function uniqueSorted(numbers) {
- return Array.from(new Set(numbers.filter((n) => typeof n === 'number' && !isNaN(n) && n > 0)))
- .sort((a, b) => a - b);
- }
- /** 教师字段去重 **/
- function cleanTeacher(value) {
- const raw = textOf(value);
- if (!raw) return '';
- const names = raw.split(/[,,、;;]/).map((item) => textOf(item)).filter((item) => item !== '');
- return Array.from(new Set(names)).join(',') || raw;
- }
- // ==================== 教务接口 ====================
- function rootPath() {
- const config = window._JW_INIT_CONFIG || {};
- let root = config.path || config.ROOT_PATH || config.contextPath || '/jwapp';
- root = String(root);
- if (root.charAt(0) !== '/') root = '/' + root;
- return root.replace(/\/+$/, '');
- }
- function encodeForm(data) {
- const pairs = [];
- Object.keys(data || {}).forEach((key) => {
- const value = data[key];
- if (value === null || value === undefined) return;
- pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(value)));
- });
- return pairs.join('&');
- }
- /** 与教务页面自身(BH_UTILS.doSyncAjax)等价的表单 POST:同源、带 Cookie */
- async function api(modulePath, data) {
- const response = await fetch(rootPath() + '/sys/' + APP_CODE + modulePath, {
- method: 'POST',
- credentials: 'include',
- headers: {
- 'accept': 'application/json, text/javascript, */*; q=0.01',
- 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
- 'x-requested-with': 'XMLHttpRequest'
- },
- body: encodeForm(data || {})
- });
- if (!response || !response.ok) throw new Error('HTTP ' + (response ? response.status : '?'));
- const raw = await response.text();
- let json;
- try {
- json = JSON.parse(raw);
- } catch (e) {
- throw new Error('返回内容不是 JSON(很可能登录已失效)');
- }
- if (json && json.loginURL) throw new Error('登录状态已失效,请重新登录');
- return json;
- }
- /** 取出 {datas:{<action>:{rows:[...]}}} 中的 rows */
- function rowsOf(response, action) {
- if (!response || !response.datas) return null;
- const node = response.datas[action];
- if (!node || !node.rows || typeof node.rows.length !== 'number') return null;
- if (node.rows.length === 0 && node.extParams && String(node.extParams.code) !== '1') return null;
- return node.rows;
- }
- function messageOf(response) {
- if (!response || !response.datas) return '';
- const keys = Object.keys(response.datas);
- for (let i = 0; i < keys.length; i++) {
- const node = response.datas[keys[i]];
- if (node && node.extParams && node.extParams.msg) return String(node.extParams.msg);
- }
- return '';
- }
- // ==================== 学年学期 ====================
- /** 当前学年学期(dqxnxq.do),用于学期列表的默认选中项 */
- async function fetchTerm() {
- const response = await api(URL_TERM, {});
- const rows = rowsOf(response, ACTION_TERM);
- if (!rows || !rows.length) {
- throw new Error('未能获取当前学年学期' + (messageOf(response) ? ':' + messageOf(response) : ''));
- }
- const dm = textOf(rows[0].DM);
- if (!dm) throw new Error('学年学期接口未返回学期代码');
- return { dm: dm, mc: textOf(rows[0].MC) };
- }
- /** 学期列表(xnxqcx.do,按学期代码降序 = 最新在前) */
- async function fetchTermList() {
- const response = await api(URL_TERM_LIST, { '*order': '-DM' });
- const rows = rowsOf(response, ACTION_TERM_LIST);
- if (!rows || !rows.length) return [];
- return sortTermsDesc(rows);
- }
- /** 规整学期行并降序排列 */
- function sortTermsDesc(rows) {
- return (rows || [])
- .map((row) => ({ dm: textOf(row && row.DM), mc: textOf(row && row.MC) }))
- .filter((term) => term.dm)
- .sort((a, b) => b.dm.localeCompare(a.dm));
- }
- /** 学期显示文本 */
- function termText(term) {
- const mc = textOf(term && term.mc);
- return mc || textOf(term && term.dm);
- }
- /** 默认选中当前学期;列表中找不到则选第一条 */
- function defaultTermIndex(list, currentDm) {
- if (!Array.isArray(list) || !list.length) return -1;
- const key = textOf(currentDm);
- if (!key) return 0;
- const index = list.findIndex((term) => term.dm === key);
- return index >= 0 ? index : 0;
- }
- /**
- * 让用户选择要导入的学期(官方 API:showSingleSelection)。
- * 返回 { dm, mc };用户取消返回 null。
- */
- async function selectTerm() {
- let list = [];
- try {
- list = await fetchTermList();
- } catch (e) {
- log('学期列表接口失败,退回当前学期: ' + e.message);
- }
- let currentDm = '';
- try {
- const current = await fetchTerm();
- currentDm = current.dm;
- if (!list.length) list = [current];
- } catch (e) {
- log('当前学期接口失败: ' + e.message);
- }
- if (!list.length) throw new Error('未能获取学期列表,请先登录教务并打开「我的本研课表」');
- if (list.length === 1) return list[0];
- const labels = list.map((term) => termText(term));
- const selected = await window.shiguangBridgePromise.showSingleSelection(
- '选择学期',
- JSON.stringify(labels),
- defaultTermIndex(list, currentDm)
- );
- if (selected === null || selected === undefined) return null;
- const index = Number(selected);
- if (!(index >= 0 && index < list.length)) return null;
- return list[index];
- }
- /** "2026-2027-1" → { xn: "2026-2027", xq: "1" } */
- function termCodes(dm) {
- const parts = textOf(dm).split('-');
- if (parts.length < 3) return null;
- const xn = parts[0] + '-' + parts[1];
- const xq = parts[2];
- if (!/^\d{4}-\d{4}$/.test(xn) || !/^\d+$/.test(xq)) return null;
- return { xn: xn, xq: xq };
- }
- /**
- * 校历:所选学期的开学日期 + 校历总周数
- * POST {ROOT}/sys/wdkbby/modules/xskcb/cxxljc.do { XN, XQ }
- * rows[0].XQKSRQ → 开学日期;rows[0].ZZC → 校历总周数
- * 取不到返回 null(不影响课程导入)。
- */
- async function fetchTermCalendar(dm) {
- const codes = termCodes(dm);
- if (!codes) return null;
- const response = await api(URL_CALENDAR, { XN: codes.xn, XQ: codes.xq });
- const rows = rowsOf(response, ACTION_CALENDAR);
- if (!rows || !rows.length) return null;
- const rawDate = textOf(rows[0].XQKSRQ);
- const startDate = /^\d{4}-\d{2}-\d{2}/.test(rawDate) ? rawDate.substring(0, 10) : '';
- const totalWeeks = parseInt(textOf(rows[0].ZZC), 10);
- if (!startDate && !(totalWeeks > 0)) return null;
- return {
- startDate: startDate,
- totalWeeks: totalWeeks > 0 ? Math.min(totalWeeks, MAX_TOTAL_WEEKS) : 0
- };
- }
- // ==================== 周次解析 ====================
- /** 周次位串,例如 "0000111111111111110000"(第 5-18 周)→ [5..18] */
- function parseWeekBitString(value) {
- const raw = textOf(value);
- if (!raw) return null;
- const compact = raw.replace(/\s+/g, '');
- if (!/^[01]+$/.test(compact)) return null;
- if (compact.length < 1 || compact.length > 60) return null;
- const weeks = [];
- for (let i = 0; i < compact.length; i++) {
- if (compact.charAt(i) === '1') weeks.push(i + 1);
- }
- return weeks;
- }
- // 周次表达式:6-18周 / 4周 / 7-17周(单) / 4-6周(双),8-13周 / 5-16周,18周
- const WEEK_GROUP = '\\d+\\s*(?:[-~至]\\s*\\d+)?\\s*(?:[((]\\s*[单双]\\s*[))])?\\s*周\\s*(?:[((]\\s*[单双]\\s*[))])?';
- const WEEK_EXPRESSION = '(?:' + WEEK_GROUP + '\\s*[,,、]?\\s*)+';
- /** 解析一段周次内容(可含逗号分隔的多组、单双周标记) */
- function parseWeekSegments(expression) {
- const raw = textOf(expression);
- if (!raw) return [];
- const weeks = [];
- raw.split(/[,,、]/).forEach((segment) => {
- if (!segment) return;
- let parity = 0; // 0=全部 1=单周 2=双周
- if (segment.indexOf('单') !== -1) parity = 1;
- if (segment.indexOf('双') !== -1) parity = 2;
- const rangePattern = /(\d+)\s*(?:[-~至]\s*(\d+))?/g;
- let match;
- while ((match = rangePattern.exec(segment)) !== null) {
- const start = parseInt(match[1], 10);
- let end = match[2] ? parseInt(match[2], 10) : start;
- if (isNaN(start) || start <= 0) continue;
- if (isNaN(end) || end < start) end = start;
- if (end - start > 60) end = start;
- for (let week = start; week <= end; week++) {
- if (parity === 1 && week % 2 === 0) continue;
- if (parity === 2 && week % 2 === 1) continue;
- weeks.push(week);
- }
- }
- });
- return uniqueSorted(weeks);
- }
- /** 只截取周次表达式,避免把「第3节-第4节」当周次 */
- function matchWeekExpression(raw) {
- const match = new RegExp(WEEK_EXPRESSION).exec(raw);
- if (!match || !match[0]) return null;
- let start = match.index;
- const back = /(?:\d+\s*(?:[-~至]\s*\d+)?\s*[,,、]\s*)+$/.exec(raw.substring(0, start));
- if (back) start -= back[0].length;
- return { index: start, text: raw.substring(start, match.index + match[0].length) };
- }
- /** 周次文本:"6-18周" / "4周" / "1-16周(单)" / "1,3,5周" */
- function parseWeeksDescription(value) {
- const raw = textOf(value);
- if (!raw) return [];
- const matched = matchWeekExpression(raw);
- if (matched) {
- const weeks = parseWeekSegments(matched.text);
- if (weeks.length) return weeks;
- }
- const head = raw.split('周')[0] || '';
- return parseWeekSegments(head.replace(/^[^\d]*/, ''));
- }
- // ==================== 接口数据 → 课程 ====================
- function buildPosition(row) {
- const room = textOf(row.JASMC) || textOf(row.JAS);
- if (room) return room;
- const building = textOf(row.JXLDM_DISPLAY) || textOf(row.JXLMC);
- if (building) return building;
- return textOf(row.XXXQMC);
- }
- function parseCourseRow(row) {
- if (!row) return null;
- const day = parseInt(textOf(row.SKXQ), 10);
- if (!(day >= 1 && day <= 7)) return null;
- const baseName = textOf(row.KCM);
- if (!baseName) return null;
- const alias = textOf(row.TYXMDM_DISPLAY);
- const name = alias ? (baseName + '(' + alias + ')') : baseName;
- const startSection = parseInt(textOf(row.KSJC), 10);
- let endSection = parseInt(textOf(row.JSJC), 10);
- if (!(startSection > 0)) return null;
- if (isNaN(endSection) || endSection < startSection) endSection = startSection;
- let weeks = parseWeekBitString(row.SKZC);
- if (!weeks || !weeks.length) weeks = parseWeeksDescription(row.ZCMC);
- if (!weeks || !weeks.length) return null; // 周次无法确定:不完整课程,忽略
- return {
- name: name,
- teacher: cleanTeacher(row.SKJS),
- position: buildPosition(row),
- day: day,
- startSection: startSection,
- endSection: endSection,
- weeks: weeks
- };
- }
- async function fetchCourses(term) {
- const response = await api(URL_COURSE, { XNXQDM: term.dm });
- const rows = rowsOf(response, ACTION_COURSE);
- if (!rows) {
- const message = messageOf(response);
- throw new Error('课表接口未返回数据' + (message ? ':' + message : ''));
- }
- if (!rows.length) throw new Error('课表接口返回 0 条记录(该学期可能没有排课)');
- const courses = [];
- let maxWeekBits = 0;
- rows.forEach((row) => {
- const bits = textOf(row.SKZC).replace(/\s+/g, '');
- if (/^[01]+$/.test(bits) && bits.length > maxWeekBits) maxWeekBits = bits.length;
- const course = parseCourseRow(row);
- if (course) courses.push(course);
- });
- if (!courses.length) throw new Error('课表接口返回 ' + rows.length + ' 条记录,但均无法解析出课程');
- log('接口返回 ' + rows.length + ' 条排课记录,解析出 ' + courses.length + ' 条(忽略 ' + (rows.length - courses.length) + ' 条不完整记录)');
- return { courses: courses, maxWeekBits: maxWeekBits };
- }
- // ==================== 合并去重(官方参考实现) ====================
- /**
- * 节次与周次合并去重函数
- * 来源:拾光课程表 wiki《课程合并与去重函数》
- */
- function mergeAndDistinctCourses(courses) {
- if (!Array.isArray(courses) || courses.length <= 1) return courses || [];
- const list = courses.map((c) => Object.assign({}, c, {
- name: c.name || '',
- teacher: c.teacher || '',
- position: c.position || '',
- weeks: Array.isArray(c.weeks) ? c.weeks.slice().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.concat(nxt.weeks))).sort((a, b) => a - b);
- } else {
- step2Merged.push(cur);
- cur = nxt;
- }
- }
- step2Merged.push(cur);
- return step2Merged;
- }
- // ==================== 作息方案 ====================
- function pad2(value) {
- return value < 10 ? ('0' + value) : String(value);
- }
- /** 生成 "YYYY-MM-DD" */
- function dateText(year, month, day) {
- return String(year) + '-' + pad2(month) + '-' + pad2(day);
- }
- /**
- * 生成「冬令 + 夏令」两段作息(区间为闭区间,结束日取下一段开始日前一天):
- * 夏令 05-01 ~ 09-30;冬令 10-01 ~ 次年 04-30
- * 只生成「当前时段 + 下一时段」两条规则,避免同一日期命中多条。
- */
- function buildComboSchedule(today, winterSlots, summerSlots) {
- const winter = winterSlots || SLOTS_WINTER;
- const summer = summerSlots || SLOTS_SUMMER;
- const date = today || new Date();
- const year = date.getFullYear();
- const month = date.getMonth() + 1;
- let rules;
- if (month >= 5 && month <= 9) {
- rules = [
- { name: '夏令作息', startDate: dateText(year, 5, 1), endDate: dateText(year, 9, 30), timeSlots: summer },
- { name: '冬令作息', startDate: dateText(year, 10, 1), endDate: dateText(year + 1, 4, 30), timeSlots: winter }
- ];
- } else if (month >= 10) {
- rules = [
- { name: '冬令作息', startDate: dateText(year, 10, 1), endDate: dateText(year + 1, 4, 30), timeSlots: winter },
- { name: '夏令作息', startDate: dateText(year + 1, 5, 1), endDate: dateText(year + 1, 9, 30), timeSlots: summer }
- ];
- } else {
- rules = [
- { name: '冬令作息', startDate: dateText(year - 1, 10, 1), endDate: dateText(year, 4, 30), timeSlots: winter },
- { name: '夏令作息', startDate: dateText(year, 5, 1), endDate: dateText(year, 9, 30), timeSlots: summer }
- ];
- }
- return { name: '西北农林科技大学作息(冬令/夏令)', publicSchedules: rules };
- }
- /** 分钟数 → "HH:mm" */
- function hhmm(minutes) {
- const hour = Math.floor(minutes / 60) % 24;
- return pad2(hour) + ':' + pad2(minutes % 60);
- }
- /**
- * 课程节次超出作息表时补足(45 分钟上课 + 10 分钟课间)。
- * 正常学期不会触发(作息表 11 节),只有教务临时加节时兜底,避免课程引用不存在的节次。
- */
- function extendSlots(base, maxSection) {
- const slots = (base || []).map((slot) => ({
- number: slot.number,
- startTime: slot.startTime,
- endTime: slot.endTime
- }));
- if (!slots.length) return slots;
- const target = Math.min(Math.max(parseInt(maxSection, 10) || slots.length, slots.length), 20);
- while (slots.length < target) {
- const last = slots[slots.length - 1];
- const parts = last.endTime.split(':');
- const startMinutes = parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10) + 10;
- slots.push({
- number: slots.length + 1,
- startTime: hhmm(startMinutes),
- endTime: hhmm(startMinutes + 45)
- });
- }
- return slots;
- }
- /** 课程里出现的最大的结束节次 */
- function maxSectionOf(courses) {
- let max = 0;
- (courses || []).forEach((course) => {
- const end = parseInt(course && course.endSection, 10);
- if (end > max) max = end;
- });
- return max;
- }
- /**
- * 节次结构(cxjcs.do):上午 / 下午 / 晚上各几节。
- * 仅用于日志核对硬编码作息是否过期,取不到不影响导入。
- */
- async function fetchSectionLayout(dm) {
- const codes = termCodes(dm);
- if (!codes) return null;
- const response = await api(URL_SECTIONS, { XN: codes.xn, XQ: codes.xq });
- const rows = rowsOf(response, ACTION_SECTIONS);
- if (!rows || !rows.length) return null;
- const row = rows[0];
- const morning = parseInt(textOf(row.SFJJ), 10) || 0;
- const afternoon = parseInt(textOf(row.XFJJ), 10) || 0;
- const evening = parseInt(textOf(row.WSJJ), 10) || 0;
- return { morning: morning, afternoon: afternoon, evening: evening, total: morning + afternoon + evening };
- }
- /** 课表配置:优先用校历给的开学日期与总周数 */
- function buildCourseConfig(calendar, courseWeeks) {
- const totalWeeks = (calendar && calendar.totalWeeks > 0) ? calendar.totalWeeks : courseWeeks;
- const config = { semesterTotalWeeks: totalWeeks };
- if (calendar && calendar.startDate) config.semesterStartDate = calendar.startDate;
- return config;
- }
- // ==================== 保存 ====================
- async function saveConfig(config) {
- try {
- return await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
- } catch (error) {
- toast('保存课表配置失败: ' + error.message);
- return false;
- }
- }
- async function saveCourses(courses) {
- try {
- return await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
- } catch (error) {
- toast('保存课程失败: ' + error.message);
- return false;
- }
- }
- // ==================== 主导入流程 ====================
- /** 前置确认 */
- async function promptUserToStart() {
- const confirmed = await window.shiguangBridgePromise.showAlert(
- '导入确认',
- '将从西北农林科技大学「我的本研课表」导入课程与作息。\n'
- + '请先登录 ' + PORTAL_HINT + ',再点击开始导入;\n'
- + '导入时可选择要导入的学期(默认当前学期)。',
- '开始导入'
- );
- if (!confirmed) {
- toast('已取消导入');
- return false;
- }
- toast('正在读取课表数据…');
- return true;
- }
- function computeTotalWeeks(courses, maxWeekBits) {
- let maxWeek = 0;
- courses.forEach((course) => {
- (course.weeks || []).forEach((week) => {
- if (week > maxWeek) maxWeek = week;
- });
- });
- let total = Math.max(maxWeek, maxWeekBits || 0);
- if (!total) total = DEFAULT_TOTAL_WEEKS;
- return Math.min(Math.max(total, 1), MAX_TOTAL_WEEKS);
- }
- /** 编排整个导入流程:成功才发送结束信号 */
- /**
- * 作息保存:先基础作息(冬令),再冬令/夏令组合方案。
- * 官方要求 savePresetTimeSlots 必须先成功,否则 saveComboSchedule 会被拒绝。
- */
- async function saveSchedule(winterSlots, summerSlots) {
- try {
- await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(winterSlots));
- await window.shiguangBridgePromise.saveComboSchedule(
- JSON.stringify(buildComboSchedule(new Date(), winterSlots, summerSlots))
- );
- return true;
- } catch (e) {
- log('作息导入失败(不影响课程): ' + e.message);
- return false;
- }
- }
- /** 校历与会话信息:失败都不影响课程导入 */
- async function loadExtras(dm) {
- let calendar = null;
- try {
- calendar = await fetchTermCalendar(dm);
- } catch (e) {
- log('校历接口失败(不影响导入): ' + e.message);
- }
- let layout = null;
- try {
- layout = await fetchSectionLayout(dm);
- } catch (e) {
- log('节次结构接口失败: ' + e.message);
- }
- if (layout && layout.total > 0) {
- log('教务节次结构:上午 ' + layout.morning + ' / 下午 ' + layout.afternoon
- + ' / 晚上 ' + layout.evening + ' 节(合计 ' + layout.total + ')');
- }
- return calendar;
- }
- /** 编排整个导入流程(只做流程控制):成功才发送结束信号 */
- async function runImportFlow() {
- try {
- if (!(await promptUserToStart())) return;
- // 1) 选择学期(默认当前学期;已结束且下学期已公布时可选下学期)
- const term = await selectTerm();
- if (!term) {
- toast('已取消导入');
- return;
- }
- log('选中学期: ' + term.dm + ' / ' + termText(term));
- // 2) 该学期课程 + 校历
- const fetched = await fetchCourses(term);
- const courses = mergeAndDistinctCourses(fetched.courses);
- const calendar = await loadExtras(term.dm);
- // 3) 配置 → 课程
- const config = buildCourseConfig(calendar, computeTotalWeeks(courses, fetched.maxWeekBits));
- if (!(await saveConfig(config))) {
- toast('课表配置保存失败,导入终止');
- return;
- }
- if (!(await saveCourses(courses))) {
- toast('课程保存失败,导入终止');
- return;
- }
- // 4) 作息(节次超出作息表时自动补足)→ 组合作息
- const maxSection = maxSectionOf(courses);
- if (maxSection > SLOTS_WINTER.length) {
- log('课程出现第 ' + maxSection + ' 节,作息自动补足到 ' + maxSection + ' 节');
- }
- const winterSlots = extendSlots(SLOTS_WINTER, maxSection);
- const summerSlots = extendSlots(SLOTS_SUMMER, maxSection);
- const scheduleImported = await saveSchedule(winterSlots, summerSlots);
- // 5) 完成
- toast(
- '导入成功:' + courses.length + ' 门课程'
- + (scheduleImported ? ',已写入冬令/夏令作息' : ',作息导入失败')
- + (calendar && calendar.startDate ? ',开学 ' + calendar.startDate : '')
- + '(' + termText(term) + ')'
- );
- window.shiguangBridge.notifyTaskCompletion();
- } catch (error) {
- await reportFailure(error);
- }
- }
- /** 失败提示:优先弹窗,弹窗不可用时退回 toast */
- async function reportFailure(error) {
- const message = '导入失败:' + (error && error.message ? error.message : String(error));
- console.error('[NWAFU 课表适配] ' + message);
- try {
- await window.shiguangBridgePromise.showAlert(
- '导入失败',
- message + '\n\n请先登录 ' + PORTAL_HINT + ',打开课表页面后重试。',
- '知道了'
- );
- } catch (e) {
- toast(message);
- }
- }
- // 启动导入流程
- runImportFlow();
|