Prechádzať zdrojové kódy

Merge pull request #788 from ShiGuangSchedule/pending

星河欲转 1 mesiac pred
rodič
commit
1d7ead4927

+ 3 - 6
README.md

@@ -17,12 +17,9 @@ shiguang_warehouse/
 ├───index/
 │   └───root_index.yaml      # 整个适配器仓库的根索引文件
 ├───resources/               # 资源目录
-│   ├───CUST/                # 学校目录
-│   │   ├───adapters.yaml    # 配置信息
-│   │   └───cust.js          # 适配脚本
-│   ├───GLOBAL_TOOLS/        # 通用工具
-│   │   ├───adapters.yaml
-│   │   ├───school.js        # 组件测试脚本
+│   ├───GLOBAL_TOOLS/        # 通用工具目录(目录名称和index规定的一致)
+│   │   ├───adapters.yaml    # 学校适配器配置信息(可以存放多个适配器)
+│   │   ├───school.js        # 适配代码(javascript文件,与学校适配器配置信息对应)   
 │   │   └───...
 │   └───...
 ├───proto/

+ 3 - 3
resources/CUST/adapters.yaml

@@ -1,9 +1,9 @@
 # resources/CUST/adapters.yaml
 adapters:
   - adapter_id: "CUST_01"
-    adapter_name: "长春理工大学教务系统"
+    adapter_name: "长春理工大学教务系统(朝阳校区)"
     category: "BACHELOR_AND_ASSOCIATE"
-    asset_js_path: "cust.js"
+    asset_js_path: "cust_chaoyang.js"
     import_url: "https://mysso.cust.edu.cn/cas/login?service=https://jwgl.cust.edu.cn/welcome"
     maintainer: "itdong"
-    description: ""
+    description: "适用于朝阳校区,导入个人课程表并配置第 1~12 节作息时间。"

+ 0 - 675
resources/CUST/cust.js

