Просмотр исходного кода

Merge pull request #712 from XingHeYuZhuan/pending

星河欲转 3 дней назад
Родитель
Сommit
7b6ffa934f
3 измененных файлов с 425 добавлено и 0 удалено
  1. 5 0
      index/root_index.yaml
  2. 17 0
      resources/CQUST/adapters.yaml
  3. 403 0
      resources/CQUST/cqust_01.js

+ 5 - 0
index/root_index.yaml

@@ -58,6 +58,11 @@ schools:
     initial: "C"
     resource_folder: "CQUT"
 
+  - id: "CQUST"
+    name: "重庆科技大学"
+    initial: "C"
+    resource_folder: "CQUST"
+
   - id: "CQTBI"
     name: "重庆工商职业学院"
     initial: "C"

+ 17 - 0
resources/CQUST/adapters.yaml

@@ -0,0 +1,17 @@
+# resources/CQUST/adapters.yaml
+adapters:
+  - adapter_id: "CQUST_01"
+    adapter_name: "重庆科技大学本科教务 (IPv6推荐)"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "cqust_01.js"
+    import_url: "http://jwnew.cqust.edu.ex2.http.80.ipv6.cqust.edu.cn/eams/login.action"
+    maintainer: "yuexps"
+    description: "重庆科技大学树维EAMS教务系统 IPv6 通道,校外网络访问更稳定"
+
+  - adapter_id: "CQUST_02"
+    adapter_name: "重庆科技大学本科教务 (IPv4备用)"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "cqust_01.js"
+    import_url: "http://jwnew.cqust.edu.cn/eams/login.action"
+    maintainer: "yuexps"
+    description: "重庆科技大学树维EAMS教务系统 IPv4 备用通道,当无 IPv6 环境时选用"

+ 403 - 0
resources/CQUST/cqust_01.js

@@ -0,0 +1,403 @@
+/**
+ * 重庆科技大学 (CQUST) 树维EAMS教务系统课表导入脚本
+ * 适用系统:树维 EAMS 教学管理系统 (本科)
+ * 适配支持:拾光课程表 (shiguangschedule)
+ * Maintainer: yuexps
+ */
+
+(async () => {
+    'use strict';
+
+    // 允许的教务系统域名(IPv6 专线与 IPv4 备用)
+    const ALLOWED_HOSTS = [
+        '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);
+        }
+    };
+
+    // 检查是否在教务系统域名下
+    const checkHost = () => {
+        const curHost = window.location.hostname;
+        return ALLOWED_HOSTS.some(h => curHost.includes(h) || curHost === h);
+    };
+
+    // 通用 HTTP 请求封装(自动带凭据与当前域名)
+    const request = async (path, options = {}) => {
+        const url = path.startsWith('http') ? path : `${window.location.origin}${path}`;
+        const resp = await fetch(url, {
+            credentials: 'include',
+            ...options
+        });
+        if (!resp.ok) {
+            throw new Error(`请求接口异常: ${resp.status} ${resp.statusText}`);
+        }
+        return await resp.text();
+    };
+
+    // 获取周一日期(格式化为 YYYY-MM-DD)
+    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('未能获取学生排课标识,请确认当前账号有选课排课权限');
+        }
+
+        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) {
+            return {
+                id: curSemId,
+                schoolYear: '2026-2027',
+                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];
+    };
+
+    // 解析 TaskActivity 课表与未排实践课程
+    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(`请先进入重庆科技大学教务系统 (jwnew.cqust.edu.cn)`);
+        }
+
+        toast('正在检查教务系统登录状态...');
+        const { ids, tagId } = await detectParams();
+
+        toast('正在获取可用学期日历...');
+        const semester = await selectSemester(tagId);
+        if (!semester) {
+            toast('已取消学期选择');
+            return;
+        }
+
+        toast(`正在同步 ${semester.label || ''} 课表...`);
+        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
+        };
+
+        toast('正在保存课程与作息配置...');
+
+        // 保存学期配置
+        if (window.shiguangBridgePromise && typeof window.shiguangBridgePromise.saveCourseConfig === 'function') {
+            await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+        }
+
+        // 保存重科 11 节作息时间
+        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();
+        }
+    }
+})();