|
@@ -0,0 +1,398 @@
|
|
|
|
|
+// ====================== 工具函数 ======================
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 检查用户是否已登录。
|
|
|
|
|
+ * 如果当前URL包含登录关键字,说明用户未登录,返回false。
|
|
|
|
|
+ * 如果不包含登录关键字,说明用户已登录,返回true。
|
|
|
|
|
+ */
|
|
|
|
|
+function isUserLoggedIn() {
|
|
|
|
|
+ const url = window.location.href;
|
|
|
|
|
+ const loginKeywords = [
|
|
|
|
|
+ "https://jwc.mmpt.edu.cn/xtgl/login_slogin.html"
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ // 检查URL是否包含登录关键字
|
|
|
|
|
+ for (const keyword of loginKeywords) {
|
|
|
|
|
+ if (url.includes(keyword)) {
|
|
|
|
|
+ return false; // 包含登录关键字,说明用户未登录
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 不包含登录关键字,说明用户已登录
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 解析节次字符串 -> 开始节次和结束节次
|
|
|
|
|
+function parseSections(sectionStr) {
|
|
|
|
|
+ if (!sectionStr) return { start: 1, end: 1 };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理 "1-2节" 或 "3-4节" 等格式
|
|
|
|
|
+ const match = sectionStr.match(/(\d+)-(\d+)节?/);
|
|
|
|
|
+ if (match) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ start: parseInt(match[1]),
|
|
|
|
|
+ end: parseInt(match[2])
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理单个节次 "1节" 等
|
|
|
|
|
+ const singleMatch = sectionStr.match(/(\d+)节?/);
|
|
|
|
|
+ if (singleMatch) {
|
|
|
|
|
+ const num = parseInt(singleMatch[1]);
|
|
|
|
|
+ return { start: num, end: num };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return { start: 1, end: 1 };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 解析周次字符串,处理单双周和周次范围。
|
|
|
|
|
+ */
|
|
|
|
|
+function parseWeeks(weekStr) {
|
|
|
|
|
+ if (!weekStr) return [];
|
|
|
|
|
+
|
|
|
|
|
+ const weekSets = weekStr.split(',');
|
|
|
|
|
+ let weeks = [];
|
|
|
|
|
+
|
|
|
|
|
+ for (const set of weekSets) {
|
|
|
|
|
+ const trimmedSet = set.trim();
|
|
|
|
|
+
|
|
|
|
|
+ const rangeMatch = trimmedSet.match(/(\d+)-(\d+)周/);
|
|
|
|
|
+ const singleMatch = trimmedSet.match(/^(\d+)周/); // 匹配以数字周结束的
|
|
|
|
|
+
|
|
|
|
|
+ let start = 0;
|
|
|
|
|
+ let end = 0;
|
|
|
|
|
+ let processed = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (rangeMatch) { // 范围, 如 "1-5周"
|
|
|
|
|
+ start = Number(rangeMatch[1]);
|
|
|
|
|
+ end = Number(rangeMatch[2]);
|
|
|
|
|
+ processed = true;
|
|
|
|
|
+ } else if (singleMatch) { // 单个周, 如 "6周"
|
|
|
|
|
+ start = end = Number(singleMatch[1]);
|
|
|
|
|
+ processed = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (processed) {
|
|
|
|
|
+ // 确定单双周
|
|
|
|
|
+ const isSingle = trimmedSet.includes('(单)');
|
|
|
|
|
+ const isDouble = trimmedSet.includes('(双)');
|
|
|
|
|
+
|
|
|
|
|
+ for (let w = start; w <= end; w++) {
|
|
|
|
|
+ if (isSingle && w % 2 === 0) continue; // 单周跳过偶数
|
|
|
|
|
+ if (isDouble && w % 2 !== 0) continue; // 双周跳过奇数
|
|
|
|
|
+ weeks.push(w);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 去重并排序
|
|
|
|
|
+ return [...new Set(weeks)].sort((a, b) => a - b);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 合并重复课程
|
|
|
|
|
+function mergeDuplicateCourses(courses) {
|
|
|
|
|
+ const merged = [];
|
|
|
|
|
+ const keyMap = {}; // key = name+day+startSection+endSection+position
|
|
|
|
|
+
|
|
|
|
|
+ for (const c of courses) {
|
|
|
|
|
+ const key = `${c.name}|${c.day}|${c.startSection}|${c.endSection}|${c.position}`;
|
|
|
|
|
+ if (!keyMap[key]) {
|
|
|
|
|
+ keyMap[key] = { ...c, weeks: [...c.weeks] };
|
|
|
|
|
+ } else {
|
|
|
|
|
+ keyMap[key].weeks = Array.from(new Set([...keyMap[key].weeks, ...c.weeks]));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const k in keyMap) merged.push(keyMap[k]);
|
|
|
|
|
+ return merged;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const MMPT_CONFIG = {
|
|
|
|
|
+ name: '茂名职业技术学院',
|
|
|
|
|
+ domains: ['mmpt.edu.cn', 'mmvtc.edu.cn'],
|
|
|
|
|
+ jwcUrl: 'https://jwc.mmpt.edu.cn',
|
|
|
|
|
+ apiPath: '/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151'
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 茂名职业技术学院作息时间表配置
|
|
|
|
|
+const MMVT_SCHEDULE_CONFIG = {
|
|
|
|
|
+ '南校区': {
|
|
|
|
|
+ timeSlots: [
|
|
|
|
|
+ { number: 1, startTime: "08:20", endTime: "09:00" },
|
|
|
|
|
+ { number: 2, startTime: "09:10", endTime: "09:50" },
|
|
|
|
|
+ { number: 3, startTime: "10:00", endTime: "10:40" },
|
|
|
|
|
+ { number: 4, startTime: "10:50", endTime: "11:30" },
|
|
|
|
|
+ { number: 5, startTime: "11:40", endTime: "12:20" },
|
|
|
|
|
+ { number: 6, startTime: "14:30", endTime: "15:10" },
|
|
|
|
|
+ { number: 7, startTime: "15:20", endTime: "16:00" },
|
|
|
|
|
+ { number: 8, startTime: "16:15", endTime: "16:55" },
|
|
|
|
|
+ { number: 9, startTime: "17:05", endTime: "17:45" },
|
|
|
|
|
+ { number: 10, startTime: "19:00", endTime: "19:40" },
|
|
|
|
|
+ { number: 11, startTime: "19:50", endTime: "20:30" },
|
|
|
|
|
+ { number: 12, startTime: "20:40", endTime: "21:20" }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ '北校区': {
|
|
|
|
|
+ timeSlots: [
|
|
|
|
|
+ { number: 1, startTime: "08:00", endTime: "08:40" },
|
|
|
|
|
+ { number: 2, startTime: "08:50", endTime: "09:30" },
|
|
|
|
|
+ { number: 3, startTime: "09:40", endTime: "10:20" },
|
|
|
|
|
+ { number: 4, startTime: "10:30", endTime: "11:10" },
|
|
|
|
|
+ { number: 5, startTime: "11:20", endTime: "12:00" },
|
|
|
|
|
+ { number: 6, startTime: "14:30", endTime: "15:10" },
|
|
|
|
|
+ { number: 7, startTime: "15:20", endTime: "16:00" },
|
|
|
|
|
+ { number: 8, startTime: "16:15", endTime: "16:55" },
|
|
|
|
|
+ { number: 9, startTime: "17:05", endTime: "17:45" },
|
|
|
|
|
+ { number: 10, startTime: "19:00", endTime: "19:40" },
|
|
|
|
|
+ { number: 11, startTime: "19:50", endTime: "20:30" },
|
|
|
|
|
+ { number: 12, startTime: "20:40", endTime: "21:20" }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// 获取MMPT配置(无需选择,直接使用)
|
|
|
|
|
+function getMMPTConfig() {
|
|
|
|
|
+ return MMPT_CONFIG;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 选择校区时间表
|
|
|
|
|
+async function selectCampusSchedule() {
|
|
|
|
|
+ const campuses = Object.keys(MMVT_SCHEDULE_CONFIG);
|
|
|
|
|
+ const campusIndex = await window.AndroidBridgePromise.showSingleSelection(
|
|
|
|
|
+ "选择校区时间表",
|
|
|
|
|
+ JSON.stringify(campuses),
|
|
|
|
|
+ -1
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (campusIndex === null) {
|
|
|
|
|
+ AndroidBridge.showToast("未选择校区,使用南校区时间表");
|
|
|
|
|
+ return MMVT_SCHEDULE_CONFIG['南校区'];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return MMVT_SCHEDULE_CONFIG[campuses[campusIndex]];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+// 选择学年
|
|
|
|
|
+async function selectAcademicYear() {
|
|
|
|
|
+ const currentYear = new Date().getFullYear();
|
|
|
|
|
+ const years = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 生成最近3年的学年选项,格式为 xxxx-xxxx
|
|
|
|
|
+ for (let i = 0; i < 3; i++) {
|
|
|
|
|
+ const year = currentYear - i;
|
|
|
|
|
+ years.push(`${year}-${year + 1}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const yearIndex = await window.AndroidBridgePromise.showSingleSelection(
|
|
|
|
|
+ "选择学年",
|
|
|
|
|
+ JSON.stringify(years),
|
|
|
|
|
+ 0 // 默认选择当前学年
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (yearIndex === null) {
|
|
|
|
|
+ return currentYear; // 默认当前学年
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return currentYear - yearIndex;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 选择学期
|
|
|
|
|
+async function selectSemester() {
|
|
|
|
|
+ const semesters = ["第一学期", "第二学期"];
|
|
|
|
|
+ const semesterIndex = await window.AndroidBridgePromise.showSingleSelection(
|
|
|
|
|
+ "选择学期",
|
|
|
|
|
+ JSON.stringify(semesters),
|
|
|
|
|
+ -1
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (semesterIndex === null) {
|
|
|
|
|
+ return "3"; // 默认第一学期
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return semesterIndex === 0 ? "3" : "12";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ====================== 获取课程数据 ======================
|
|
|
|
|
+async function fetchCourseData() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 使用MMPT配置,让用户选择学年和学期
|
|
|
|
|
+ const schoolConfig = getMMPTConfig();
|
|
|
|
|
+ const academicYear = await selectAcademicYear();
|
|
|
|
|
+ const semester = await selectSemester();
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`使用学校配置: ${schoolConfig.name}`);
|
|
|
|
|
+ console.log(`选择学年: ${academicYear}, 选择学期: ${semester}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 构建完整的API URL
|
|
|
|
|
+ const apiUrl = schoolConfig.jwcUrl + schoolConfig.apiPath;
|
|
|
|
|
+
|
|
|
|
|
+ // 从新系统获取课程数据
|
|
|
|
|
+ // 使用POST请求获取完整的课程数据
|
|
|
|
|
+ const response = await fetch(apiUrl, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Content-Type': 'application/x-www-form-urlencoded',
|
|
|
|
|
+ 'Accept': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: new URLSearchParams({
|
|
|
|
|
+ 'xnm': academicYear, // 用户选择的学年
|
|
|
|
|
+ 'xqm': semester, // 用户选择的学期(3或12)
|
|
|
|
|
+ 'kzlx': 'ck', // 课程类型
|
|
|
|
|
+ 'xsdm': '', // 学生代码
|
|
|
|
|
+ 'kclbdm': '' // 课程类别代码
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ return data;
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error("获取课程数据失败:", err);
|
|
|
|
|
+ AndroidBridge.showToast("获取课程数据失败:" + err.message);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ====================== 解析课程数据 ======================
|
|
|
|
|
+function parseCourseData(responseData) {
|
|
|
|
|
+ const courses = [];
|
|
|
|
|
+
|
|
|
|
|
+ if (!responseData || !responseData.kbList) {
|
|
|
|
|
+ console.error("响应数据格式不正确");
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ responseData.kbList.forEach(course => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 解析节次
|
|
|
|
|
+ const sections = parseSections(course.jc);
|
|
|
|
|
+
|
|
|
|
|
+ // 解析周次
|
|
|
|
|
+ const weeks = parseWeeks(course.zcd);
|
|
|
|
|
+
|
|
|
|
|
+ if (weeks.length === 0) {
|
|
|
|
|
+ console.warn(`课程 ${course.kcmc} 没有有效的周次信息`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ courses.push({
|
|
|
|
|
+ name: course.kcmc || "未知课程",
|
|
|
|
|
+ teacher: course.xm || "未知教师",
|
|
|
|
|
+ position: course.cdmc || "未知教室",
|
|
|
|
|
+ day: parseInt(course.xqj) || 1,
|
|
|
|
|
+ startSection: sections.start,
|
|
|
|
|
+ endSection: sections.end,
|
|
|
|
|
+ weeks: weeks
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error(`解析课程数据失败:`, err, course);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return courses;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ====================== 保存课程 ======================
|
|
|
|
|
+async function saveCourses(courses) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
|
|
+ if (result === true) {
|
|
|
|
|
+ AndroidBridge.showToast("课程导入成功!");
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ AndroidBridge.showToast("课程导入失败,请查看日志!");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error("保存课程失败:", err);
|
|
|
|
|
+ AndroidBridge.showToast("保存课程失败:" + err.message);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ====================== 导入预设时间段(根据选择的校区) ======================
|
|
|
|
|
+async function importPresetTimeSlots() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 让用户选择校区时间表
|
|
|
|
|
+ const campusConfig = await selectCampusSchedule();
|
|
|
|
|
+ const timeSlots = campusConfig.timeSlots;
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`使用校区时间表: ${campusConfig.name || '未知校区'}`);
|
|
|
|
|
+ console.log(`时间段数量: ${timeSlots.length}`);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
|
|
+ if (result === true) {
|
|
|
|
|
+ AndroidBridge.showToast(`时间段导入成功!共导入${timeSlots.length}个时间段`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ AndroidBridge.showToast("时间段导入失败,请查看日志!");
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error("时间段导入失败:", err);
|
|
|
|
|
+ AndroidBridge.showToast("时间段导入失败:" + err.message);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ====================== 主流程 ======================
|
|
|
|
|
+async function runImportFlow() {
|
|
|
|
|
+ // 检查用户是否已登录
|
|
|
|
|
+ if (!isUserLoggedIn()) {
|
|
|
|
|
+ AndroidBridge.showToast("检测到未登录状态,请先登录后再使用课程导入功能!");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ AndroidBridge.showToast("课程导入流程即将开始...");
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 1️⃣ 获取课程数据(选择学年学期)
|
|
|
|
|
+ AndroidBridge.showToast("请选择学年和学期...");
|
|
|
|
|
+
|
|
|
|
|
+ // 从新系统获取课程数据
|
|
|
|
|
+ const responseData = await fetchCourseData();
|
|
|
|
|
+ if (!responseData) {
|
|
|
|
|
+ AndroidBridge.showToast("获取课程数据失败!");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2️⃣ 解析课程数据
|
|
|
|
|
+ AndroidBridge.showToast("正在解析课程数据...");
|
|
|
|
|
+ const courses = parseCourseData(responseData);
|
|
|
|
|
+ if (!courses || courses.length === 0) {
|
|
|
|
|
+ AndroidBridge.showToast("未找到课程数据!");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3️⃣ 合并重复课程
|
|
|
|
|
+ AndroidBridge.showToast("正在合并重复课程...");
|
|
|
|
|
+ const mergedCourses = mergeDuplicateCourses(courses);
|
|
|
|
|
+
|
|
|
|
|
+ // 4️⃣ 保存课程
|
|
|
|
|
+ AndroidBridge.showToast("正在保存课程数据...");
|
|
|
|
|
+ const saveResult = await saveCourses(mergedCourses);
|
|
|
|
|
+ if (!saveResult) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5️⃣ 导入预设时间段(用户选择校区)
|
|
|
|
|
+ AndroidBridge.showToast("请选择校区时间表...");
|
|
|
|
|
+ await importPresetTimeSlots();
|
|
|
|
|
+
|
|
|
|
|
+ // ✅ 只有所有步骤都成功完成,才通知任务完成
|
|
|
|
|
+ AndroidBridge.showToast(`课程导入成功,共导入 ${mergedCourses.length} 门课程!`);
|
|
|
|
|
+ console.log("JS:整个导入流程执行完毕并成功。");
|
|
|
|
|
+ AndroidBridge.notifyTaskCompletion();
|
|
|
|
|
+
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error("导入流程失败:", err);
|
|
|
|
|
+ AndroidBridge.showToast("导入失败:" + err.message);
|
|
|
|
|
+ // ❌ 失败时不调用 notifyTaskCompletion()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 启动流程
|
|
|
|
|
+runImportFlow();
|