| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- /**
- * 重庆科技大学 (CQUST) 树维 EAMS 教务系统课表导入
- * Maintainer: yuexps
- */
- (async () => {
- 'use strict';
- // 允许访问的教务域名
- const ALLOWED_HOSTS = [
- 'web.cqust.edu.cn',
- 'jwnew.cqust.edu.ex2.http.80.ipv6.cqust.edu.cn',
- 'jwnew.cqust.edu.cn'
- ];
- // 作息时间表(共 11 节)
- const CQUST_TIME_SLOTS = [
- { number: 1, startTime: '08:30', endTime: '09:15' },
- { number: 2, startTime: '09:25', endTime: '10:10' },
- { number: 3, startTime: '10:30', endTime: '11:15' },
- { number: 4, startTime: '11:25', endTime: '12:10' },
- { 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:55', endTime: '20:40' },
- { number: 11, startTime: '20:50', endTime: '21:35' }
- ];
- // Toast 提示
- const toast = (msg) => {
- if (window.shiguangBridge && typeof window.shiguangBridge.showToast === 'function') {
- window.shiguangBridge.showToast(msg);
- } else {
- console.log('[Toast]', msg);
- }
- };
- // 判断是否为 WebVPN
- const isWebvpn = () => window.location.hostname === 'web.cqust.edu.cn';
- // 检查页面域名与路径
- const checkHost = () => {
- const curHost = window.location.hostname;
- if (!ALLOWED_HOSTS.some(h => curHost.includes(h) || curHost === h)) return false;
- if (isWebvpn()) {
- return window.location.pathname.includes('/eams/') || window.location.pathname.includes('fae04f99307e6b416b1b9de29d51367b4912');
- }
- return true;
- };
- // 提取 WebVPN 路径前缀
- const getWebvpnPrefix = () => {
- const m = window.location.pathname.match(/^\/(?:http|https)\/[0-9a-fA-F]+/);
- return m ? m[0] : '';
- };
- // HTTP 请求封装
- const request = async (path, options = {}) => {
- const prefix = getWebvpnPrefix();
- const cleanPath = path.startsWith('/') ? path : `/${path}`;
- const url = path.startsWith('http') ? path : `${window.location.origin}${prefix}${cleanPath}`;
- const resp = await fetch(url, {
- credentials: 'include',
- ...options
- });
- if (!resp.ok) {
- throw new Error(`请求接口异常: ${resp.status} ${resp.statusText}`);
- }
- return await resp.text();
- };
- // 获取所在周周一日期
- const getWeekMondayStr = (date) => {
- const d = new Date(date);
- d.setHours(0, 0, 0, 0);
- const day = d.getDay() === 0 ? 7 : d.getDay();
- d.setDate(d.getDate() - (day - 1));
- const y = d.getFullYear();
- const m = String(d.getMonth() + 1).padStart(2, '0');
- const dayStr = String(d.getDate()).padStart(2, '0');
- return `${y}-${m}-${dayStr}`;
- };
- // 推算学期开始日期
- const calcSemesterStartDate = (schoolYear, termName) => {
- const years = (schoolYear || '').match(/\d{4}/g) || [];
- const isSecond = String(termName) === '2' || (termName && termName.includes('2'));
- if (years.length >= 2) {
- return isSecond ? getWeekMondayStr(`${years[1]}-02-22`) : getWeekMondayStr(`${years[0]}-09-07`);
- }
- const nowYear = new Date().getFullYear();
- return isSecond ? getWeekMondayStr(`${nowYear}-02-22`) : getWeekMondayStr(`${nowYear}-09-07`);
- };
- // 获取排课标识 ids
- const detectParams = async () => {
- const html = await request('/eams/courseTableForStd.action');
- if (html.includes('actionError') || html.includes('login.action') || html.includes('密码错误') || html.includes('用户登录')) {
- throw new Error('未检测到登录状态,请先登录教务系统');
- }
- const idsMatch = html.match(/bg\.form\.addInput\(form,\s*["']ids["'],\s*["'](\d+)["']\)/);
- const tagMatch = html.match(/id=["'](semesterBar\d+Semester)["']/);
- let ids = idsMatch ? idsMatch[1] : null;
- if (!ids) {
- const inputEl = document.querySelector('form input[name="ids"]');
- if (inputEl && inputEl.value) ids = inputEl.value;
- }
- if (!ids) {
- throw new Error('未获取到排课标识 ids');
- }
- const tagId = tagMatch ? tagMatch[1] : 'semesterBar8875271691Semester';
- return { ids, tagId };
- };
- // 选择学期
- const selectSemester = async (tagId) => {
- let semesterList = [];
- let curSemId = '561';
- try {
- const queryRes = await request('/eams/dataQuery.action', {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
- body: `tagId=${encodeURIComponent(tagId)}&dataType=semesterCalendar&value=561&empty=false`
- });
- const semIdMatch = queryRes.match(/semesterId:\s*["']?(\d+)["']?/);
- if (semIdMatch) curSemId = semIdMatch[1];
- const itemRegex = /\{id:(\d+),schoolYear:"([^"]+)",name:"([^"]+)"\}/g;
- let m;
- while ((m = itemRegex.exec(queryRes)) !== null) {
- semesterList.unshift({
- id: m[1],
- schoolYear: m[2],
- name: m[3],
- label: `${m[2]}学年 第${m[3]}学期${m[1] === curSemId ? ' (当前)' : ''}`
- });
- }
- } catch (e) {
- console.warn('[获取学期列表异常]', e);
- }
- if (!semesterList.length) {
- const nowYear = new Date().getFullYear();
- return {
- id: curSemId,
- schoolYear: `${nowYear}-${nowYear + 1}`,
- name: '1',
- label: '当前学期'
- };
- }
- let defaultIdx = semesterList.findIndex(s => s.id === curSemId);
- if (defaultIdx < 0) defaultIdx = 0;
- if (window.shiguangBridgePromise && typeof window.shiguangBridgePromise.showSingleSelection === 'function') {
- const labels = semesterList.map(s => s.label);
- const chosenIdx = await window.shiguangBridgePromise.showSingleSelection(
- '请选择需要导入的学期',
- JSON.stringify(labels),
- defaultIdx
- );
- if (chosenIdx === null || chosenIdx < 0) {
- return null;
- }
- return semesterList[chosenIdx];
- }
- return semesterList[defaultIdx];
- };
- // 解析课表数据
- const parseCourseTableHtml = (html) => {
- const rawSlots = [];
- const creditMap = new Map();
- // 提取课程代码与学分映射
- const cReg = />([A-Za-z0-9._-]+)<\/a>\s*<\/td>\s*<td>([^<]+)<\/td>\s*<td>([0-9.]+)<\/td>/g;
- let cm;
- while ((cm = cReg.exec(html)) !== null) {
- if (cm[1]) creditMap.set(cm[1].trim(), cm[3].trim());
- if (cm[2]) creditMap.set(cm[2].trim(), cm[3].trim());
- }
- // 解析 TaskActivity 课程
- const scriptMatch = html.match(/var\s+table0\s*=\s*new\s+CourseTable[\s\S]*?<\/script>/);
- if (scriptMatch) {
- const lines = scriptMatch[0].split('\n');
- let curAct = null;
- const actReg = /activity\s*=\s*new\s+TaskActivity\((.*)\);/;
- const idxReg = /index\s*=\s*(\d+)\s*\*\s*unitCount\s*\+\s*(\d+);/;
- const strReg = /"([^"]*)"/g;
- for (const lineRaw of lines) {
- const line = lineRaw.trim();
- const aM = line.match(actReg);
- if (aM) {
- const args = [];
- let sm;
- while ((sm = strReg.exec(aM[1])) !== null) args.push(sm[1]);
- if (args.length >= 7) {
- const rawName = args[3] || '';
- const cleanName = rawName.replace(/\([A-Za-z0-9._-]+\)$/, '').trim() || rawName;
- curAct = {
- teacher: args[1] || '',
- name: cleanName,
- room: args[5] || '',
- weeksStr: args[6] || ''
- };
- }
- }
- const iM = line.match(idxReg);
- if (iM && curAct) {
- const day = parseInt(iM[1], 10) + 1;
- const sec = parseInt(iM[2], 10) + 1;
- const weeks = [];
- for (let w = 1; w < curAct.weeksStr.length; w++) {
- if (curAct.weeksStr[w] === '1') weeks.push(w);
- }
- rawSlots.push({
- name: curAct.name,
- teacher: curAct.teacher,
- position: curAct.room,
- day,
- section: sec,
- weeks
- });
- }
- }
- }
- // 合并同天连续节次
- const groups = new Map();
- for (const slot of rawSlots) {
- const key = `${slot.name}|${slot.teacher}|${slot.position}|${slot.day}|${slot.weeks.join(',')}`;
- if (!groups.has(key)) groups.set(key, []);
- groups.get(key).push(slot);
- }
- const mergedCourses = [];
- for (const slots of groups.values()) {
- if (!slots.length) continue;
- slots.sort((a, b) => a.section - b.section);
- let startSec = slots[0].section;
- let endSec = slots[0].section;
- for (let i = 1; i < slots.length; i++) {
- if (slots[i].section === endSec + 1) {
- endSec = slots[i].section;
- } else {
- mergedCourses.push({
- name: slots[0].name,
- teacher: slots[0].teacher,
- position: slots[0].position,
- day: slots[0].day,
- startSection: startSec,
- endSection: endSec,
- weeks: slots[0].weeks
- });
- startSec = slots[i].section;
- endSec = slots[i].section;
- }
- }
- mergedCourses.push({
- name: slots[0].name,
- teacher: slots[0].teacher,
- position: slots[0].position,
- day: slots[0].day,
- startSection: startSec,
- endSection: endSec,
- weeks: slots[0].weeks
- });
- }
- // 解析未安排时间任务列表
- const unarrangedMatch = html.match(/未安排时间任务列表[\s\S]*?<table[^>]*>([\s\S]*?)<\/table>/i);
- if (unarrangedMatch) {
- const tableHtml = unarrangedMatch[1];
- const trReg = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
- const strip = /<[^>]+>/g;
- let tr;
- while ((tr = trReg.exec(tableHtml)) !== null) {
- const tds = (tr[1].match(/<td[^>]*>[\s\S]*?<\/td>/gi) || []).map(td => td.replace(strip, '').trim());
- if (tds.length >= 8 && /^\d+$/.test(tds[0])) {
- const name = tds[2];
- const teacher = tds[5] || '';
- const weeksStr = tds[6] || '';
- const weeks = [];
- if (weeksStr) {
- const parts = weeksStr.split(/[,,]/);
- for (const p of parts) {
- const range = p.trim().match(/^(\d+)\s*[-~至]\s*(\d+)$/);
- if (range) {
- const s = parseInt(range[1], 10);
- const e = parseInt(range[2], 10);
- for (let w = s; w <= e; w++) weeks.push(w);
- } else {
- const single = parseInt(p.trim(), 10);
- if (!isNaN(single)) weeks.push(single);
- }
- }
- }
- if (name) {
- mergedCourses.push({
- name,
- teacher,
- position: '集中实践',
- day: 0,
- startSection: 0,
- endSection: 0,
- weeks: weeks.length ? weeks : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
- });
- }
- }
- }
- }
- return mergedCourses;
- };
- // 导入主流程
- const runImport = async () => {
- if (!checkHost()) {
- throw new Error('请先登录并进入教务系统页面');
- }
- const { ids, tagId } = await detectParams();
- const semester = await selectSemester(tagId);
- if (!semester) {
- toast('已取消学期选择');
- return;
- }
- const courseHtml = await request('/eams/courseTableForStd!courseTable.action', {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: `ignoreHead=1&setting.kind=std&startWeek=1&project.id=1&semester.id=${semester.id}&ids=${ids}`
- });
- if (!courseHtml.includes('TaskActivity')) {
- throw new Error('未查询到该学期的有效排课数据');
- }
- const courses = parseCourseTableHtml(courseHtml);
- if (!courses || !courses.length) {
- throw new Error('未能从课表页面解析出课程记录');
- }
- // 计算最大周次与开学日期
- let maxWeek = 20;
- courses.forEach(c => {
- if (Array.isArray(c.weeks) && c.weeks.length) {
- const m = Math.max(...c.weeks);
- if (m > maxWeek) maxWeek = m;
- }
- });
- const semesterStartDate = calcSemesterStartDate(semester.schoolYear, semester.name);
- const config = {
- semesterStartDate,
- semesterTotalWeeks: maxWeek,
- defaultClassDuration: 45,
- defaultBreakDuration: 10
- };
- // 保存学期配置
- if (window.shiguangBridgePromise && typeof window.shiguangBridgePromise.saveCourseConfig === 'function') {
- await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
- }
- // 保存作息时间
- if (window.shiguangBridgePromise && typeof window.shiguangBridgePromise.savePresetTimeSlots === 'function') {
- await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(CQUST_TIME_SLOTS));
- }
- // 保存课程列表
- if (window.shiguangBridgePromise && typeof window.shiguangBridgePromise.saveImportedCourses === 'function') {
- const ok = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
- if (ok) {
- toast(`导入成功,共 ${courses.length} 门课程`);
- } else {
- toast('课程数据保存完成');
- }
- } else {
- console.log('[课程数据]', courses);
- toast(`解析成功,共 ${courses.length} 门课程`);
- }
- };
- try {
- await runImport();
- } catch (err) {
- console.error('[CQUST 导入异常]', err);
- toast(`导入失败: ${err.message || '未知错误'}`);
- } finally {
- if (window.shiguangBridge && typeof window.shiguangBridge.notifyTaskCompletion === 'function') {
- window.shiguangBridge.notifyTaskCompletion();
- }
- }
- })();
|