| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- // 贵州机电职业技术学院(gztcme.cn)正方 V9 教务适配
- // 入口:登录后进入「个人课表查询」,再执行导入。
- function gztcmeText(value) {
- return String(value == null ? '' : value).replace(/^\s+|\s+$/g, '');
- }
- function gztcmeConfirmed(value) {
- return value === true || value === 'true';
- }
- function gztcmeCheckPage() {
- if (window.location.pathname.indexOf('/jwglxt/') < 0) {
- throw new Error('请先在教务系统(jwxt.gztcme.cn)登录并进入“个人课表查询”,再执行导入。');
- }
- }
- async function gztcmeFetchText(path, body) {
- const init = { method: body ? 'POST' : 'GET', credentials: 'include' };
- if (body) {
- init.headers = { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' };
- init.body = body;
- }
- let response;
- try {
- response = await fetch('/jwglxt/' + path, init);
- } catch (error) {
- throw new Error('请求教务失败,请确认网络通畅并且已经登录教务系统。');
- }
- const text = await response.text();
- if ((response.url || '').indexOf('login_slogin') >= 0 || text.indexOf('login_slogin') >= 0) {
- throw new Error('登录已过期,请重新登录教务系统后再导入。');
- }
- if (!response.ok) throw new Error('教务返回异常(HTTP ' + response.status + '),请稍后重试。');
- return text;
- }
- async function gztcmeFetchJson(path, body) {
- const text = await gztcmeFetchText(path, body);
- try {
- return JSON.parse(text);
- } catch (error) {
- throw new Error('教务未返回有效数据,请重新登录后重试。');
- }
- }
- function gztcmeOptions(doc, selector) {
- const list = [];
- const nodes = doc.querySelectorAll(selector + ' option');
- for (let i = 0; i < nodes.length; i++) {
- const value = gztcmeText(nodes[i].getAttribute('value'));
- if (!value) continue;
- list.push({
- value: value,
- label: gztcmeText(nodes[i].textContent) || value,
- selected: nodes[i].selected === true || nodes[i].hasAttribute('selected')
- });
- }
- return list;
- }
- async function gztcmePick(title, items, defaultIndex) {
- const labels = [];
- for (let i = 0; i < items.length; i++) labels.push(items[i].label);
- const picked = await window.shiguangBridgePromise.showSingleSelection(title, JSON.stringify(labels), defaultIndex);
- if (picked === null || picked === undefined) return null;
- const text = String(picked).toLowerCase();
- if (text === 'null' || text === '') return null;
- const index = Number(picked);
- if (!(index >= 0 && index < items.length)) throw new Error('所选项目无效,请重新导入。');
- return items[index];
- }
- async function gztcmeChooseTerm() {
- const html = await gztcmeFetchText('kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default');
- const doc = new DOMParser().parseFromString(html, 'text/html');
- const years = gztcmeOptions(doc, '#xnm');
- const terms = gztcmeOptions(doc, '#xqm');
- if (years.length === 0 || terms.length === 0) throw new Error('未取得学年学期列表,请确认已经登录教务系统。');
- let yearIndex = 0, termIndex = 0;
- for (let i = 0; i < years.length; i++) if (years[i].selected) yearIndex = i;
- for (let i = 0; i < terms.length; i++) if (terms[i].selected) termIndex = i;
- const year = await gztcmePick('选择学年', years, yearIndex);
- if (!year) return null;
- const term = await gztcmePick('选择学期', terms, termIndex);
- if (!term) return null;
- return { year: year.value, term: term.value, label: year.label + ' 第 ' + term.label + ' 学期' };
- }
- function gztcmeWeeks(value) {
- const text = gztcmeText(value).replace(/[第周\s]/g, '').replace(/(/g, '(').replace(/)/g, ')').replace(/[,、]/g, ',');
- const weeks = [], seen = {};
- const parts = text.split(',');
- for (let i = 0; i < parts.length; i++) {
- if (!parts[i]) continue;
- const match = parts[i].match(/^(\d+)(?:-(\d+))?(?:\(([单双])\))?$/);
- if (!match) throw new Error('无法识别上课周次「' + value + '」,请核对教务课表。');
- const from = Number(match[1]), to = Number(match[2] || match[1]);
- if (from < 1 || to < from || to > 60) throw new Error('上课周次超出有效范围。');
- for (let week = from; week <= to; week++) {
- if (match[3] === '单' && week % 2 === 0) continue;
- if (match[3] === '双' && week % 2 === 1) continue;
- if (!seen[week]) { seen[week] = true; weeks.push(week); }
- }
- }
- weeks.sort(function (a, b) { return a - b; });
- if (weeks.length === 0) throw new Error('课程周次为空,请核对教务课表。');
- return weeks;
- }
- function gztcmeCourses(data, term) {
- if (!data || !Array.isArray(data.kbList) || !data.xsxx) throw new Error('教务未返回课表数据,请重新进入个人课表页面。');
- if (gztcmeText(data.xsxx.XNM) !== term.year || gztcmeText(data.xsxx.XQM) !== term.term) {
- throw new Error('课表与所选学期不一致,请重新导入。');
- }
- const courses = [], campuses = [];
- for (let i = 0; i < data.kbList.length; i++) {
- const row = data.kbList[i];
- const name = gztcmeText(row.kcmc);
- if (!name) continue;
- const day = Number(row.xqj);
- const section = gztcmeText(row.jcs).replace(/节/g, '').match(/^(\d+)(?:-(\d+))?$/);
- if (!(day >= 1 && day <= 7) || !section) throw new Error('课程「' + name + '」的星期或节次异常,请核对教务课表。');
- const startSection = Number(section[1]), endSection = Number(section[2] || section[1]);
- if (startSection < 1 || endSection < startSection) throw new Error('课程「' + name + '」的节次异常,请核对教务课表。');
- courses.push({
- name: name,
- teacher: gztcmeText(row.xm),
- position: gztcmeText(row.cdmc),
- day: day,
- startSection: startSection,
- endSection: endSection,
- weeks: gztcmeWeeks(row.zcd)
- });
- const campusId = gztcmeText(row.xqh_id);
- if (campusId) {
- let known = false;
- for (let j = 0; j < campuses.length; j++) if (campuses[j].value === campusId) known = true;
- if (!known) campuses.push({ value: campusId, label: gztcmeText(row.xqmc) || '校区' });
- }
- }
- if (courses.length === 0) throw new Error('该学期没有可导入的课程,请确认所选学期。');
- return { courses: courses, campuses: campuses, unscheduled: Array.isArray(data.sjkList) ? data.sjkList : [] };
- }
- function gztcmeCalendar(rows) {
- if (!Array.isArray(rows) || rows.length === 0) throw new Error('未取得所选学期的校历,请确认学期选择。');
- const weeks = [];
- for (let i = 0; i < rows.length; i++) {
- const number = Number(rows[i].zs);
- const match = gztcmeText(rows[i].rq).match(/^(\d{4}-\d{2}-\d{2})\/(\d{4}-\d{2}-\d{2})$/);
- if (!(number >= 1) || !match) throw new Error('校历格式异常,无法确定开学日期。');
- weeks.push({ number: number, start: match[1], end: match[2] });
- }
- weeks.sort(function (a, b) { return a.number - b.number; });
- if (weeks.length > 60) throw new Error('校历周数超出有效范围。');
- const first = new Date(weeks[0].start + 'T00:00:00Z');
- if (isNaN(first.getTime()) || first.getUTCDay() !== 1) throw new Error('校历第一周不是周一,无法确定开学日期。');
- for (let i = 0; i < weeks.length; i++) {
- const start = new Date(weeks[i].start + 'T00:00:00Z');
- const end = new Date(weeks[i].end + 'T00:00:00Z');
- if (weeks[i].number !== i + 1) throw new Error('校历周次不连续。');
- if (isNaN(start.getTime()) || isNaN(end.getTime()) || end < start) throw new Error('校历日期格式异常。');
- if (start.getTime() - first.getTime() !== i * 7 * 86400000) throw new Error('校历日期与教学周不对应。');
- }
- return { semesterStartDate: weeks[0].start, semesterTotalWeeks: weeks.length, firstDayOfWeek: 1 };
- }
- function gztcmeClock(value) {
- const match = gztcmeText(value).match(/^(\d{1,2}):(\d{2})/);
- if (!match) throw new Error('作息时间格式异常。');
- return (match[1].length === 1 ? '0' + match[1] : match[1]) + ':' + match[2];
- }
- function gztcmeTimeSlots(rows) {
- const slots = [], seen = {};
- for (let i = 0; i < rows.length; i++) {
- const number = Number(rows[i].jcmc);
- if (!(number >= 1) || seen[number]) continue;
- seen[number] = true;
- slots.push({ number: number, startTime: gztcmeClock(rows[i].qssj), endTime: gztcmeClock(rows[i].jssj) });
- }
- slots.sort(function (a, b) { return a.number - b.number; });
- for (let i = 0; i < slots.length; i++) {
- if (slots[i].number !== i + 1) throw new Error('作息节次编号不连续,请核对教务作息。');
- if (slots[i].startTime >= slots[i].endTime) throw new Error('作息起止时间异常,请核对教务作息。');
- }
- return slots;
- }
- async function gztcmeSave(method, value, label) {
- const result = await window.shiguangBridgePromise[method](JSON.stringify(value));
- if (!gztcmeConfirmed(result)) throw new Error(label + '保存未成功,请重新导入。');
- }
- async function gztcmeRunImport() {
- let savedStage = '';
- try {
- gztcmeCheckPage();
- window.shiguangBridge.showToast('正在读取学年学期…');
- const term = await gztcmeChooseTerm();
- if (!term) { window.shiguangBridge.showToast('导入已取消'); return; }
- window.shiguangBridge.showToast('正在读取课表、校历和作息…');
- const fetched = await Promise.all([
- gztcmeFetchJson('kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151', 'xnm=' + term.year + '&xqm=' + term.term + '&kzlx=ck'),
- gztcmeFetchJson('kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154', 'xnm=' + term.year + '&xqm=' + term.term)
- ]);
- const parsed = gztcmeCourses(fetched[0], term);
- const config = gztcmeCalendar(fetched[1]);
- let campus = parsed.campuses.length > 0 ? parsed.campuses[0] : null;
- if (parsed.campuses.length > 1) {
- campus = await gztcmePick('选择课表作息校区', parsed.campuses, 0);
- if (!campus) { window.shiguangBridge.showToast('导入已取消'); return; }
- }
- let slots = [];
- if (campus) {
- const rows = await gztcmeFetchJson('kbcx/xskbcx_cxRjc.html?gnmkdm=N2151',
- 'xnm=' + term.year + '&xqm=' + term.term + '&xqh_id=' + campus.value);
- slots = gztcmeTimeSlots(Array.isArray(rows) ? rows : []);
- }
- let maxSection = 0;
- const names = [];
- for (let i = 0; i < parsed.courses.length; i++) {
- const course = parsed.courses[i];
- if (course.endSection > maxSection) maxSection = course.endSection;
- for (let j = 0; j < course.weeks.length; j++) {
- if (course.weeks[j] > config.semesterTotalWeeks) {
- throw new Error('课程「' + course.name + '」周次超出本学期校历(' + config.semesterTotalWeeks + ' 周)。');
- }
- }
- let known = false;
- for (let j = 0; j < names.length; j++) if (names[j] === course.name) known = true;
- if (!known) names.push(course.name);
- }
- if (slots.length > 0 && maxSection > slots.length) throw new Error('课表节次超出作息节数,请核对教务课表。');
- let note = term.label + '\n' +
- '课程时段:' + parsed.courses.length + ' 条 / ' + names.length + ' 门课\n' +
- '开学日期:' + config.semesterStartDate + '(第 1 周周一)\n' +
- '校历周数:' + config.semesterTotalWeeks + ' 周\n' +
- '作息节数:' + (slots.length > 0 ? slots.length + ' 节(' + (campus ? campus.label : '') + ')' : '未取到,使用应用默认作息');
- if (parsed.unscheduled.length > 0) {
- let pending = '';
- for (let i = 0; i < parsed.unscheduled.length; i++) {
- const row = parsed.unscheduled[i];
- pending += '\n· ' + (gztcmeText(row.kcmc) || '未命名课程') + '(' + (gztcmeText(row.qsjsz) || '周次未知') + ')';
- }
- note += '\n\n以下 ' + parsed.unscheduled.length + ' 门课尚未排定上课时间,本次不导入:' + pending;
- }
- const confirmed = await window.shiguangBridgePromise.showAlert('核对导入信息', note, '确认导入');
- if (!gztcmeConfirmed(confirmed)) { window.shiguangBridge.showToast('导入已取消'); return; }
- await gztcmeSave('saveCourseConfig', config, '学期配置');
- savedStage = '学期配置';
- if (slots.length > 0) {
- await gztcmeSave('savePresetTimeSlots', slots, '作息');
- savedStage = '学期配置和作息';
- }
- await gztcmeSave('saveImportedCourses', parsed.courses, '课程');
- window.shiguangBridge.showToast('已导入 ' + parsed.courses.length + ' 条课程时段');
- window.shiguangBridge.notifyTaskCompletion();
- } catch (error) {
- const message = error && error.message ? error.message : String(error);
- await window.shiguangBridgePromise.showAlert('导入未完成',
- (savedStage ? savedStage + '已保存,后续步骤未完成,请重新导入。\n' : '') + message, '知道了');
- }
- }
- gztcmeRunImport();
|