|
@@ -0,0 +1,264 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * 解析正方教务系统的周次字符串
|
|
|
|
|
+ */
|
|
|
|
|
+function parseZhengFangWeeks(weekStr) {
|
|
|
|
|
+ if (!weekStr) return [];
|
|
|
|
|
+ let weeks = [];
|
|
|
|
|
+ let isOdd = weekStr.includes('单');
|
|
|
|
|
+ let isEven = weekStr.includes('双');
|
|
|
|
|
+ let cleanStr = weekStr.replace(/周|\(单\)|\(双\)|单|双/g, '').replace(/\s+/g, '');
|
|
|
|
|
+
|
|
|
|
|
+ let parts = cleanStr.split(',');
|
|
|
|
|
+ for (let part of parts) {
|
|
|
|
|
+ if (part.includes('-')) {
|
|
|
|
|
+ let [start, end] = part.split('-');
|
|
|
|
|
+ for (let i = parseInt(start); i <= parseInt(end); i++) {
|
|
|
|
|
+ if (isOdd && i % 2 === 0) continue;
|
|
|
|
|
+ if (isEven && i % 2 !== 0) continue;
|
|
|
|
|
+ if (!weeks.includes(i)) weeks.push(i);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let w = parseInt(part);
|
|
|
|
|
+ if (!isNaN(w) && !weeks.includes(w)) {
|
|
|
|
|
+ if (isOdd && w % 2 === 0) continue;
|
|
|
|
|
+ if (isEven && w % 2 !== 0) continue;
|
|
|
|
|
+ weeks.push(w);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return weeks.sort((a, b) => a - b);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 封装兼容 WebVPN 的原生 AJAX 请求
|
|
|
|
|
+ */
|
|
|
|
|
+function requestData(url, method = 'GET', data = null) {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ let xhr = new XMLHttpRequest();
|
|
|
|
|
+ xhr.open(method, url, true);
|
|
|
|
|
+ if (method === 'POST') {
|
|
|
|
|
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
|
|
|
|
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
|
|
|
|
+ }
|
|
|
|
|
+ xhr.onreadystatechange = function() {
|
|
|
|
|
+ if (xhr.readyState === 4) {
|
|
|
|
|
+ if (xhr.status >= 200 && xhr.status < 300) {
|
|
|
|
|
+ resolve(xhr.responseText);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ reject(new Error("网络请求失败,状态码:" + xhr.status));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ xhr.onerror = function() {
|
|
|
|
|
+ reject(new Error("网络请求发生错误,请检查网络连接。"));
|
|
|
|
|
+ };
|
|
|
|
|
+ xhr.send(data);
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 用正则从 HTML 文本中硬核提取 select 选项
|
|
|
|
|
+ */
|
|
|
|
|
+function extractOptionsByRegex(html, selectId) {
|
|
|
|
|
+ let selectRegex = new RegExp(`<select[^>]*id="${selectId}"[^>]*>(.*?)<\/select>`, 'is');
|
|
|
|
|
+ let selectMatch = html.match(selectRegex);
|
|
|
|
|
+ if (!selectMatch) return [];
|
|
|
|
|
+
|
|
|
|
|
+ let optionsHtml = selectMatch[1];
|
|
|
|
|
+ let optionRegex = /<option\s+value="([^"]+)"([^>]*)>([^<]+)<\/option>/gi;
|
|
|
|
|
+ let options = [];
|
|
|
|
|
+ let match;
|
|
|
|
|
+ while ((match = optionRegex.exec(optionsHtml)) !== null) {
|
|
|
|
|
+ if (match[1].trim() !== "") {
|
|
|
|
|
+ options.push({
|
|
|
|
|
+ value: match[1].trim(),
|
|
|
|
|
+ text: match[3].trim(),
|
|
|
|
|
+ selected: match[2].includes('selected')
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return options;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 异步编排流程
|
|
|
|
|
+ */
|
|
|
|
|
+async function runImportFlow() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (typeof window.AndroidBridge !== 'undefined') {
|
|
|
|
|
+ AndroidBridge.showToast("正在通过 WebVPN 通道读取数据...");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log("【1/4】正在获取学期选项...");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let sysPath = typeof _path !== 'undefined' ? _path : '/jwglxt';
|
|
|
|
|
+ let semesters = [];
|
|
|
|
|
+ let semesterValues = [];
|
|
|
|
|
+ let defaultIndex = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 尝试直接通过正则表达式,去后台请求并提取页面中的下拉框
|
|
|
|
|
+ console.log("正在后台请求页面源码...");
|
|
|
|
|
+ const indexHtml = await requestData(`${sysPath}/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default`, 'GET');
|
|
|
|
|
+
|
|
|
|
|
+ let yearOpts = extractOptionsByRegex(indexHtml, 'xnm');
|
|
|
|
|
+ let termOpts = extractOptionsByRegex(indexHtml, 'xqm');
|
|
|
|
|
+
|
|
|
|
|
+ if (yearOpts.length === 0 || termOpts.length === 0) {
|
|
|
|
|
+ throw new Error("源码提取学期信息失败,可能是 WebVPN 会话过期,请重新登录教务系统!");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let count = 0;
|
|
|
|
|
+ yearOpts.forEach(y => {
|
|
|
|
|
+ termOpts.forEach(t => {
|
|
|
|
|
+ semesters.push(`${y.text} 第${t.text}学期`);
|
|
|
|
|
+ semesterValues.push({ xnm: y.value, xqm: t.value });
|
|
|
|
|
+ if (y.selected && t.selected) {
|
|
|
|
|
+ defaultIndex = count;
|
|
|
|
|
+ }
|
|
|
|
|
+ count++;
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 弹出选择框
|
|
|
|
|
+ let selectedIdx = defaultIndex;
|
|
|
|
|
+ if (typeof window.AndroidBridgePromise !== 'undefined') {
|
|
|
|
|
+ let userChoice = await window.AndroidBridgePromise.showSingleSelection(
|
|
|
|
|
+ "请选择要导入的学期",
|
|
|
|
|
+ JSON.stringify(semesters),
|
|
|
|
|
+ defaultIndex
|
|
|
|
|
+ );
|
|
|
|
|
+ if (userChoice === null) {
|
|
|
|
|
+ AndroidBridge.showToast("已取消导入");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ selectedIdx = userChoice;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let msg = "【浏览器测试】请选择学期对应的序号:\n\n";
|
|
|
|
|
+ semesters.forEach((s, idx) => {
|
|
|
|
|
+ if (Math.abs(idx - defaultIndex) <= 4) msg += `[ ${idx} ] : ${s}\n`;
|
|
|
|
|
+ });
|
|
|
|
|
+ msg += "\n请输入方括号中的数字:";
|
|
|
|
|
+ let userInput = prompt(msg, defaultIndex);
|
|
|
|
|
+ if (userInput === null) return;
|
|
|
|
|
+ selectedIdx = parseInt(userInput);
|
|
|
|
|
+ if (isNaN(selectedIdx) || selectedIdx < 0 || selectedIdx >= semesters.length) {
|
|
|
|
|
+ alert("输入无效,使用默认学期!");
|
|
|
|
|
+ selectedIdx = defaultIndex;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const targetData = semesterValues[selectedIdx];
|
|
|
|
|
+ if (typeof window.AndroidBridge !== 'undefined') {
|
|
|
|
|
+ AndroidBridge.showToast(`正在获取 [${semesters[selectedIdx]}] 数据...`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log(`【2/4】正在向服务器请求 [${semesters[selectedIdx]}] 的 JSON 数据...`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 发送 Ajax 请求获取真实的课表和作息时间
|
|
|
|
|
+ const postBody = `xnm=${targetData.xnm}&xqm=${targetData.xqm}&kzlx=ck&xsdm=&kclbdm=&kclxdm=`;
|
|
|
|
|
+
|
|
|
|
|
+ const [kbResText, timeResText] = await Promise.all([
|
|
|
|
|
+ requestData(`${sysPath}/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151`, 'POST', postBody),
|
|
|
|
|
+ requestData(`${sysPath}/kbcx/xskbcx_cxRjc.html?gnmkdm=N2151`, 'POST', postBody)
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ const kbJson = JSON.parse(kbResText);
|
|
|
|
|
+ const timeJson = JSON.parse(timeResText);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 解析作息时间 (拦截 21:35 之后的时间)
|
|
|
|
|
+ let timeSlots = [];
|
|
|
|
|
+ if (Array.isArray(timeJson)) {
|
|
|
|
|
+ timeJson.forEach(t => {
|
|
|
|
|
+ if (t.jssj > "21:35") return;
|
|
|
|
|
+ timeSlots.push({
|
|
|
|
|
+ number: parseInt(t.jcmc, 10),
|
|
|
|
|
+ startTime: t.qssj,
|
|
|
|
|
+ endTime: t.jssj
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ timeSlots.sort((a, b) => a.number - b.number);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 解析课程数据
|
|
|
|
|
+ let parsedCourses = [];
|
|
|
|
|
+ if (kbJson && kbJson.kbList) {
|
|
|
|
|
+ kbJson.kbList.forEach(c => {
|
|
|
|
|
+ let courseObj = {
|
|
|
|
|
+ name: c.kcmc || "未知课程",
|
|
|
|
|
+ teacher: c.xm || "未知",
|
|
|
|
|
+ position: c.cdmc || "待定",
|
|
|
|
|
+ day: parseInt(c.xqj),
|
|
|
|
|
+ isCustomTime: false
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ courseObj.weeks = parseZhengFangWeeks(c.zcd);
|
|
|
|
|
+
|
|
|
|
|
+ if (c.jcs) {
|
|
|
|
|
+ let secParts = c.jcs.split('-');
|
|
|
|
|
+ courseObj.startSection = parseInt(secParts[0]);
|
|
|
|
|
+ courseObj.endSection = parseInt(secParts[secParts.length - 1] || secParts[0]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (courseObj.name && courseObj.weeks.length > 0 && courseObj.startSection) {
|
|
|
|
|
+ parsedCourses.push(courseObj);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (parsedCourses.length === 0) {
|
|
|
|
|
+ const errMsg = "该学期暂无排课数据。";
|
|
|
|
|
+ if (typeof window.AndroidBridgePromise !== 'undefined') {
|
|
|
|
|
+ await window.AndroidBridgePromise.showAlert("提示", errMsg, "好的");
|
|
|
|
|
+ } else alert(errMsg);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 去重
|
|
|
|
|
+ let uniqueCourses = [];
|
|
|
|
|
+ let courseSet = new Set();
|
|
|
|
|
+ parsedCourses.forEach(course => {
|
|
|
|
|
+ let uniqueKey = `${course.day}-${course.startSection}-${course.endSection}-${course.name}-${course.weeks.join(',')}`;
|
|
|
|
|
+ if (!courseSet.has(uniqueKey)) {
|
|
|
|
|
+ courseSet.add(uniqueKey);
|
|
|
|
|
+ uniqueCourses.push(course);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const config = {
|
|
|
|
|
+ "defaultClassDuration": 45,
|
|
|
|
|
+ "defaultBreakDuration": 5
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 打印并保存
|
|
|
|
|
+ if (typeof window.AndroidBridgePromise === 'undefined') {
|
|
|
|
|
+ console.log("【测试成功】被 21:35 规则过滤后的作息时间表:\n", timeSlots);
|
|
|
|
|
+ console.log(`【测试成功】共获取到 ${uniqueCourses.length} 门课程:\n`, JSON.stringify(uniqueCourses, null, 2));
|
|
|
|
|
+ alert(`解析成功!获取到 ${uniqueCourses.length} 门课程及过滤后的作息时间。\n请打开F12控制台查看。`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
|
|
|
|
|
+ if (timeSlots.length > 0) {
|
|
|
|
|
+ await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const saveResult = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(uniqueCourses));
|
|
|
|
|
+ if (!saveResult) {
|
|
|
|
|
+ AndroidBridge.showToast("保存失败,请重试!");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ AndroidBridge.showToast(`成功导入 ${uniqueCourses.length} 节课程!`);
|
|
|
|
|
+ AndroidBridge.notifyTaskCompletion();
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (typeof window.AndroidBridge !== 'undefined') {
|
|
|
|
|
+ AndroidBridge.showToast("导入异常: " + error.message);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error("【导入异常】", error);
|
|
|
|
|
+ alert("导入异常: " + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+runImportFlow();
|