|
@@ -1,6 +1,6 @@
|
|
|
// 文件: capadap.js
|
|
// 文件: capadap.js
|
|
|
//后期可加入接口-获取校区 https://jwxt.cap.edu.cn/jwapp/sys/kbapp/api/wdkbcx/getMyScheduledCampus.do
|
|
//后期可加入接口-获取校区 https://jwxt.cap.edu.cn/jwapp/sys/kbapp/api/wdkbcx/getMyScheduledCampus.do
|
|
|
-
|
|
|
|
|
|
|
+// 新版适配 - 接口新增了每个字段,可以直接使用无需再做正则提取
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 显示导入提示
|
|
* 显示导入提示
|
|
@@ -23,156 +23,162 @@ async function promptUserToStart() {
|
|
|
* 请求工具
|
|
* 请求工具
|
|
|
*/
|
|
*/
|
|
|
async function api(url, options = {}) {
|
|
async function api(url, options = {}) {
|
|
|
- //设置默认值
|
|
|
|
|
const method = options.method || (options.data ? "POST" : "GET");
|
|
const method = options.method || (options.data ? "POST" : "GET");
|
|
|
-
|
|
|
|
|
const headers = {
|
|
const headers = {
|
|
|
- "fetch-api": "true",
|
|
|
|
|
|
|
+ "accept": "application/json, text/javascript, */*; q=0.01",
|
|
|
"x-requested-with": "XMLHttpRequest",
|
|
"x-requested-with": "XMLHttpRequest",
|
|
|
- "Referer": "https://jwxt.cap.edu.cn/jwapp/sys/homeapp/home/index.html",
|
|
|
|
|
|
|
+ "Referer": "https://jwxt.cap.edu.cn/jwapp/sys/kbapp/*default/index.do",
|
|
|
...(options.data && { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" }),
|
|
...(options.data && { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" }),
|
|
|
- ...options.headers // 允许传入自定义 header 覆盖上面这些
|
|
|
|
|
|
|
+ ...options.headers
|
|
|
};
|
|
};
|
|
|
- //发起请求
|
|
|
|
|
const res = await fetch(url, {
|
|
const res = await fetch(url, {
|
|
|
method: method,
|
|
method: method,
|
|
|
headers: headers,
|
|
headers: headers,
|
|
|
body: options.data || null,
|
|
body: options.data || null,
|
|
|
credentials: "include"
|
|
credentials: "include"
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
return res.json();
|
|
return res.json();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-//共享变量
|
|
|
|
|
|
|
+// ========== 共享变量 ==========
|
|
|
const AppConfig = {
|
|
const AppConfig = {
|
|
|
currentSemester: null,
|
|
currentSemester: null,
|
|
|
postData: null,
|
|
postData: null,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 提取上课时间 开学时间 课程周数
|
|
|
|
|
- */
|
|
|
|
|
-async function extractCourseTime() {
|
|
|
|
|
-
|
|
|
|
|
- try { //上课时间
|
|
|
|
|
- const userRes = await api("https://jwxt.cap.edu.cn/jwapp/sys/homeapp/api/home/currentUser.do");
|
|
|
|
|
- AppConfig.currentSemester = userRes.datas.welcomeInfo.xnxqdm; //获取学期
|
|
|
|
|
|
|
+// ========== 1. 提取上课时间 & 学期信息 ==========
|
|
|
|
|
+async function extractCourseTime() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 1. 获取当前学期
|
|
|
|
|
+ const userRes = await api(
|
|
|
|
|
+ "https://jwxt.cap.edu.cn/jwapp/sys/homeapp/api/home/currentUser.do"
|
|
|
|
|
+ );
|
|
|
|
|
+ AppConfig.currentSemester = userRes.datas.welcomeInfo.xnxqdm;
|
|
|
console.log("检测到当前学期:", AppConfig.currentSemester);
|
|
console.log("检测到当前学期:", AppConfig.currentSemester);
|
|
|
|
|
|
|
|
AppConfig.postData = `XNXQDM=${AppConfig.currentSemester}&XQDM=01`;
|
|
AppConfig.postData = `XNXQDM=${AppConfig.currentSemester}&XQDM=01`;
|
|
|
- //XQDM这里暂不知道有什么用,2返回的也是一个时间 不知道是不是代表不同校区 暂时用(‘龙泉’校区)替代
|
|
|
|
|
- const res = await api("https://jwxt.cap.edu.cn/jwapp/sys/kbapp/api/wdkbcx/getMySectionList.do", {
|
|
|
|
|
- data: AppConfig.postData,
|
|
|
|
|
- })
|
|
|
|
|
- const rawSections = res.datas.getMySectionList;
|
|
|
|
|
|
|
|
|
|
|
|
+ // 2. 获取节次时间表(小节),这里原来被 return 挡在后面了
|
|
|
|
|
+ const sectionRes = await api(
|
|
|
|
|
+ "https://jwxt.cap.edu.cn/jwapp/sys/kbapp/api/wdkbcx/getMySectionList.do",
|
|
|
|
|
+ { data: AppConfig.postData }
|
|
|
|
|
+ );
|
|
|
|
|
+ const rawSections = sectionRes.datas.getMySectionList;
|
|
|
const cleanSections = rawSections
|
|
const cleanSections = rawSections
|
|
|
- .filter(item => item.name.includes("第"))
|
|
|
|
|
- .map(item => ({
|
|
|
|
|
- "number": parseInt(item.name.replace(/[^0-9]/g, "")),
|
|
|
|
|
- "startTime": item.startTime, // 必须叫 startTime
|
|
|
|
|
- "endTime": item.endTime // 必须叫 endTime
|
|
|
|
|
- }))
|
|
|
|
|
- .sort((a, b) => a.number - b.number);
|
|
|
|
|
- console.log(cleanSections)
|
|
|
|
|
- //开学时间 课程周数
|
|
|
|
|
-
|
|
|
|
|
- const weekRes = await api("https://jwxt.cap.edu.cn/jwapp/sys/homeapp/api/home/getTermWeeks.do",
|
|
|
|
|
- {
|
|
|
|
|
- data: `termCode=${AppConfig.currentSemester}`
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ .filter(item => item.name.includes("第"))
|
|
|
|
|
+ .map(item => ({
|
|
|
|
|
+ number: parseInt(item.name.replace(/[^0-9]/g, "")),
|
|
|
|
|
+ startTime: item.startTime,
|
|
|
|
|
+ endTime: item.endTime
|
|
|
|
|
+ }))
|
|
|
|
|
+ .sort((a, b) => a.number - b.number);
|
|
|
|
|
+ console.log("节次时间表:", cleanSections);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 获取学期周次
|
|
|
|
|
+ const weekRes = await api(
|
|
|
|
|
+ "https://jwxt.cap.edu.cn/jwapp/sys/homeapp/api/home/getTermWeeks.do",
|
|
|
|
|
+ { data: `termCode=${AppConfig.currentSemester}` }
|
|
|
|
|
+ );
|
|
|
const finalWeeks = weekRes.datas.map(item => ({
|
|
const finalWeeks = weekRes.datas.map(item => ({
|
|
|
- "week": item.serialNumber, // 周序 (1, 2, 3...)
|
|
|
|
|
- "startTime": item.startDate.split(' ')[0], // 格式化为 YYYY-MM-DD
|
|
|
|
|
- "endTime": item.endDate.split(' ')[0], // 格式化为 YYYY-MM-DD
|
|
|
|
|
- "isCurrent": item.curWeek // 是否为当前周
|
|
|
|
|
|
|
+ week: item.serialNumber,
|
|
|
|
|
+ startTime: item.startDate.split(' ')[0],
|
|
|
|
|
+ endTime: item.endDate.split(' ')[0],
|
|
|
|
|
+ isCurrent: item.curWeek
|
|
|
}));
|
|
}));
|
|
|
const totalWeeks = finalWeeks.length;
|
|
const totalWeeks = finalWeeks.length;
|
|
|
const startDate = finalWeeks[0].startTime;
|
|
const startDate = finalWeeks[0].startTime;
|
|
|
- console.log(AppConfig.currentSemester, totalWeeks,startDate,cleanSections)
|
|
|
|
|
-
|
|
|
|
|
|
|
+ console.log("学期信息:", {
|
|
|
|
|
+ semester: AppConfig.currentSemester,
|
|
|
|
|
+ totalWeeks,
|
|
|
|
|
+ startDate
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 把 cleanSections 带出去
|
|
|
return {
|
|
return {
|
|
|
currentSemester: AppConfig.currentSemester,
|
|
currentSemester: AppConfig.currentSemester,
|
|
|
totalWeeks,
|
|
totalWeeks,
|
|
|
startDate,
|
|
startDate,
|
|
|
- cleanSections //每日课程时间 timeSlots!!!
|
|
|
|
|
|
|
+ cleanSections
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
- }
|
|
|
|
|
- catch (error) {
|
|
|
|
|
- console.error('解析开学时间时出错:', error);
|
|
|
|
|
- AndroidBridge.showToast(`解析开学时间失败: ${error.message}`);
|
|
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('解析基础信息时出错:', error);
|
|
|
|
|
+ AndroidBridge.showToast(`解析失败: ${error.message}`);
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-}//返回 学期时间 课程周数 开始时间 时间表
|
|
|
|
|
-// 2025-2026-2 19 2026-03-09 Array
|
|
|
|
|
|
|
+// ========== 2. 获取课表原始数据 ==========
|
|
|
|
|
+async function getCourseData(totalWeeks) {
|
|
|
|
|
+ const allRaw = [];
|
|
|
|
|
+ const seen = new Set();
|
|
|
|
|
+
|
|
|
|
|
+ const weekRequests = [];
|
|
|
|
|
+ for (let zc = 1; zc <= totalWeeks; zc++) {
|
|
|
|
|
+ weekRequests.push(
|
|
|
|
|
+ api("https://jwxt.cap.edu.cn/jwapp/sys/kbapp/api/wdkbcx/getMyScheduleDetail.do", {
|
|
|
|
|
+ data: `${AppConfig.postData}&ZC=${zc}`,
|
|
|
|
|
+ }).then(res => {
|
|
|
|
|
+ const list = res?.datas?.getMyScheduleDetail?.arrangedList || [];
|
|
|
|
|
+ list.forEach(item => {
|
|
|
|
|
+ // 更精确的去重键:课程名+教学班ID+星期+节次+周次字符串
|
|
|
|
|
+ // 加上周次字符串可以避免同一门课在不同周被误判为重复
|
|
|
|
|
+ const key = `${item.courseCode || item.courseName}|${item.teachClassId}|${item.dayOfWeek}|${item.beginSection}`;
|
|
|
|
|
+
|
|
|
|
|
+ if (!seen.has(key)) {
|
|
|
|
|
+ seen.add(key);
|
|
|
|
|
+ allRaw.push(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }).catch(e => {
|
|
|
|
|
+ console.warn(`第${zc}周请求失败:`, e.message);
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 获取课表数据 返回的是原始数据
|
|
|
|
|
- */
|
|
|
|
|
-async function getCourseData() {
|
|
|
|
|
- const courseRes = await api("https://jwxt.cap.edu.cn/jwapp/sys/kbapp/api/wdkbcx/getMyScheduleDetail.do", {
|
|
|
|
|
- data: AppConfig.postData,
|
|
|
|
|
- })
|
|
|
|
|
- const rawCourses = courseRes?.datas?.getMyScheduleDetail?.arrangedList || [];
|
|
|
|
|
- // console.log("获取到课程数据:", rawCourses);
|
|
|
|
|
- return rawCourses;
|
|
|
|
|
|
|
+ await Promise.all(weekRequests);
|
|
|
|
|
+ console.log(`获取到 ${allRaw.length} 条课程数据(含短期实验/实习)`);
|
|
|
|
|
+ return allRaw;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function parseWeeks(weekStr) {
|
|
|
|
|
|
|
+// ========== 3. 辅助周次解析函数 ==========
|
|
|
|
|
+function parseWeekString(weekStr) {
|
|
|
|
|
+ // "101101011111111111" -> [1,3,4,6,8,9,...]
|
|
|
if (!weekStr) return [];
|
|
if (!weekStr) return [];
|
|
|
const weeks = [];
|
|
const weeks = [];
|
|
|
- // 1. 处理逗号分隔的多个区间
|
|
|
|
|
- const segments = weekStr.replace(/周/g, "").split(",");
|
|
|
|
|
-
|
|
|
|
|
- segments.forEach(seg => {
|
|
|
|
|
- // 处理单双周逻辑
|
|
|
|
|
- const isOnlyOdd = seg.includes("(单)");
|
|
|
|
|
- const isOnlyEven = seg.includes("(双)");
|
|
|
|
|
- const cleanSeg = seg.replace(/\(单\)|\(双\)/g, "");
|
|
|
|
|
-
|
|
|
|
|
- if (cleanSeg.includes("-")) {
|
|
|
|
|
- // 处理范围型:1-4
|
|
|
|
|
- const [start, end] = cleanSeg.split("-").map(Number);
|
|
|
|
|
- for (let i = start; i <= end; i++) {
|
|
|
|
|
- if (isOnlyOdd && i % 2 === 0) continue;
|
|
|
|
|
- if (isOnlyEven && i % 2 !== 0) continue;
|
|
|
|
|
- weeks.push(i);
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // 处理单个数字
|
|
|
|
|
- const num = Number(cleanSeg);
|
|
|
|
|
- if (!isNaN(num)) weeks.push(num);
|
|
|
|
|
|
|
+ for (let i = 0; i < weekStr.length; i++) {
|
|
|
|
|
+ if (weekStr[i] === '1') {
|
|
|
|
|
+ weeks.push(i + 1);
|
|
|
}
|
|
}
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- return [...new Set(weeks)].sort((a, b) => a - b);
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ return weeks;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 1. 展开周次函数:支持 1-3周(单), 7-17周(单) 等
|
|
|
|
|
|
|
+ * 解析学期间的周次描述,例如 "14-15周"、"3周"、"1-3周(单),7周,11-17周(单)"
|
|
|
|
|
+ * 返回周次数组
|
|
|
*/
|
|
*/
|
|
|
-function expandWeeks(rawStr) {
|
|
|
|
|
|
|
+function parseWeeksDescription(desc) {
|
|
|
|
|
+ if (!desc) return [];
|
|
|
const weeks = [];
|
|
const weeks = [];
|
|
|
- if (!rawStr) return weeks;
|
|
|
|
|
-
|
|
|
|
|
- const cleanStr = rawStr.replace(/\s+/g, '').replace(/,/g, ',').replace(/周/g, '');
|
|
|
|
|
- const isOdd = cleanStr.includes('(单)');
|
|
|
|
|
- const isEven = cleanStr.includes('(双)');
|
|
|
|
|
- const rangePart = cleanStr.replace(/\([单双]\)/g, '');
|
|
|
|
|
-
|
|
|
|
|
- rangePart.split(',').forEach(segment => {
|
|
|
|
|
- if (segment.includes('-')) {
|
|
|
|
|
- const [start, end] = segment.split('-').map(Number);
|
|
|
|
|
|
|
+ // 预处理:去掉“周”字、空格,中文逗号变英文
|
|
|
|
|
+ let clean = desc.replace(/\s+/g, '').replace(/,/g, ',').replace(/周/g, '');
|
|
|
|
|
+
|
|
|
|
|
+ const segments = clean.split(',');
|
|
|
|
|
+ segments.forEach(seg => {
|
|
|
|
|
+ // 检测单双周标记
|
|
|
|
|
+ const isOdd = seg.includes('(单)');
|
|
|
|
|
+ const isEven = seg.includes('(双)');
|
|
|
|
|
+ seg = seg.replace(/\(单\)|\(双\)/g, '');
|
|
|
|
|
+
|
|
|
|
|
+ if (seg.includes('-')) {
|
|
|
|
|
+ const [start, end] = seg.split('-').map(Number);
|
|
|
for (let i = start; i <= end; i++) {
|
|
for (let i = start; i <= end; i++) {
|
|
|
if (isOdd && i % 2 === 0) continue;
|
|
if (isOdd && i % 2 === 0) continue;
|
|
|
if (isEven && i % 2 !== 0) continue;
|
|
if (isEven && i % 2 !== 0) continue;
|
|
|
weeks.push(i);
|
|
weeks.push(i);
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- const num = parseInt(segment);
|
|
|
|
|
|
|
+ const num = parseInt(seg);
|
|
|
if (!isNaN(num)) {
|
|
if (!isNaN(num)) {
|
|
|
if (isOdd && num % 2 === 0) return;
|
|
if (isOdd && num % 2 === 0) return;
|
|
|
if (isEven && num % 2 !== 0) return;
|
|
if (isEven && num % 2 !== 0) return;
|
|
@@ -180,264 +186,234 @@ function expandWeeks(rawStr) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
- return weeks;
|
|
|
|
|
|
|
+ return [...new Set(weeks)].sort((a, b) => a - b);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 4. 从HTML片段中提取教师姓名 ==========
|
|
|
|
|
+function extractTeacherFromHTML(html) {
|
|
|
|
|
+ if (!html) return null;
|
|
|
|
|
+ const match = html.match(/<a[^>]*>([^<]+)<\/a>/);
|
|
|
|
|
+ return match ? match[1].trim() : null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 2. 单行解析函数:提取核心信息
|
|
|
|
|
|
|
+ * 从描述文本中提取非教师、非校区的备注地点
|
|
|
*/
|
|
*/
|
|
|
-function parseDetailLine(line) {
|
|
|
|
|
- // 移除 HTML 标签
|
|
|
|
|
- const cleanLine = line.replace(/<[^>]+>/g, "").trim();
|
|
|
|
|
- const parts = cleanLine.split(/\s+/);
|
|
|
|
|
-
|
|
|
|
|
- // 假设格式为:[周次] [老师] [建筑/校区] [具体地点]
|
|
|
|
|
- const rawWeek = parts[0] || "";
|
|
|
|
|
- const teacher = parts[1] || "未知教师";
|
|
|
|
|
- const building = parts[2] || "";
|
|
|
|
|
- const location = parts[3] || "";
|
|
|
|
|
-
|
|
|
|
|
- let finalPosition = "";
|
|
|
|
|
-
|
|
|
|
|
- if (location.includes(building)) {
|
|
|
|
|
- finalPosition = location;
|
|
|
|
|
- } else {
|
|
|
|
|
- finalPosition = building + " " + location;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+function extractExtraLocation(htmlText, campusName, teacher) {
|
|
|
|
|
+ if (!htmlText) return '';
|
|
|
|
|
+ // 去掉所有HTML标签
|
|
|
|
|
+ let clean = htmlText.replace(/<[^>]+>/g, ' ').trim();
|
|
|
|
|
+ // 去掉已知校区名
|
|
|
|
|
+ if (campusName) clean = clean.replace(new RegExp(campusName, 'g'), '');
|
|
|
|
|
+ // 去掉开头的周次描述
|
|
|
|
|
+ clean = clean.replace(/^\d+(-\d+)?周\s*/, '');
|
|
|
|
|
+ // 去掉教师姓名(如果传入)
|
|
|
|
|
+ if (teacher) clean = clean.replace(new RegExp(teacher, 'g'), '');
|
|
|
|
|
+ // 清理多余空格
|
|
|
|
|
+ clean = clean.replace(/\s+/g, ' ').trim();
|
|
|
|
|
+ return clean;
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- finalPosition = finalPosition.trim();
|
|
|
|
|
|
|
+// ========== 5. 核心解析函数:将单条 raw item 解析为多个 course 片段 ==========
|
|
|
|
|
+function parseCourseItem(item) {
|
|
|
|
|
+ const courseName = item.courseName;
|
|
|
|
|
+ const day = item.dayOfWeek;
|
|
|
|
|
+ const beginSection = item.beginSection;
|
|
|
|
|
+ const endSection = item.endSection;
|
|
|
|
|
+ const campusName = item.campusName || '';
|
|
|
|
|
+ const placeName = item.placeName || '';
|
|
|
|
|
+ const tags = item.tags || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 优先使用 cellWeekTeacherClassroomDetail,如果为空则用 multiCourseTitleDetail 或 titleDetail
|
|
|
|
|
+ let segmentsSource = [];
|
|
|
|
|
+
|
|
|
|
|
+ if (item.cellWeekTeacherClassroomDetail && item.cellWeekTeacherClassroomDetail.length > 0) {
|
|
|
|
|
+ segmentsSource = item.cellWeekTeacherClassroomDetail.map(cell => cell.text);
|
|
|
|
|
+ } else if (item.multiCourseTitleDetail && item.multiCourseTitleDetail.length > 1) {
|
|
|
|
|
+ segmentsSource = item.multiCourseTitleDetail
|
|
|
|
|
+ .slice(1)
|
|
|
|
|
+ .filter(line => {
|
|
|
|
|
+ const plainText = line.replace(/<[^>]+>/g, '').trim();
|
|
|
|
|
+ // 过滤掉纯数字/逗号/空格组成的行(班级列表)
|
|
|
|
|
+ if (/^[\d,\s]+$/.test(plainText)) return false;
|
|
|
|
|
+ // 过滤掉空行
|
|
|
|
|
+ return plainText.length > 0;
|
|
|
|
|
+ })
|
|
|
|
|
+ .map(line => line.trim())
|
|
|
|
|
+ .filter(line => line !== '');
|
|
|
|
|
+ } else if (item.titleDetail && item.titleDetail.length > 1) {
|
|
|
|
|
+ // ✅ 兜底:只有 titleDetail 时,用其中第一行教师/地点信息
|
|
|
|
|
+ segmentsSource = [item.titleDetail[1]];
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return {
|
|
|
|
|
- rawWeek,
|
|
|
|
|
- teacher,
|
|
|
|
|
- building,
|
|
|
|
|
- location,
|
|
|
|
|
- weeks: parseWeeks(rawWeek)
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ const courses = [];
|
|
|
|
|
+ // 保存总周次作为兜底
|
|
|
|
|
+ const totalWeeks = parseWeekString(item.week || '');
|
|
|
|
|
+
|
|
|
|
|
+ segmentsSource.forEach(segText => {
|
|
|
|
|
+ const teacher = extractTeacherFromHTML(segText) || '未知教师';
|
|
|
|
|
+ let weeks;
|
|
|
|
|
+ // 尝试从文本中提取周次描述
|
|
|
|
|
+ const weekDescMatch = segText.match(/^([\d\-\(\),周单双\s]+?)\s*</);
|
|
|
|
|
+ if (weekDescMatch && weekDescMatch[1]) {
|
|
|
|
|
+ const wd = weekDescMatch[1].trim();
|
|
|
|
|
+ weeks = parseWeeksDescription(wd);
|
|
|
|
|
+ if (weeks.length === 0) weeks = totalWeeks;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ weeks = totalWeeks;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 3. 智能汇总函数:处理地点变动逻辑
|
|
|
|
|
- */
|
|
|
|
|
-function extractAndMergeCourse(titleDetail) {
|
|
|
|
|
- if (!titleDetail || titleDetail.length === 0) return null;
|
|
|
|
|
-
|
|
|
|
|
- const courseName = titleDetail[0];
|
|
|
|
|
- // 过滤掉第一行课程名,解析后面所有的详情行
|
|
|
|
|
- const rawSlots = titleDetail.slice(1).map(line => parseDetailLine(line));
|
|
|
|
|
-
|
|
|
|
|
- const mergedMap = new Map();
|
|
|
|
|
-
|
|
|
|
|
- rawSlots.forEach(slot => {
|
|
|
|
|
- // 连堂课如果地点老师一样,就合并周次;如果不一样(比如一半在教室一半在实验室),会拆分成两个 segment
|
|
|
|
|
- const identifier = `${slot.teacher}|${slot.building}|${slot.location}`;
|
|
|
|
|
-
|
|
|
|
|
- if (mergedMap.has(identifier)) {
|
|
|
|
|
- const existing = mergedMap.get(identifier);
|
|
|
|
|
- // 合并周次并去重排序
|
|
|
|
|
- existing.weeks = [...new Set([...existing.weeks, ...slot.weeks])].sort((a, b) => a - b);
|
|
|
|
|
- existing.rawWeeksDesc += `, ${slot.rawWeek}`;
|
|
|
|
|
|
|
+ // 确定地点
|
|
|
|
|
+ let position;
|
|
|
|
|
+ if (placeName) {
|
|
|
|
|
+ position = (campusName && !placeName.includes(campusName)) ? `${campusName} ${placeName}` : placeName;
|
|
|
} else {
|
|
} else {
|
|
|
- mergedMap.set(identifier, {
|
|
|
|
|
- teacher: slot.teacher,
|
|
|
|
|
- building: slot.building,
|
|
|
|
|
- location: slot.location,
|
|
|
|
|
- weeks: slot.weeks,
|
|
|
|
|
- rawWeeksDesc: slot.rawWeek
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // placeName 为空时,从描述提取备注
|
|
|
|
|
+ const extra = extractExtraLocation(segText, campusName, teacher);
|
|
|
|
|
+ position = campusName ? `${campusName} ${extra}`.trim() : extra;
|
|
|
}
|
|
}
|
|
|
|
|
+ position = position || campusName || '未知地点';
|
|
|
|
|
+
|
|
|
|
|
+ courses.push({
|
|
|
|
|
+ name: courseName,
|
|
|
|
|
+ teacher: teacher,
|
|
|
|
|
+ position: position.trim(),
|
|
|
|
|
+ day: day,
|
|
|
|
|
+ startSection: beginSection,
|
|
|
|
|
+ endSection: endSection,
|
|
|
|
|
+ weeks: weeks,
|
|
|
|
|
+ campusName: campusName,
|
|
|
|
|
+ rawPlaceName: placeName,
|
|
|
|
|
+ hasExperimentTag: tags.some(t => t.text === '实')
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- const segments = Array.from(mergedMap.values());
|
|
|
|
|
-
|
|
|
|
|
- // --- 修复点:先计算,再打印和返回 ---
|
|
|
|
|
- const allActiveWeeks = [...new Set(segments.flatMap(s => s.weeks))].sort((a, b) => a - b);
|
|
|
|
|
-
|
|
|
|
|
- console.log("解析课程:", courseName, "总周次:", allActiveWeeks);
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- courseName,
|
|
|
|
|
- allActiveWeeks,
|
|
|
|
|
- segments
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ return courses;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ========== 6. 聚合所有课程并映射小节编号 ==========
|
|
|
|
|
+function parseAllCourses(rawArrangedList, sectionList) {
|
|
|
|
|
+ const allCourses = [];
|
|
|
|
|
|
|
|
-function fixSection(section) {
|
|
|
|
|
- let realSection = section;
|
|
|
|
|
- if (section > 5) realSection -= 1; // 超过午餐,向上平移1格
|
|
|
|
|
- if (section > 10) realSection -= 1; // 超过晚餐,再向上平移1格
|
|
|
|
|
- return realSection;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ if (!rawArrangedList || !Array.isArray(rawArrangedList)) {
|
|
|
|
|
+ return { courses: [], timeSlots: [] };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 构建时间 -> 小节编号 的映射
|
|
|
|
|
+ const startTimeToSection = {};
|
|
|
|
|
+ const endTimeToSection = {};
|
|
|
|
|
+ sectionList.forEach(slot => {
|
|
|
|
|
+ startTimeToSection[slot.startTime] = slot.number;
|
|
|
|
|
+ endTimeToSection[slot.endTime] = slot.number;
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 解析所有课程并应用修正
|
|
|
|
|
- */
|
|
|
|
|
-function parseAllCourses(rawArrangedList) {
|
|
|
|
|
- const finalCourses = [];
|
|
|
|
|
- if (!rawArrangedList || !Array.isArray(rawArrangedList)) return [];
|
|
|
|
|
-
|
|
|
|
|
rawArrangedList.forEach(item => {
|
|
rawArrangedList.forEach(item => {
|
|
|
- if (item.titleDetail && item.titleDetail.length > 0) {
|
|
|
|
|
- const mergedResult = extractAndMergeCourse(item.titleDetail);
|
|
|
|
|
- if (!mergedResult) return;
|
|
|
|
|
-
|
|
|
|
|
- // 1. 获取原始节次数据
|
|
|
|
|
- const rawStart = parseInt(item.beginSection || item.startSection);
|
|
|
|
|
- const rawEnd = parseInt(item.endSection);
|
|
|
|
|
- const day = parseInt(item.dayOfWeek || item.day);
|
|
|
|
|
-
|
|
|
|
|
- // 2. 调用 fixSection 解决错位
|
|
|
|
|
- const sSection = fixSection(rawStart);
|
|
|
|
|
- const eSection = fixSection(rawEnd);
|
|
|
|
|
-
|
|
|
|
|
- mergedResult.segments.forEach(seg => {
|
|
|
|
|
- if (!isNaN(sSection) && !isNaN(eSection)) {
|
|
|
|
|
- // 3. 处理地点显示问题(去重:避免出现 "励行楼 励行楼xxx")
|
|
|
|
|
- let finalPos = seg.location;
|
|
|
|
|
- if (seg.building && !seg.location.includes(seg.building)) {
|
|
|
|
|
- finalPos = seg.building + " " + seg.location;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (item.dayOfWeek === null || item.beginSection === null) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 根据 beginTime 和 endTime 查找正确的小节区间
|
|
|
|
|
+ const realStart = startTimeToSection[item.beginTime];
|
|
|
|
|
+ const realEnd = endTimeToSection[item.endTime];
|
|
|
|
|
|
|
|
- finalCourses.push({
|
|
|
|
|
- name: mergedResult.courseName,
|
|
|
|
|
- teacher: seg.teacher,
|
|
|
|
|
- position: finalPos.trim(),
|
|
|
|
|
- day: day,
|
|
|
|
|
- startSection: sSection, // 写入修正后的开始节次
|
|
|
|
|
- endSection: eSection, // 写入修正后的结束节次
|
|
|
|
|
- weeks: seg.weeks
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ if (realStart === undefined || realEnd === undefined) {
|
|
|
|
|
+ // 时间无法匹配,丢弃该课程(或使用原始值,但不推荐)
|
|
|
|
|
+ console.warn(`课程 ${item.courseName} 时间无法匹配时间槽: ${item.beginTime}-${item.endTime}`);
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 用正确的小节编号覆盖原始 beginSection/endSection
|
|
|
|
|
+ const correctedItem = {
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ beginSection: realStart,
|
|
|
|
|
+ endSection: realEnd
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const courses = parseCourseItem(correctedItem);
|
|
|
|
|
+ allCourses.push(...courses);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- return finalCourses;
|
|
|
|
|
-}
|
|
|
|
|
-/**
|
|
|
|
|
- * 获取所有课程信息
|
|
|
|
|
- */
|
|
|
|
|
-async function fetchAllRawData() {
|
|
|
|
|
- try {
|
|
|
|
|
- // 获取基础环境信息 (学期、开学日期、时间表)
|
|
|
|
|
- const baseInfo = await extractCourseTime();
|
|
|
|
|
- if (!baseInfo) return null;
|
|
|
|
|
-
|
|
|
|
|
- const rawArrangedList = await getCourseData();
|
|
|
|
|
-
|
|
|
|
|
- if (!rawArrangedList || rawArrangedList.length === 0) {
|
|
|
|
|
- AndroidBridge.showToast("未检测到当前学期的课程数据");
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 时间槽直接使用 sectionList,编号保持 1,2,3...
|
|
|
|
|
+ const timeSlots = sectionList.map(sec => ({
|
|
|
|
|
+ number: sec.number,
|
|
|
|
|
+ startTime: sec.startTime,
|
|
|
|
|
+ endTime: sec.endTime
|
|
|
|
|
+ }));
|
|
|
|
|
|
|
|
- return { baseInfo, rawArrangedList };
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error("抓取数据失败:", e);
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ console.log(`解析完成,共 ${allCourses.length} 个课程片段,${timeSlots.length} 个时间段`);
|
|
|
|
|
+ return { courses: allCourses, timeSlots };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 保存
|
|
|
|
|
- */
|
|
|
|
|
- async function executeSaveSequence(finalCourses, baseInfo) {
|
|
|
|
|
- try {
|
|
|
|
|
- // 1. 保存基础配置 (开学日期、总周数)
|
|
|
|
|
- const configData = {
|
|
|
|
|
- semesterStartDate: baseInfo.startDate,
|
|
|
|
|
- semesterTotalWeeks: baseInfo.totalWeeks || 20,
|
|
|
|
|
- };
|
|
|
|
|
|
|
|
|
|
- const configSuccess = await AndroidBridge.saveCourseConfig(JSON.stringify(configData));
|
|
|
|
|
-
|
|
|
|
|
- if (!configSuccess) {
|
|
|
|
|
- AndroidBridge.showToast("学期保存失败");
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 2. 保存时间段 (节次时间表
|
|
|
|
|
- const slotSuccess = await AndroidBridge.savePresetTimeSlots(JSON.stringify(baseInfo.cleanSections));
|
|
|
|
|
- if (!slotSuccess) return false;
|
|
|
|
|
|
|
+// ========== 7. 获取所有数据 ==========
|
|
|
|
|
+async function fetchAllRawData() {
|
|
|
|
|
+ const baseInfo = await extractCourseTime();
|
|
|
|
|
+ if (!baseInfo) return null;
|
|
|
|
|
|
|
|
- // 3. 保存课程数据
|
|
|
|
|
- const saveResult = await AndroidBridge.saveImportedCourses(JSON.stringify(finalCourses));
|
|
|
|
|
-
|
|
|
|
|
- return saveResult;
|
|
|
|
|
|
|
+ const rawArrangedList = await getCourseData(baseInfo.totalWeeks);
|
|
|
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error("保存流程崩溃:", e);
|
|
|
|
|
- AndroidBridge.showToast("导入过程发生意外");
|
|
|
|
|
- return false;
|
|
|
|
|
|
|
+ if (!rawArrangedList || rawArrangedList.length === 0) {
|
|
|
|
|
+ AndroidBridge.showToast("未检测到当前学期的课程数据");
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
|
|
+ return { baseInfo, rawArrangedList };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 保存配置 (日期和周数)
|
|
|
|
|
- */
|
|
|
|
|
|
|
+// ========== 8. 保存配置 ==========
|
|
|
async function saveConfig(baseInfo) {
|
|
async function saveConfig(baseInfo) {
|
|
|
const configData = {
|
|
const configData = {
|
|
|
semesterStartDate: baseInfo.startDate,
|
|
semesterStartDate: baseInfo.startDate,
|
|
|
semesterTotalWeeks: baseInfo.totalWeeks || 20,
|
|
semesterTotalWeeks: baseInfo.totalWeeks || 20,
|
|
|
-
|
|
|
|
|
};
|
|
};
|
|
|
try {
|
|
try {
|
|
|
const configSuccess = await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(configData));
|
|
const configSuccess = await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(configData));
|
|
|
- if (configSuccess) {
|
|
|
|
|
- return true;
|
|
|
|
|
|
|
+ if (!configSuccess) {
|
|
|
|
|
+ AndroidBridge.showToast("学期保存失败");
|
|
|
|
|
+ return false;
|
|
|
}
|
|
}
|
|
|
- return false;
|
|
|
|
|
|
|
+ return true;
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
AndroidBridge.showToast("保存配置失败: " + error.message);
|
|
AndroidBridge.showToast("保存配置失败: " + error.message);
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 主导入流
|
|
|
|
|
- */
|
|
|
|
|
|
|
+// ========== 9. 主导入流程 ==========
|
|
|
async function runImportFlow() {
|
|
async function runImportFlow() {
|
|
|
try {
|
|
try {
|
|
|
- // 1. 前置确认
|
|
|
|
|
const isReady = await promptUserToStart();
|
|
const isReady = await promptUserToStart();
|
|
|
if (!isReady) return;
|
|
if (!isReady) return;
|
|
|
|
|
|
|
|
- // 2. 抓取所有必要数据
|
|
|
|
|
const dataBundle = await fetchAllRawData();
|
|
const dataBundle = await fetchAllRawData();
|
|
|
if (!dataBundle) return;
|
|
if (!dataBundle) return;
|
|
|
|
|
|
|
|
- // 3. 解析原始数据
|
|
|
|
|
- const finalCourses = parseAllCourses(dataBundle.rawArrangedList);
|
|
|
|
|
|
|
+ const { courses: finalCourses, timeSlots } = parseAllCourses(dataBundle.rawArrangedList, dataBundle.baseInfo.cleanSections);
|
|
|
if (finalCourses.length === 0) {
|
|
if (finalCourses.length === 0) {
|
|
|
AndroidBridge.showToast("解析失败:未能提取到有效课程");
|
|
AndroidBridge.showToast("解析失败:未能提取到有效课程");
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 4. 保存配置数据 (存日期、周数)
|
|
|
|
|
|
|
+ // 保存学期配置
|
|
|
const configSaveResult = await saveConfig(dataBundle.baseInfo);
|
|
const configSaveResult = await saveConfig(dataBundle.baseInfo);
|
|
|
if (!configSaveResult) return;
|
|
if (!configSaveResult) return;
|
|
|
|
|
|
|
|
- //时间段保存
|
|
|
|
|
|
|
+ // 保存时间段 (基于实际课程生成的大节)
|
|
|
try {
|
|
try {
|
|
|
- const slotJson = JSON.stringify(dataBundle.baseInfo.cleanSections);
|
|
|
|
|
|
|
+ const slotJson = JSON.stringify(timeSlots);
|
|
|
console.log("写入时间段数据:", slotJson);
|
|
console.log("写入时间段数据:", slotJson);
|
|
|
await window.AndroidBridgePromise.savePresetTimeSlots(slotJson);
|
|
await window.AndroidBridgePromise.savePresetTimeSlots(slotJson);
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error("时间段写入失败:", e);
|
|
console.error("时间段写入失败:", e);
|
|
|
- // 这里可以选择跳过或报错
|
|
|
|
|
|
|
+ AndroidBridge.showToast("时间段保存失败");
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 5. 课程数据保存
|
|
|
|
|
|
|
+ // 保存课程数据
|
|
|
const saveResult = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(finalCourses));
|
|
const saveResult = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(finalCourses));
|
|
|
if (!saveResult) {
|
|
if (!saveResult) {
|
|
|
AndroidBridge.showToast("课程数据保存失败");
|
|
AndroidBridge.showToast("课程数据保存失败");
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 6. 流程成功结束
|
|
|
|
|
- AndroidBridge.showToast("Hi ~ 课表导入成功!");
|
|
|
|
|
|
|
+ AndroidBridge.showToast("Hi ~ 课表导入成功!");
|
|
|
AndroidBridge.notifyTaskCompletion();
|
|
AndroidBridge.notifyTaskCompletion();
|
|
|
|
|
|
|
|
} catch (error) {
|
|
} catch (error) {
|