cup_02.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. // resources/CUP/cup_02.js
  2. // 中国石油大学(北京)拾光课程表适配脚本
  3. // https://gmis.cup.edu.cn/gmis/student/default/index
  4. // 教务平台:南京南软
  5. // 适配开发者:larryyan
  6. // ==========================================
  7. // 0. 全局配置与验证函数
  8. // ==========================================
  9. const PRESET_TIME_CONFIG = {
  10.     campuses: {
  11.         MainCampus: {
  12.             startTimes: {
  13.                 morning: "08:00",
  14.                 afternoon: "13:30",
  15.                 evening: "18:30"
  16.             },
  17.             sectionCounts: {
  18.                 morning: 4,
  19.                 afternoon: 4,
  20.                 evening: 3
  21.             },
  22.             durations: {
  23.                 classMinutes: 45,
  24.                 shortBreakMinutes: 5,
  25.                 longBreakMinutes: 30
  26.             }
  27.         },
  28.         Karamay: {
  29.             startTimes: {
  30.                 morning: "09:30",
  31.                 afternoon: "16:00",
  32.                 evening: "20:30"
  33.             },
  34.             sectionCounts: {
  35.                 morning: 5,
  36.                 afternoon: 4,
  37.                 evening: 3
  38.             },
  39.             durations: {
  40.                 classMinutes: 45,
  41.                 shortBreakMinutes: 5,
  42.                 longBreakMinutes: 20
  43.             },
  44.         }
  45.     },
  46.     common: {
  47.         longBreakAfter: {
  48.             morning: 2,
  49.             afternoon: 2,
  50.             evening: 0  // 晚间课程无大课间
  51.         }
  52.     }
  53. };
  54. const CAMPUS_OPTIONS = [
  55.     { id: "MainCampus", label: "主校区" },
  56.     { id: "Karamay", label: "克拉玛依校区" }
  57. ];
  58. /**
  59.  * 验证开学日期的输入格式
  60.  */
  61. function validateDateInput(input) {
  62.     if (/^\d{4}[-\/\.]\d{2}[-\/\.]\d{2}$/.test(input)) {
  63.         return false;
  64.     } else {
  65.         return "请输入正确的日期格式,例如: 2025-09-01";
  66.     }
  67. }
  68. // ==========================================
  69. // 业务流程函数
  70. // ==========================================
  71. // 1. 显示一个公告信息弹窗
  72. async function promptUserToStart() {
  73.     try {
  74.         console.log("即将显示公告弹窗...");
  75.         const confirmed = await window.AndroidBridgePromise.showAlert(
  76.             "重要通知",
  77.             "导入前请确保您已成功登录教务系统,并选定正确的学期。",
  78.             "好的,开始"
  79.         );
  80.         if (confirmed) {
  81.             AndroidBridge.showToast("Alert:用户点击了确认!");
  82.             return true;
  83.         } else {
  84.             AndroidBridge.showToast("Alert:用户取消了!");
  85.             return false;
  86.         }
  87.     } catch (error) {
  88.         AndroidBridge.showToast("Alert:显示弹窗出错!" + error.message);
  89.         return false;
  90.     }
  91. }
  92. // 2. 选择校区 (使用配置项)
  93. async function selectCampus() {
  94.     try {
  95.         // 从配置中提取用于展示的名称数组
  96.         const campusLabels = CAMPUS_OPTIONS.map(opt => opt.label);
  97.        
  98.         const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  99.             "选择所在校区",
  100.             JSON.stringify(campusLabels),
  101.             0
  102.         );
  103.         if (selectedIndex !== null && selectedIndex >= 0) {
  104.             const selectedCampus = CAMPUS_OPTIONS[selectedIndex];
  105.             if (typeof AndroidBridge !== 'undefined') {
  106.                 AndroidBridge.showToast("已选择: " + selectedCampus.label);
  107.             }
  108.             // 返回选中的校区 ID ("MainCampus" 或 "Karamay")
  109.             return selectedCampus.id;
  110.         } else {
  111.             if (typeof AndroidBridge !== 'undefined') {
  112.                 AndroidBridge.showToast("取消导入:未选择校区。");
  113.             }
  114.             return null;
  115.         }
  116.     } catch (error) {
  117.         console.error("选择校区时发生错误:", error);
  118.         return null;
  119.     }    
  120. }
  121. // 3. 获取学期信息
  122. async function getTermCode() {
  123.     try {
  124.         if (typeof AndroidBridge !== 'undefined') AndroidBridge.showToast("正在获取学期列表...");
  125.         if (typeof $ === 'undefined' || !$.ajax) {
  126.             throw new Error("未检测到 jQuery 环境,请确保在正确的课表页面执行。");
  127.         }
  128.         const termData = await new Promise((resolve, reject) => {
  129.             $.ajax({
  130.                 type: 'get',
  131.                 dataType: 'json',
  132.                 url: '/gmis/default/bindterm',
  133.                 cache: false,
  134.                 success: function (data) { resolve(data); },
  135.                 error: function (xhr, status, error) { reject(new Error(`网络请求失败,状态码: ${xhr.status} ${error}`)); }
  136.             });
  137.         });
  138.         if (!termData || termData.length === 0) {
  139.             throw new Error("未能获取到有效的学期列表数据。");
  140.         }
  141.         const semesterTexts = [];
  142.         const semesterValues = [];
  143.         let defaultSelectedIndex = 0;
  144.         termData.forEach((item, index) => {
  145.             semesterTexts.push(item.termname);
  146.             semesterValues.push(item.termcode);
  147.             if (item.selected) {
  148.                 defaultSelectedIndex = index;
  149.             }
  150.         });
  151.         const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  152.             "选择导入学期",
  153.             JSON.stringify(semesterTexts),
  154.             defaultSelectedIndex
  155.         );
  156.         if (selectedIndex !== null && selectedIndex >= 0) {
  157.             const selectedValue = semesterValues[selectedIndex];
  158.             if (typeof AndroidBridge !== 'undefined') {
  159.                 AndroidBridge.showToast("已选择学期: " + semesterTexts[selectedIndex]);
  160.             }
  161.             return selectedValue;
  162.         } else {
  163.             if (typeof AndroidBridge !== 'undefined') {
  164.                 AndroidBridge.showToast("取消导入:未选择学期。");
  165.             }
  166.             return null;
  167.         }
  168.     } catch (error) {
  169.         console.error("读取学期信息时发生错误:", error);
  170.         if (typeof AndroidBridge !== 'undefined') {
  171.             AndroidBridge.showToast("Alert:读取学期信息出错!" + error.message);
  172.         }
  173.         return null;
  174.     }
  175. }
  176. // 4. 获取课程数据
  177. async function fetchData(termCode) {
  178.     try {
  179.         if (typeof $ === 'undefined' || !$.ajax) {
  180.             throw new Error("未检测到 jQuery 环境,请确保在正确的课表页面执行。");
  181.         }
  182.         const response = await new Promise((resolve, reject) => {
  183.             $.ajax({
  184.                 type: 'post',
  185.                 dataType: 'json',
  186.                 url: "../pygl/py_kbcx_ew",
  187.                 data: { 'kblx': 'xs', 'termcode': termCode },
  188.                 cache: false,
  189.                 success: function (data) { resolve(data); },
  190.                 error: function (xhr, status, error) { reject(new Error(`网络请求失败,状态码: ${xhr.status} ${error}`)); }
  191.             });
  192.         });
  193.         if (!response || !response.rows) {
  194.             throw new Error("接口返回数据为空或解密后格式不正确");
  195.         }
  196.         return response.rows;
  197.     } catch (error) {
  198.         AndroidBridge.showToast("Alert:获取数据出错!" + error.message);
  199.         return null;
  200.     }
  201. }
  202. // 5. 导入课程数据
  203. async function parseCourses(py_kbcx_ew, isKaramayCampus) {   
  204.     // 用于存放每一小节课的临时数组
  205.     let allCourseBlocks = [];
  206. // 辅助函数 1:根据 jcid 转换成标准的节次编号
  207.     function getStandardSection(jcid) {
  208.         // 上午始终是 1 开始 (11-15 -> 1-5)
  209.         if (jcid >= 11 && jcid <= 15) return jcid - 10;
  210.        
  211.         // 下午偏移量:克拉玛依上午有5节,所以下午从第6节开始(+5);本校上午只有4节,下午从第5节开始(+4)
  212.         let afternoonOffset = isKaramayCampus ? 5 : 4;
  213.         if (jcid >= 21 && jcid <= 24) return jcid - 20 + afternoonOffset;
  214.        
  215.         // 晚上偏移量:克拉玛依白天9节(5+4),晚上从10开始(+9);本校白天8节(4+4),晚上从9开始(+8)
  216.         let eveningOffset = isKaramayCampus ? 9 : 8;
  217.         if (jcid >= 31 && jcid <= 33) return jcid - 30 + eveningOffset;
  218.        
  219.         return 1; // 默认兜底
  220.     }
  221.     // 辅助函数 2:解析类似 "连续周 1-12周" 或 "单周 1-11周" 的字符串,返回数字数组
  222.     function parseWeeks(weekStr) {
  223.         let weeks = [];
  224.         let isSingle = weekStr.includes('单');
  225.         let isDouble = weekStr.includes('双');
  226.         let matches = weekStr.match(/\d+-\d+|\d+/g);
  227.         if (matches) {
  228.             matches.forEach(m => {
  229.                 if (m.includes('-')) {
  230.                     let [start, end] = m.split('-').map(Number);
  231.                     for (let i = start; i <= end; i++) {
  232.                         if (isSingle && i % 2 === 0) continue;
  233.                         if (isDouble && i % 2 !== 0) continue;
  234.                         weeks.push(i);
  235.                     }
  236.                 } else {
  237.                     let w = Number(m);
  238.                     if (isSingle && w % 2 === 0) return;
  239.                     if (isDouble && w % 2 !== 0) return;
  240.                     weeks.push(w);
  241.                 }
  242.             });
  243.         }
  244.         return [...new Set(weeks)].sort((a, b) => a - b);
  245.     }
  246.     // --- 第一步:将按“行”排列的数据,拆解提取出每一小节课 ---
  247.     py_kbcx_ew.forEach(row => {
  248.         // 本校区强行剔除上午第5节 (jcid === 15)
  249.         if (!isKaramayCampus && row.jcid === 15) {
  250.             return;
  251.         }
  252.         let currentSection = getStandardSection(row.jcid);
  253.         // 遍历星期一 (z1) 到星期日 (z7)
  254.         for (let day = 1; day <= 7; day++) {
  255.             let zVal = row['z' + day];
  256.             if (zVal) {
  257.                 // 如果同一个时间有两门课(比如单双周不同),按 <br/> 拆分
  258.                 let classParts = zVal.split(/<br\s*\/?>/i);
  259.                
  260.                 classParts.forEach(part => {
  261.                     let match = part.match(/(.*?)\[(.*?)\]([^\[]*)(?:\[(.*?)\])?$/);
  262.                    
  263.                     if (match) {
  264.                         allCourseBlocks.push({
  265.                             name: match[1].trim(),                   // 提取:课程名
  266.                             weekStr: match[2].trim(),                // 提取:原始周次字符串 (用于后续比对)
  267.                             weeks: parseWeeks(match[2]),             // 解析:纯数字周次数组
  268.                             teacher: match[3] ? match[3].trim() : "",// 提取:老师
  269.                             position: match[4] ? match[4].trim() : "未知地点", // 提取:上课地点
  270.                             day: day,                                // 星期几
  271.                             section: currentSection                  // 当前是第几节
  272.                         });
  273.                     }
  274.                 });
  275.             }
  276.         }
  277.     });
  278.     // --- 第二步:将连续的小节课“合并”成一门完整的课 ---
  279.     let mergedCourses = [];
  280.     allCourseBlocks.forEach(block => {
  281.         // 寻找是否已经有相邻的课可以合并 (同星期、同课名、同老师、同地点、同周次,且节次刚好挨着)
  282.         let existingCourse = mergedCourses.find(c =>
  283.             c.day === block.day &&
  284.             c.name === block.name &&
  285.             c.teacher === block.teacher &&
  286.             c.position === block.position &&
  287.             c.weekStr === block.weekStr &&
  288.             c.endSection === block.section - 1 // 核心:判断是否紧挨着上一节
  289.         );
  290.         if (existingCourse) {
  291.             // 如果可以合并,就把结束节次往后延
  292.             existingCourse.endSection = block.section;
  293.         } else {
  294.             // 如果不能合并,就作为一门新课加入
  295.             mergedCourses.push({
  296.                 name: block.name,
  297.                 teacher: block.teacher,
  298.                 position: block.position,
  299.                 day: block.day,
  300.                 startSection: block.section,
  301.                 endSection: block.section,
  302.                 weeks: block.weeks,
  303.                 weekStr: block.weekStr
  304.             });
  305.         }
  306.     });
  307.     // 清理掉多余的辅助比对字段,输出最终给拾光 App 的标准格式
  308.     const finalCourses = mergedCourses.map(c => {
  309.         delete c.weekStr;
  310.         return c;
  311.     });
  312.     try {
  313.         const result = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(finalCourses));
  314.         if (result === true) {
  315.             if (typeof AndroidBridge !== 'undefined') AndroidBridge.showToast("测试课程导入成功!");
  316.         } else {
  317.             if (typeof AndroidBridge !== 'undefined') AndroidBridge.showToast("测试课程导入失败,请查看日志。");
  318.         }
  319.     } catch (error) {
  320.         if (typeof AndroidBridge !== 'undefined') AndroidBridge.showToast("导入课程失败: " + error.message);
  321.     }
  322. }
  323. // 6. 导入预设时间段
  324. async function importPresetTimeSlots(campusId) {   
  325.     const campusConfig = PRESET_TIME_CONFIG.campuses[campusId];
  326.     const commonConfig = PRESET_TIME_CONFIG.common;
  327.     const generatedSlots = [];
  328.     let currentSectionNum = 1;
  329.     // 辅助函数:把 HH:mm 转换成分钟数 (例如 08:00 -> 480)
  330.     function timeToMinutes(timeStr) {
  331.         const [h, m] = timeStr.split(':').map(Number);
  332.         return h * 60 + m;
  333.     }
  334.     // 辅助函数:把分钟数转换成 HH:mm
  335.     function minutesToTime(mins) {
  336.         const h = Math.floor(mins / 60).toString().padStart(2, '0');
  337.         const m = (mins % 60).toString().padStart(2, '0');
  338.         return `${h}:${m}`;
  339.     }
  340.     // 按照上午、下午、晚上的顺序生成
  341.     const periods = ["morning", "afternoon", "evening"];
  342.    
  343.     periods.forEach(period => {
  344.         const count = campusConfig.sectionCounts[period];
  345.         if (count === 0) return; // 如果该时段没课,跳过
  346.         let currentMins = timeToMinutes(campusConfig.startTimes[period]);
  347.         const longBreakPos = commonConfig.longBreakAfter[period];
  348.         for (let i = 1; i <= count; i++) {
  349.             const startStr = minutesToTime(currentMins);
  350.             currentMins += campusConfig.durations.classMinutes; // 加上课时间
  351.             const endStr = minutesToTime(currentMins);
  352.             generatedSlots.push({
  353.                 number: currentSectionNum,
  354.                 startTime: startStr,
  355.                 endTime: endStr
  356.             });
  357.             currentSectionNum++;
  358.             // 如果不是该时段的最后一节课,则加上课间休息时间,推算出下一节的开始时间
  359.             if (i < count) {
  360.                 if (i === longBreakPos) {
  361.                     currentMins += campusConfig.durations.longBreakMinutes;
  362.                 } else {
  363.                     currentMins += campusConfig.durations.shortBreakMinutes;
  364.                 }
  365.             }
  366.         }
  367.     });
  368.     try {
  369.         const result = await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(generatedSlots));
  370.         if (result === true) {
  371.             window.AndroidBridge.showToast("测试时间段导入成功!");
  372.         } else {
  373.             window.AndroidBridge.showToast("测试时间段导入失败,请查看日志。");
  374.         }
  375.     } catch (error) {
  376.         window.AndroidBridge.showToast("导入时间段失败: " + error.message);
  377.     }
  378. }
  379. // 7. 导入课表配置
  380. async function saveConfig() {
  381.     let startDate = await window.AndroidBridgePromise.showPrompt(
  382.         "输入开学日期",
  383.         "请输入本学期开学日期 (格式: YYYY-MM-DD):",
  384.         "2025-09-01",          
  385.         "validateDateInput"    
  386.     );
  387.     if (startDate === null) {
  388.         if (typeof AndroidBridge !== 'undefined') {
  389.             AndroidBridge.showToast("已取消开学日期设置,将使用默认配置。");
  390.         }
  391.         startDate = "2025-09-01";
  392.     } else {
  393.         startDate = startDate.trim().replace(/[\/\.]/g, '-');
  394.     }
  395.     const courseConfigData = {
  396.         "semesterStartDate": startDate,
  397.         "semesterTotalWeeks": 25,
  398.         "defaultClassDuration": 45,
  399.         "defaultBreakDuration": 5,
  400.         "firstDayOfWeek": 1
  401.     };
  402.     try {
  403.         const configJsonString = JSON.stringify(courseConfigData);
  404.         const result = await window.AndroidBridgePromise.saveCourseConfig(configJsonString);
  405.         if (result === true) {
  406.             AndroidBridge.showToast("测试配置导入成功!开学日期: " + startDate);
  407.         } else {
  408.             AndroidBridge.showToast("测试配置导入失败,请查看日志。");
  409.         }
  410.     } catch (error) {
  411.         AndroidBridge.showToast("导入配置失败: " + error.message);
  412.     }
  413. }
  414. /**
  415.  * 编排整个课程导入流程。
  416.  */
  417. async function runImportFlow() {
  418.     // 1. 公告和前置检查。
  419.     const alertConfirmed = await promptUserToStart();
  420.     if (!alertConfirmed) return;
  421.    
  422.     // 2. 选择校区。 (获取校区ID)
  423.     const campusId = await selectCampus();
  424.     if (campusId === null) return;
  425.    
  426.     // 生成一个 boolean 给解析课程使用
  427.     const isKaramayCampus = (campusId === "Karamay");
  428.     // 3. 获取学期。
  429.     const termCode = await getTermCode();
  430.     if (termCode === null) {
  431.         AndroidBridge.showToast("导入已取消。");
  432.         return;
  433.     }
  434.     // 4. 获取课程数据
  435.     const py_kbcx_ew = await fetchData(termCode);
  436.     if (py_kbcx_ew === null) {
  437.         AndroidBridge.showToast("导入已取消。");
  438.         return;
  439.     }
  440.     // 5. 解析课程信息。 (传入 boolean)
  441.     await parseCourses(py_kbcx_ew, isKaramayCampus);
  442.    
  443.     // 6. 导入时间段数据。 (传入字符串 campusId,供引擎推算)
  444.     await importPresetTimeSlots(campusId);
  445.    
  446.     // 7. 保存配置数据
  447.     await saveConfig();
  448.     // 8. 流程**完全成功**,发送结束信号。
  449.     AndroidBridge.notifyTaskCompletion();
  450. }
  451. // 启动所有演示
  452. runImportFlow();