@@ -1,675 +0,0 @@
-// 文件: school.js
-// 长春理工大学教务系统课程表导入脚本
-
-// 检查是否在正确的页面
-function isOnStudentPage() {
-    const url = window.location.href;
-    return /jwgls[0-4]\.cust\.edu\.cn\/Student/i.test(url);
-}
-
-// 解析周数字符串(如 "1-8周[1-2节]" -> [1,2,3,4,5,6,7,8])
-function parseWeeks(timeStr) {
-    const weeks = [];
-    const weekMatch = timeStr.match(/(\d+)(?:-(\d+))?周/);
-    if (weekMatch) {
-        const start = parseInt(weekMatch[1]);
-        const end = weekMatch[2] ? parseInt(weekMatch[2]) : start;
-        for (let i = start; i <= end; i++) {
-            weeks.push(i);
-        }
-    }
-    return weeks;
-}
-
-// 解析节次字符串(如 "第1-2节" -> {start: 1, end: 2})
-function parseSections(sectionStr) {
-    const match = sectionStr.match(/第(\d+)(?:-(\d+))?节/);
-    if (match) {
-        const start = parseInt(match[1]);
-        const end = match[2] ? parseInt(match[2]) : start;
-        return { start, end };
-    }
-    return null;
-}
-
-// 从API响应中提取并转换课程数据
-function convertScheduleData(apiData) {
-    const coursesMap = new Map(); // 使用Map来临时存储和合并课程
-    const timeSlots = new Set();
-
-    // 遍历每一天
-    apiData.data.AdjustDays.forEach(day => {
-        const dayIndex = day.WIndex; // 1=周一, 7=周日
-
-        // 处理所有时间段
-        const allTimePieces = [
-            ...(day.AM__TimePieces || []),
-            ...(day.PM__TimePieces || []),
-            ...(day.EV__TimePieces || [])
-        ];
-
-        allTimePieces.forEach(timePiece => {
-            // 记录时间段
-            if (timePiece.StartSection && timePiece.EndSection) {
-                timeSlots.add(JSON.stringify({
-                    startSection: timePiece.StartSection,
-                    endSection: timePiece.EndSection,
-                    startTime: timePiece.StartTime,
-                    endTime: timePiece.EndTime
-                }));
-            }
-
-            // 处理课程
-            if (timePiece.Dtos && timePiece.Dtos.length > 0) {
-                timePiece.Dtos.forEach(dto => {
-                    const content = dto.Content || [];
-
-                    // 提取课程信息
-                    let courseName = '';
-                    let teacher = [];
-                    let room = '';
-                    let weeks = [];
-                    let timeInfo = '';
-
-                    content.forEach(item => {
-                        switch(item.Key) {
-                            case 'Lesson':
-                                courseName = item.Name;
-                                break;
-                            case 'Teacher':
-                                teacher.push(item.Name);
-                                break;
-                            case 'Room':
-                                room = item.Name;
-                                break;
-                            case 'Time':
-                                timeInfo = item.Name;
-                                weeks = parseWeeks(item.Name);
-                                break;
-                        }
-                    });
-
-                    if (courseName && weeks.length > 0) {
-                        // 创建唯一键来识别相同的课程(同一天、同一课程、同一老师、同一地点、同一周次)
-                        const courseKey = `${dayIndex}-${courseName}-${teacher.join('、')}-${room}-${weeks.join(',')}`;
-
-                        if (coursesMap.has(courseKey)) {
-                            // 如果已存在,更新节次范围
-                            const existingCourse = coursesMap.get(courseKey);
-                            existingCourse.startSection = Math.min(existingCourse.startSection, timePiece.StartSection);
-                            existingCourse.endSection = Math.max(existingCourse.endSection, timePiece.EndSection);
-                        } else {
-                            // 如果不存在,创建新课程
-                            coursesMap.set(courseKey, {
-                                name: courseName,
-                                teacher: teacher.join('、'),
-                                position: room || '未指定',
-                                day: dayIndex,
-                                startSection: timePiece.StartSection,
-                                endSection: timePiece.EndSection,
-                                weeks: weeks
-                            });
-                        }
-                    }
-                });
-            }
-        });
-    });
-
-    // 将Map转换为数组
-    const courses = Array.from(coursesMap.values());
-
-    // 按照星期和节次排序
-    courses.sort((a, b) => {
-        if (a.day !== b.day) return a.day - b.day;
-        if (a.startSection !== b.startSection) return a.startSection - b.startSection;
-        return a.endSection - b.endSection;
-    });
-
-    console.log(`合并后的课程数量: ${courses.length}`);
-
-    return { courses, timeSlots: Array.from(timeSlots).map(s => JSON.parse(s)) };
-}
-
-// 获取课表数据
-async function fetchScheduleData() {
-    try {
-        console.log('正在获取课表数据...');
-
-        // 构建请求参数(完整的参数结构)
-        const requestData = {
-            "param": "JTdCJTdE",
-            "__permission": {
-                "MenuID": "00000000-0000-0000-0000-000000000000",
-                "Operate": "select",
-                "Operation": 0
-            },
-            "__log": {
-                "MenuID": "00000000-0000-0000-0000-000000000000",
-                "Logtype": 6,
-                "Context": "查询"
-            }
-        };
-
-        // 获取当前域名
-        const currentHost = window.location.hostname;
-        const apiUrl = `https://${currentHost}/api/ClientStudent/Home/StudentHomeApi/QueryStudentScheduleData`;
-
-        const response = await fetch(apiUrl, {
-            method: 'POST',
-            headers: {
-                'Accept': 'application/json, text/plain, */*',
-                'Content-Type': 'application/json;charset=UTF-8',
-                'Cache-Control': 'no-cache',
-                'Pragma': 'no-cache'
-            },
-            credentials: 'include',
-            body: JSON.stringify(requestData)
-        });
-
-        if (!response.ok) {
-            throw new Error(`HTTP error! status: ${response.status}`);
-        }
-
-        const data = await response.json();
-
-        if (data.state !== 0) {
-            throw new Error(data.message || '获取课表失败');
-        }
-
-        console.log('成功获取课表数据:', data);
-        return data;
-
-    } catch (error) {
-        console.error('获取课表数据失败:', error);
-        window.shiguangBridge.showToast('获取课表失败: ' + error.message);
-        return null;
-    }
-}
-
-// 生成时间段配置
-function generateTimeSlots(timeSlotsFromAPI) {
-    const defaultTimeSlots = [
-        { "number": 1, "startTime": "08:00", "endTime": "08:45" },
-        { "number": 2, "startTime": "08:55", "endTime": "09:35" },
-        { "number": 3, "startTime": "10:05", "endTime": "10:50" },
-        { "number": 4, "startTime": "11:00", "endTime": "11:40" },
-        { "number": 5, "startTime": "13:30", "endTime": "14:15" },
-        { "number": 6, "startTime": "14:25", "endTime": "15:05" },
-        { "number": 7, "startTime": "15:35", "endTime": "16:20" },
-        { "number": 8, "startTime": "16:30", "endTime": "17:10" },
-        { "number": 9, "startTime": "18:00", "endTime": "18:45" },
-        { "number": 10, "startTime": "18:45", "endTime": "19:35" },
-        { "number": 11, "startTime": "19:45", "endTime": "20:30" },
-        { "number": 12, "startTime": "20:30", "endTime": "21:20" }
-    ];
-
-    // 如果API提供了时间信息,更新默认时间
-    if (timeSlotsFromAPI && timeSlotsFromAPI.length > 0) {
-        timeSlotsFromAPI.forEach(slot => {
-            for (let i = slot.startSection; i <= slot.endSection; i++) {
-                const timeSlot = defaultTimeSlots.find(t => t.number === i);
-                if (timeSlot && i === slot.startSection) {
-                    timeSlot.startTime = slot.startTime;
-                }
-                if (timeSlot && i === slot.endSection) {
-                    timeSlot.endTime = slot.endTime;
-                }
-            }
-        });
-    }
-
-    return defaultTimeSlots;
-}
-
-// 主函数:导入课程
-async function importCourseSchedule() {
-    try {
-        console.log('开始导入课程表...');
-        window.shiguangBridge.showToast('正在获取课表数据...');
-
-        // 获取课表数据
-        const scheduleData = await fetchScheduleData();
-        if (!scheduleData) {
-            return false;
-        }
-
-        // 转换数据
-        const { courses, timeSlots } = convertScheduleData(scheduleData);
-
-        if (courses.length === 0) {
-            window.shiguangBridge.showToast('未找到课程数据');
-            return false;
-        }
-
-        console.log(`找到 ${courses.length} 门课程`);
-        console.log('课程数据:', courses);
-
-        // 导入课程
-        const coursesResult = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
-        if (coursesResult === true) {
-            console.log('课程导入成功!');
-            window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
-        } else {
-            console.log('课程导入失败');
-            window.shiguangBridge.showToast('课程导入失败');
-            return false;
-        }
-
-        // 生成并导入时间段
-        const finalTimeSlots = generateTimeSlots(timeSlots);
-        console.log('时间段配置:', finalTimeSlots);
-
-        const timeSlotsResult = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(finalTimeSlots));
-        if (timeSlotsResult === true) {
-            console.log('时间段导入成功!');
-            window.shiguangBridge.showToast('时间段配置成功!');
-        } else {
-            console.log('时间段导入失败');
-            window.shiguangBridge.showToast('时间段配置失败');
-        }
-
-        return true;
-
-    } catch (error) {
-        console.error('导入过程出错:', error);
-        window.shiguangBridge.showToast('导入失败: ' + error.message);
-        return false;
-    }
-}
-
-// ========== 以下是原有的演示函数(保留以供测试) ==========
-
-// 1. 显示一个公告信息弹窗
-async function demoAlert() {
-    try {
-        console.log("即将显示公告弹窗...");
-        const confirmed = await window.shiguangBridgePromise.showAlert(
-            "重要通知",
-            "这是一个弹窗示例。",
-            "好的"
-        );
-        if (confirmed) {
-            console.log("用户点击了确认按钮。Alert Promise Resolved: " + confirmed);
-            window.shiguangBridge.showToast("Alert:用户点击了确认!");
-            return true; // 成功时返回 true
-        } else {
-            console.log("用户点击了取消按钮或关闭了弹窗。Alert Promise Resolved: " + confirmed);
-            window.shiguangBridge.showToast("Alert:用户取消了!");
-            return false; // 用户取消时返回 false
-        }
-    } catch (error) {
-        console.error("显示公告弹窗时发生错误:", error);
-        window.shiguangBridge.showToast("Alert:显示弹窗出错!" + error.message);
-        return false; // 出现错误时也返回 false
-    }
-}
-
-// 2. 显示带输入框的弹窗,并进行简单验证
-function validateName(name) {
-    if (name === null || name.trim().length === 0) {
-        return "输入不能为空!";
-    }
-    if (name.length < 2) {
-        return "姓名至少需要2个字符!";
-    }
-    return false;
-}
-
-async function demoPrompt() {
-    try {
-        console.log("即将显示输入框弹窗...");
-        const name = await window.shiguangBridgePromise.showPrompt(
-            "输入你的姓名",
-            "请输入至少2个字符",
-            "测试用户",
-            "validateName"
-        );
-        if (name !== null) {
-            console.log("用户输入的姓名是: " + name);
-            window.shiguangBridge.showToast("欢迎你," + name + "!");
-            return true; // 成功时返回 true
-        } else {
-            console.log("用户取消了输入。");
-            window.shiguangBridge.showToast("Prompt:用户取消了输入!");
-            return false; // 用户取消时返回 false
-        }
-    } catch (error) {
-        console.error("显示输入框弹窗时发生错误:", error);
-        window.shiguangBridge.showToast("Prompt:显示输入框出错!" + error.message);
-        return false; // 出现错误时也返回 false
-    }
-}
-
-// 3. 显示一个单选列表弹窗
-async function demoSingleSelection() {
-    const fruits = ["苹果", "香蕉", "橙子", "葡萄", "西瓜", "芒果"];
-    try {
-        console.log("即将显示单选列表弹窗...");
-        const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
-            "选择你喜欢的水果",
-            JSON.stringify(fruits),
-            2
-        );
-        if (selectedIndex !== null && selectedIndex >= 0 && selectedIndex < fruits.length) {
-            console.log("用户选择了: " + fruits[selectedIndex] + " (索引: " + selectedIndex + ")");
-            window.shiguangBridge.showToast("你选择了 " + fruits[selectedIndex]);
-            return true; // 成功时返回 true
-        } else {
-            console.log("用户取消了选择。");
-            window.shiguangBridge.showToast("Single Selection:用户取消了选择!");
-            return false; // 用户取消时返回 false
-        }
-    } catch (error) {
-        console.error("显示单选列表弹窗时发生错误:", error);
-        window.shiguangBridge.showToast("Single Selection:显示列表出错!" + error.message);
-        return false; // 出现错误时也返回 false
-    }
-}
-
-// 仍然可以使用原始的 AndroidBridge 对象
-window.shiguangBridge.showToast("这是一个来自 JS 的 Toast 消息,会很快消失!");
-
-async function demoSaveCourses() {
-    // ... 代码保持不变 ...
-    console.log("正在准备测试课程数据...");
-    const testCourses = [
-    {
-        "name": "高等数学",
-        "teacher": "张教授",
-        "position": "教101",
-        "day": 1,
-        "startSection": 1,
-        "endSection": 2,
-        "weeks": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
-    },
-    {
-        "name": "大学英语",
-        "teacher": "李老师",
-        "position": "文史楼203",
-        "day": 1,
-        "startSection": 2,
-        "endSection": 4,
-        "weeks": [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
-    },
-    {
-        "name": "数据结构",
-        "teacher": "王副教授",
-        "position": "信息楼B301",
-        "day": 7,
-        "startSection": 2,
-        "endSection": 2,
-        "weeks": [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
-    },
-    {
-        "name": "数据结构",
-        "teacher": "王副教授",
-        "position": "信息楼B301",
-        "day": 7,
-        "startSection": 3,
-        "endSection": 3,
-        "weeks": [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
-    },
-    {
-        "name": "数据结构",
-        "teacher": "王副教授",
-        "position": "信息楼B301",
-        "day": 7,
-        "startSection": 4,
-        "endSection": 4,
-        "weeks": [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
-    },
-    {
-        "name": "数据结构",
-        "teacher": "王副教授",
-        "position": "信息楼B301",
-        "day": 7,
-        "startSection": 5,
-        "endSection": 5,
-        "weeks": [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
-    },
-    {
-        "name": "数据结构",
-        "teacher": "王副教授",
-        "position": "信息楼B301",
-        "day": 7,
-        "startSection": 6,
-        "endSection": 6,
-        "weeks": [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
-    },
-    {
-        "name": "计算机组成原理",
-        "teacher": "赵教授",
-        "position": "实验楼401",
-        "day": 4,
-        "startSection": 4,
-        "endSection": 4,
-        "weeks": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
-    },
-    {
-        "name": "操作系统",
-        "teacher": "钱副教授",
-        "position": "信息楼C205",
-        "day": 5,
-        "startSection": 5,
-        "endSection": 5,
-        "weeks": [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
-    },
-    {
-        "name": "计算机网络",
-        "teacher": "孙教授",
-        "position": "信息楼D103",
-        "day": 6,
-        "startSection": 6,
-        "endSection": 6,
-        "weeks": [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
-    },
-    {
-        "name": "软件工程",
-        "teacher": "周副教授",
-        "position": "创新楼301",
-        "day": 7,
-        "startSection": 7,
-        "endSection": 7,
-        "weeks": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
-    },
-    {
-        "name": "数据库原理",
-        "teacher": "吴教授",
-        "position": "信息楼E201",
-        "day": 1,
-        "startSection": 8,
-        "endSection": 8,
-        "weeks": [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
-    },
-    {
-        "name": "人工智能",
-        "teacher": "郑副教授",
-        "position": "智能楼101",
-        "day": 2,
-        "startSection": 9,
-        "endSection": 9,
-        "weeks": [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
-    },
-    {
-        "name": "机器学习",
-        "teacher": "冯教授",
-        "position": "智能楼203",
-        "day": 3,
-        "startSection": 10,
-        "endSection": 10,
-        "weeks": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
-    },
-    {
-        "name": "编译原理",
-        "teacher": "陈副教授",
-        "position": "信息楼F105",
-        "day": 4,
-        "startSection": 11,
-        "endSection": 11,
-        "weeks": [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
-    },
-    {
-        "name": "计算机图形学",
-        "teacher": "褚教授",
-        "position": "图形楼301",
-        "day": 5,
-        "startSection": 12,
-        "endSection": 12,
-        "weeks": [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
-    },
-    {
-        "name": "网络安全",
-        "teacher": "卫副教授",
-        "position": "安全楼201",
-        "day": 6,
-        "startSection": 13,
-        "endSection": 13,
-        "weeks": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
-    },
-    {
-        "name": "分布式系统",
-        "teacher": "蒋教授",
-        "position": "云楼101",
-        "day": 7,
-        "startSection": 14,
-        "endSection": 14,
-        "weeks": [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
-    },
-    {
-        "name": "大数据技术",
-        "teacher": "沈副教授",
-        "position": "数据楼301",
-        "day": 1,
-        "startSection": 15,
-        "endSection": 15,
-        "weeks": [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
-    },
-    {
-        "name": "物联网技术",
-        "teacher": "韩教授",
-        "position": "物联楼201",
-        "day": 2,
-        "startSection": 16,
-        "endSection": 16,
-        "weeks": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
-    }
-    ];
-
-    try {
-        console.log("正在尝试导入课程...");
-        const result = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(testCourses));
-        if (result === true) {
-            console.log("课程导入成功!");
-            window.shiguangBridge.showToast("测试课程导入成功!");
-        } else {
-            console.log("课程导入未成功,结果:" + result);
-            window.shiguangBridge.showToast("测试课程导入失败,请查看日志。");
-        }
-    } catch (error) {
-        console.error("导入课程时发生错误:", error);
-        window.shiguangBridge.showToast("导入课程失败: " + error.message);
-    }
-}
-
-// 5. 导入预设时间段(保持不变)
-async function importPresetTimeSlots() {
-    console.log("正在准备预设时间段数据...");
-    const presetTimeSlots = [
-        { "number": 1, "startTime": "08:00", "endTime": "08:01" },
-        { "number": 2, "startTime": "09:00", "endTime": "09:01" },
-        { "number": 3, "startTime": "10:00", "endTime": "10:01" },
-        { "number": 4, "startTime": "11:00", "endTime": "11:01" },
-        { "number": 5, "startTime": "12:00", "endTime": "12:01" },
-        { "number": 6, "startTime": "13:00", "endTime": "13:01" },
-        { "number": 7, "startTime": "14:00", "endTime": "14:01" },
-        { "number": 8, "startTime": "15:00", "endTime": "15:01" },
-        { "number": 9, "startTime": "16:00", "endTime": "16:01" },
-        { "number": 10, "startTime": "17:00", "endTime": "17:01" },
-        { "number": 11, "startTime": "18:00", "endTime": "18:01" },
-        { "number": 12, "startTime": "19:00", "endTime": "19:01" },
-        { "number": 13, "startTime": "20:00", "endTime": "20:01" },
-        { "number": 14, "startTime": "21:00", "endTime": "21:01" },
-        { "number": 15, "startTime": "22:00", "endTime": "22:01" },
-        { "number": 16, "startTime": "23:00", "endTime": "23:01" }
-    ];
-
-    try {
-        console.log("正在尝试导入预设时间段...");
-        const result = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
-        if (result === true) {
-            console.log("预设时间段导入成功!");
-            window.shiguangBridge.showToast("测试时间段导入成功!");
-        } else {
-            console.log("预设时间段导入未成功,结果:" + result);
-            window.shiguangBridge.showToast("测试时间段导入失败,请查看日志。");
-        }
-    } catch (error) {
-        console.error("导入时间段时发生错误:", error);
-        window.shiguangBridge.showToast("导入时间段失败: " + error.message);
-    }
-}
-
-/**
- * 编排这些异步操作,并在用户取消时停止后续执行。
- */
-async function runAllDemosSequentially() {
-    window.shiguangBridge.showToast("所有演示将按顺序开始...");
-
-    // 1. 运行第一个演示:Alert
-    const alertResult = await demoAlert();
-    if (!alertResult) {
-        console.log("用户取消了 Alert 演示,停止后续执行。");
-        return; // 用户取消,立即退出函数
-    }
-
-    // 2. 运行第二个演示:Prompt
-    const promptResult = await demoPrompt();
-    if (!promptResult) {
-        console.log("用户取消了 Prompt 演示,停止后续执行。");
-        return; // 用户取消,立即退出函数
-    }
-
-    // 3. 运行第三个演示:SingleSelection
-    const selectionResult = await demoSingleSelection();
-    if (!selectionResult) {
-        console.log("用户取消了 Single Selection 演示,停止后续执行。");
-        return; // 用户取消,立即退出函数
-    }
-
-    console.log("所有弹窗演示已完成。");
-    window.shiguangBridge.showToast("所有弹窗演示已完成!");
-
-    // 以下是数据导入,与用户交互无关,可以继续
-    await demoSaveCourses();
-    await importPresetTimeSlots();
-
-    // 发送最终的生命周期完成信号
-    window.shiguangBridge.notifyTaskCompletion();
-}
-
-// ========== 主执行逻辑 ==========
-
-// 检查页面并执行相应操作
-if (isOnStudentPage()) {
-    console.log('检测到在长春理工大学教务系统学生页面');
-    window.shiguangBridge.showToast('正在准备导入课程表...');
-
-    // 延迟一秒确保页面加载完成
-    setTimeout(async () => {
-        const success = await importCourseSchedule();
-        if (success) {
-            window.shiguangBridge.notifyTaskCompletion();
-        }
-    }, 1000);
-
-} else {
-    console.log('当前不在教务系统页面,运行演示模式');
-    window.shiguangBridge.showToast('请先登录教务系统!');
-
-    // 可选:运行演示
-    const runDemo = false; // 设为true可以运行演示
-    if (runDemo) {
-        runAllDemosSequentially();
-    }
-}

+ 481 - 0
resources/CUST/cust_chaoyang.js

@@ -0,0 +1,481 @@
+// 文件: cust_chaoyang.js
+// 长春理工大学朝阳校区 kbpro 课程表导入脚本
+//
+// 适用校区:朝阳校区;沿用现有第 1~12 节作息配置。
+// 导入学生个人课表,课程地点仍按接口返回的校区、教学楼和教室显示。
+// 导入流程:判断页面 → 获取学期和课程 → 合并课程 → 保存课程 → 保存每节作息 → 通知完成。
+// 数据分开保存:课程包含星期、周次和起止节次;作息表包含各节次的开始、结束时间。
+// 本脚本使用 V2 的 window.shiguangBridge / window.shiguangBridgePromise。
+
+// 课表页面用于建立当前登录会话;两个接口请求均使用该会话的 Cookie。
+const SCHEDULE_PAGE_URL = 'https://kbpro.cust.edu.cn/Schedule/';
+const SCHEDULE_INFO_API = 'https://kbpro.cust.edu.cn/Schedule/scheduleInfo';
+const SCHEDULE_JSON_API = 'https://kbpro.cust.edu.cn/Schedule/getSchedulejson';
+
+// 判断当前是否已经进入新版课表页面
+function isOnSchedulePage() {
+    try {
+        const url = new URL(window.location.href);
+        return url.hostname.toLowerCase() === 'kbpro.cust.edu.cn'
+            && url.pathname.toLowerCase().startsWith('/schedule');
+    } catch (error) {
+        return false;
+    }
+}
+
+// 直接进入新版课表页面,不再进入旧教务系统 Student 页面
+// 跳转只负责打开目标页面;当前执行不会在跳转后接着请求数据,需要在新页面执行脚本。
+function redirectToSchedulePage() {
+    console.log('当前不在新版课表页面,正在跳转:', SCHEDULE_PAGE_URL);
+    window.shiguangBridge.showToast('正在打开课程表...');
+    window.location.replace(SCHEDULE_PAGE_URL);
+}
+
+// 通用 GET JSON 请求。Cookie 由浏览器当前登录会话自动携带,不写死 JSESSIONID。
+async function fetchJson(url, description) {
+    const response = await fetch(url, {
+        method: 'GET',
+        headers: {
+            'Accept': 'application/json, text/plain, */*',
+            'Cache-Control': 'no-cache',
+            'Pragma': 'no-cache'
+        },
+        credentials: 'include',
+        cache: 'no-store'
+    });
+
+    if (!response.ok) {
+        throw new Error(`${description}失败,HTTP ${response.status}`);
+    }
+
+    const contentType = response.headers.get('content-type') || '';
+    // 登录失效时可能返回登录页 HTML,不能把它当作课程 JSON 继续导入。
+    if (!contentType.toLowerCase().includes('application/json')) {
+        throw new Error(`${description}返回的不是 JSON,可能登录状态已失效`);
+    }
+
+    return await response.json();
+}
+
+// 获取学期、开学日期、当前周以及周次日期映射
+// 当前实现只将这些信息输出到日志,没有调用 saveCourseConfig 保存学期开始日期或总周数。
+async function fetchScheduleInfo() {
+    try {
+        console.log('正在获取学期信息...');
+        const data = await fetchJson(SCHEDULE_INFO_API, '获取学期信息');
+        console.log('学期信息:', data);
+        return data;
+    } catch (error) {
+        console.error('获取学期信息失败:', error);
+        window.shiguangBridge.showToast('获取学期信息失败: ' + error.message);
+        return null;
+    }
+}
+
+// 获取课程明细
+// 预期返回课程数组:courseName/teacherName 为课程和教师,dayOfWeek 为星期,
+// beginSection/endSection 为节次,weekDescription 为周次位图,beginTime/endTime 为时刻。
+async function fetchScheduleData() {
+    try {
+        console.log('正在获取课程明细...');
+        const data = await fetchJson(SCHEDULE_JSON_API, '获取课程数据');
+
+        if (!Array.isArray(data)) {
+            throw new Error('课程接口返回格式异常');
+        }
+
+        console.log(`成功获取 ${data.length} 条课程明细`);
+        console.log('课程接口原始数据:', data);
+        return data;
+    } catch (error) {
+        console.error('获取课程数据失败:', error);
+        window.shiguangBridge.showToast('获取课程失败: ' + error.message);
+        return null;
+    }
+}
+
+// weekDescription 是 25 位周次位图。
+// 下标直接对应 scheduleInfo.dateList 中的 weekOrder:
+// 第 0 位 = weekOrder 0(开学前一周),正式导入时忽略;第 1 位 = 第 1 周,以此类推。
+// 例如 "0101..." 表示第 1、3 周有课;这是接口约定,不能把所有下标统一加 1。
+function parseWeekDescription(weekDescription) {
+    if (typeof weekDescription !== 'string') {
+        return [];
+    }
+
+    const weeks = [];
+
+    for (let index = 1; index < weekDescription.length; index++) {
+        if (weekDescription[index] === '1') {
+            weeks.push(index);
+        }
+    }
+
+    return weeks;
+}
+
+// 教师显示名称:保持接口第一次出现的顺序,只把空格统一成“、”
+// 重复姓名只保留一次,例如 "张老师 李老师 张老师" 显示为 "张老师、李老师"。
+function normalizeTeacherDisplay(teacherName) {
+    if (!teacherName) {
+        return '';
+    }
+
+    const names = teacherName
+        .trim()
+        .split(/\s+/)
+        .filter(Boolean);
+
+    return [...new Set(names)].join('、');
+}
+
+// 教师比较键:忽略多人教师在不同周返回时的排列顺序
+// "张老师 李老师" 与 "李老师 张老师" 可归为同一教师集合,显示名称仍保持首次顺序。
+function normalizeTeacherKey(teacherName) {
+    if (!teacherName) {
+        return '';
+    }
+
+    const names = teacherName
+        .trim()
+        .split(/\s+/)
+        .filter(Boolean);
+
+    return [...new Set(names)].sort().join('、');
+}
+
+// 拼接完整上课地点
+// 按校区、教学楼、教室依次拼接;空字段跳过,全部为空时显示“未指定”。
+function buildPosition(item) {
+    return [item.campus, item.buildingName, item.classroomName]
+        .map(value => (value || '').trim())
+        .filter(Boolean)
+        .join(' ') || '未指定';
+}
+
+// 基于主仓库教程的 mergeAndDistinctCourses:先合并节次与重复记录,再合并同节次的周次。
+// 来源:https://github.com/ShiGuangSchedule/shiguangschedule/wiki/课程合并与去重函数
+// kbpro 补充:周次先去重、处理重叠节次,并在周次合并后继续处理新形成的连续节次。
+function mergeAndDistinctCourses(courses) {
+    if (!Array.isArray(courses)) {
+        return [];
+    }
+
+    // 复制课程和周次数组,避免合并过程修改原始数据。
+    let list = courses.map(course => ({
+        ...course,
+        name: course.name || '',
+        teacher: course.teacher || '',
+        position: course.position || '',
+        weeks: Array.isArray(course.weeks)
+            ? Array.from(new Set(course.weeks)).sort((a, b) => a - b)
+            : []
+    }));
+
+    if (list.length <= 1) {
+        return list;
+    }
+
+    let previousCount;
+
+    do {
+        previousCount = list.length;
+
+        // 阶段 1:名称、教师、地点、星期、周次一致时合并连续节次,并去掉重复或重叠部分。
+        list.sort((a, b) =>
+            a.name.localeCompare(b.name)
+            || a.teacher.localeCompare(b.teacher)
+            || a.position.localeCompare(b.position)
+            || a.day - b.day
+            || a.weeks.join(',').localeCompare(b.weeks.join(','))
+            || a.startSection - b.startSection
+            || a.endSection - b.endSection
+        );
+
+        const step1Merged = [];
+        let current = list[0];
+
+        for (let index = 1; index < list.length; index++) {
+            const next = list[index];
+            const isSameCourseAndWeeks = current.name === next.name
+                && current.teacher === next.teacher
+                && current.position === next.position
+                && current.day === next.day
+                && current.weeks.join(',') === next.weeks.join(',');
+
+            // 已按开始节次排序,因此该条件同时覆盖连续、完全重复和部分重叠的节次。
+            if (isSameCourseAndWeeks && next.startSection <= current.endSection + 1) {
+                current.endSection = Math.max(current.endSection, next.endSection);
+            } else {
+                step1Merged.push(current);
+                current = next;
+            }
+        }
+        step1Merged.push(current);
+
+        // 阶段 2:名称、教师、地点、星期及起止节次一致时,合并并排序上课周次。
+        step1Merged.sort((a, b) =>
+            a.name.localeCompare(b.name)
+            || a.teacher.localeCompare(b.teacher)
+            || a.position.localeCompare(b.position)
+            || a.day - b.day
+            || a.startSection - b.startSection
+            || a.endSection - b.endSection
+        );
+
+        const step2Merged = [];
+        let cur = step1Merged[0];
+
+        for (let index = 1; index < step1Merged.length; index++) {
+            const next = step1Merged[index];
+            const isSameCourseAndSection = cur.name === next.name
+                && cur.teacher === next.teacher
+                && cur.position === next.position
+                && cur.day === next.day
+                && cur.startSection === next.startSection
+                && cur.endSection === next.endSection;
+
+            if (isSameCourseAndSection) {
+                cur.weeks = Array.from(new Set([...cur.weeks, ...next.weeks])).sort((a, b) => a - b);
+            } else {
+                step2Merged.push(cur);
+                cur = next;
+            }
+        }
+        step2Merged.push(cur);
+        list = step2Merged;
+
+        // 周次合并可能让相邻时段的周次变得一致;每轮有合并时再处理,记录数不再减少即停止。
+    } while (list.length < previousCount);
+
+    return list;
+}
+
+// 将 getSchedulejson 返回结果转换成时光课表格式
+// 返回 { courses, timeSlots }:courses 用于课程导入,timeSlots 是接口给出的课程时段边界。
+// timeSlots 不是完整的每节作息表,后续由 generateTimeSlots 补齐为第 1~12 节。
+function convertScheduleData(apiData) {
+    // 先解析为教程函数要求的标准课程数组,作息边界单独收集。
+    const rawCourses = [];
+    const teacherDisplaysMap = new Map();
+    const timeSlotsMap = new Map();
+
+    apiData.forEach(item => {
+        const courseName = (item.courseName || '').trim();
+        const teacherDisplay = normalizeTeacherDisplay(item.teacherName);
+        const teacherKey = normalizeTeacherKey(item.teacherName);
+        const position = buildPosition(item);
+        // 接口字段可能是数字字符串,转为整数后再校验;星期 1~7 分别表示周一~周日。
+        const day = Number(item.dayOfWeek);
+        const startSection = Number(item.beginSection);
+        const endSection = Number(item.endSection);
+        const weeks = parseWeekDescription(item.weekDescription);
+
+        // 无课程名称、无正式上课周次或星期/节次不合法的记录不导入。
+        if (!courseName || weeks.length === 0) {
+            return;
+        }
+
+        if (!Number.isInteger(day) || day < 1 || day > 7) {
+            console.warn('忽略星期字段异常的课程:', item);
+            return;
+        }
+
+        if (!Number.isInteger(startSection) || !Number.isInteger(endSection)
+            || startSection <= 0 || endSection < startSection) {
+            console.warn('忽略节次字段异常的课程:', item);
+            return;
+        }
+
+        // 教程函数直接比较 teacher 字段,先使用排序后的教师集合,防止姓名顺序不同影响合并。
+        // 显示名称另外保留,合并后还原为接口首次出现的顺序。
+        const teacherDisplayKey = JSON.stringify([courseName, teacherKey, position, day]);
+        if (!teacherDisplaysMap.has(teacherDisplayKey)) {
+            teacherDisplaysMap.set(teacherDisplayKey, teacherDisplay);
+        }
+
+        rawCourses.push({
+            name: courseName,
+            teacher: teacherKey,
+            position,
+            day,
+            startSection,
+            endSection,
+            weeks
+        });
+
+        // 收集 API 给出的时间范围,用于覆盖默认作息时间的起止边界
+        // beginTime/endTime 表示整段课程的开始和结束,不能据此推算中间每节课的时间。
+        const beginTime = (item.beginTime || '').trim();
+        const endTime = (item.endTime || '').trim();
+
+        if (beginTime && endTime) {
+            const timeKey = `${startSection}-${endSection}-${beginTime}-${endTime}`;
+            timeSlotsMap.set(timeKey, {
+                startSection,
+                endSection,
+                startTime: beginTime,
+                endTime
+            });
+        }
+    });
+
+    // 调用教程的合并去重函数,再还原教师显示名称,不向 App 输出内部比较字段。
+    const courses = mergeAndDistinctCourses(rawCourses).map(course => ({
+        ...course,
+        teacher: teacherDisplaysMap.get(JSON.stringify([
+            course.name, course.teacher, course.position, course.day
+        ])) ?? course.teacher
+    }));
+
+    // 最终按星期、起止节次、课程名称排序,便于检查导入结果。
+    courses.sort((a, b) => {
+        if (a.day !== b.day) return a.day - b.day;
+        if (a.startSection !== b.startSection) return a.startSection - b.startSection;
+        if (a.endSection !== b.endSection) return a.endSection - b.endSection;
+        return a.name.localeCompare(b.name, 'zh-CN');
+    });
+
+    console.log(`接口原始记录: ${apiData.length} 条`);
+    console.log(`合并后的课程记录: ${courses.length} 条`);
+    console.log('转换后的课程:', courses);
+
+    return {
+        courses,
+        timeSlots: Array.from(timeSlotsMap.values())
+    };
+}
+
+// 生成朝阳校区时间段配置
+// 每节都有独立的 number、startTime、endTime;当前共配置 12 节,时刻格式为 HH:mm。
+// 下表为现有朝阳校区的默认作息,不表示每一节都由本次接口返回。
+// 本文件的作息只用于朝阳校区;春明湖校区需另行配置独立脚本和时间表。
+// 课程记录只保存节次,App 使用这张作息表将节次对应到具体时间,不使用课程自定义时间模式。
+function generateTimeSlots(timeSlotsFromAPI) {
+    const defaultTimeSlots = [
+        { number: 1, startTime: '08:00', endTime: '08:45' },
+        { number: 2, startTime: '08:55', endTime: '09:35' },
+        { number: 3, startTime: '10:05', endTime: '10:50' },
+        { number: 4, startTime: '11:00', endTime: '11:40' },
+        { number: 5, startTime: '13:30', endTime: '14:15' },
+        { number: 6, startTime: '14:25', endTime: '15:05' },
+        { number: 7, startTime: '15:35', endTime: '16:20' },
+        { number: 8, startTime: '16:30', endTime: '17:10' },
+        { number: 9, startTime: '18:00', endTime: '18:45' },
+        { number: 10, startTime: '18:45', endTime: '19:35' },
+        { number: 11, startTime: '19:45', endTime: '20:30' },
+        { number: 12, startTime: '20:30', endTime: '21:20' }
+    ];
+
+    if (Array.isArray(timeSlotsFromAPI)) {
+        timeSlotsFromAPI.forEach(slot => {
+            // 只覆盖首节的开始时间、末节的结束时间,其余节次继续使用默认值。
+            // 例如接口返回 5-7 节 13:30~16:20:更新第 5 节开始、第 7 节结束,
+            // 第 5 节结束、第 6 节起止、第 7 节开始仍按内置作息,不均分课程总时长。
+            // 同一边界有多条不同时间时,后处理的记录覆盖先处理的记录。
+            // 接口节次超出 1~12 时找不到对应作息,因此不会新增节次。
+            const first = defaultTimeSlots.find(item => item.number === slot.startSection);
+            const last = defaultTimeSlots.find(item => item.number === slot.endSection);
+
+            if (first && slot.startTime) {
+                first.startTime = slot.startTime;
+            }
+
+            if (last && slot.endTime) {
+                last.endTime = slot.endTime;
+            }
+        });
+    }
+
+    return defaultTimeSlots;
+}
+
+// 主函数:获取并导入课程
+// 所有数据保存都 await V2 Promise 接口;获取失败、没有课程或课程保存失败时退出。
+async function importCourseSchedule() {
+    try {
+        console.log('开始导入朝阳校区 kbpro 课程表...');
+        window.shiguangBridge.showToast('正在获取课程表...');
+
+        // 1. 获取学期信息供日志查看;此处不修改 App 的学期配置。
+        const scheduleInfo = await fetchScheduleInfo();
+        if (!scheduleInfo) {
+            return false;
+        }
+
+        console.log(
+            `当前学期: ${scheduleInfo.termName || '未知'},`
+            + `开学日期: ${scheduleInfo.schoolStartTime || '未知'},`
+            + `当前周: ${scheduleInfo.weekNum ?? '未知'}`
+        );
+
+        // 2. 获取当前会话对应的课程数组,并转换为 App 需要的字段。
+        const scheduleData = await fetchScheduleData();
+        if (!scheduleData) {
+            return false;
+        }
+
+        const { courses, timeSlots } = convertScheduleData(scheduleData);
+
+        if (courses.length === 0) {
+            window.shiguangBridge.showToast('未找到可导入的课程');
+            return false;
+        }
+
+        // 3. 先保存课程:name、teacher、position、day、startSection、endSection、weeks。
+        // 起止时刻不直接放入课程对象,下一步单独保存每节作息。
+        const coursesResult = await window.shiguangBridgePromise.saveImportedCourses(
+            JSON.stringify(courses)
+        );
+
+        if (coursesResult !== true) {
+            console.error('课程导入失败,返回:', coursesResult);
+            window.shiguangBridge.showToast('课程导入失败');
+            return false;
+        }
+
+        console.log('课程导入成功');
+        window.shiguangBridge.showToast(`成功导入 ${courses.length} 条课程记录!`);
+
+        // 4. 以朝阳校区 12 节默认作息为基础应用接口边界,再提交完整作息表。
+        const finalTimeSlots = generateTimeSlots(timeSlots);
+        console.log('朝阳校区时间段配置:', finalTimeSlots);
+
+        const timeSlotsResult = await window.shiguangBridgePromise.savePresetTimeSlots(
+            JSON.stringify(finalTimeSlots)
+        );
+
+        if (timeSlotsResult === true) {
+            console.log('时间段导入成功');
+            window.shiguangBridge.showToast('朝阳校区作息配置成功!');
+        } else {
+            // 返回 false 时保留已导入课程,并提示作息失败;抛出异常时则进入下面的 catch。
+            console.warn('时间段导入失败,返回:', timeSlotsResult);
+            window.shiguangBridge.showToast('课程已导入,但时间段配置失败');
+        }
+
+        return true;
+    } catch (error) {
+        console.error('导入过程出错:', error);
+        window.shiguangBridge.showToast('导入失败: ' + error.message);
+        return false;
+    }
+}
+
+// ========== 主执行逻辑 ==========
+
+if (!isOnSchedulePage()) {
+    redirectToSchedulePage();
+} else {
+    console.log('已进入长春理工大学朝阳校区新版课表导入流程');
+    window.shiguangBridge.showToast('正在准备导入朝阳校区课程表...');
+
+    // 等待页面和登录态稳定后请求接口
+    setTimeout(async () => {
+        const success = await importCourseSchedule();
+        if (success) {
+            // 完成信号用于通知 App 收尾;获取或课程保存失败时不发送。
+            window.shiguangBridge.notifyTaskCompletion();
+        }
+    }, 1000);
+}