|
|
@@ -6,58 +6,140 @@ const PRESET_TIME_SLOTS = [
|
|
|
{ number: 3, startTime: "09:50", endTime: "10:30" },
|
|
|
{ number: 4, startTime: "10:40", endTime: "11:20" },
|
|
|
{ number: 5, startTime: "11:30", endTime: "12:10" },
|
|
|
- { number: 6, startTime: "14:30", endTime: "15:10" },
|
|
|
- { number: 7, startTime: "15:20", endTime: "16:00" },
|
|
|
- { number: 8, startTime: "16:20", endTime: "17:00" },
|
|
|
- { number: 9, startTime: "17:10", endTime: "17:50" },
|
|
|
- { number: 10, startTime: "18:00", endTime: "18:40" },
|
|
|
- { number: 11, startTime: "19:30", endTime: "20:10" },
|
|
|
- { number: 12, startTime: "20:20", endTime: "21:00" }
|
|
|
+ { number: 6, startTime: "14:00", endTime: "14:40" },
|
|
|
+ { number: 7, startTime: "14:50", endTime: "15:30" },
|
|
|
+ { number: 8, startTime: "15:50", endTime: "16:30" },
|
|
|
+ { number: 9, startTime: "16:40", endTime: "17:20" },
|
|
|
+ { number: 10, startTime: "17:30", endTime: "18:10" },
|
|
|
+ { number: 11, startTime: "19:00", endTime: "19:40" },
|
|
|
+ { number: 12, startTime: "19:50", endTime: "20:30" },
|
|
|
+ { number: 13, startTime: "20:50", endTime: "21:30" },
|
|
|
+ { number: 14, startTime: "21:40", endTime: "22:20" }
|
|
|
];
|
|
|
|
|
|
-// 周次字符串 "10,7,8,9" → 去重排序的周数组
|
|
|
+function uniqueSortedNumbers(numbers) {
|
|
|
+ return [...new Set(numbers)].sort((a, b) => a - b);
|
|
|
+}
|
|
|
+
|
|
|
+// 支持 "10,7,8,9"、"1-4"、"1-4,7" 等周次写法。
|
|
|
function parseWeeks(weekStr) {
|
|
|
if (!weekStr) return [];
|
|
|
- const weeks = weekStr.split(",").map(w => parseInt(w.trim(), 10)).filter(w => !isNaN(w) && w > 0);
|
|
|
- return [...new Set(weeks)].sort((a, b) => a - b);
|
|
|
+ const weeks = [];
|
|
|
+ String(weekStr).replace(/[,、]/g, ",").split(",").forEach(part => {
|
|
|
+ const text = part.trim().replace(/周/g, "");
|
|
|
+ const range = text.match(/^(\d+)\s*[-~]\s*(\d+)$/);
|
|
|
+ if (range) {
|
|
|
+ const start = parseInt(range[1], 10);
|
|
|
+ const end = parseInt(range[2], 10);
|
|
|
+ if (start > 0 && end >= start) {
|
|
|
+ for (let week = start; week <= end; week += 1) weeks.push(week);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const week = parseInt(text, 10);
|
|
|
+ if (!isNaN(week) && week > 0) weeks.push(week);
|
|
|
+ });
|
|
|
+ return uniqueSortedNumbers(weeks);
|
|
|
}
|
|
|
|
|
|
-// 教室可能为空、含 "\\" 分隔的多教室(如 "B205\206教室")或以 "," 分隔的多场地
|
|
|
+function parseSections(sectionStr) {
|
|
|
+ return uniqueSortedNumbers(
|
|
|
+ String(sectionStr || "")
|
|
|
+ .replace(/[,、]/g, ",")
|
|
|
+ .split(",")
|
|
|
+ .map(section => parseInt(section.trim(), 10))
|
|
|
+ .filter(section => !isNaN(section) && section > 0)
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// 反斜杠表示合并教室(如 "B205\206教室"),逗号才是不同地点的分隔符。
|
|
|
function resolvePosition(raw) {
|
|
|
- const position = String(raw || "").replace(/\\/g, "/").trim();
|
|
|
+ const position = String(raw || "").replace(/\\/g, "/").replace(/\s+/g, " ").trim();
|
|
|
return position || "待定";
|
|
|
}
|
|
|
|
|
|
-// kbxx 课程 JSON → 拾光课程格式(合并同 key 课程的周次)
|
|
|
+function cleanTeacher(raw) {
|
|
|
+ const teacher = String(raw || "")
|
|
|
+ .split(/[,,、]/)
|
|
|
+ .map(name => name.trim())
|
|
|
+ .filter(Boolean)
|
|
|
+ .join("、");
|
|
|
+ return teacher || "未知";
|
|
|
+}
|
|
|
+
|
|
|
+function courseKey(course) {
|
|
|
+ return [
|
|
|
+ course.name,
|
|
|
+ course.teacher,
|
|
|
+ course.position,
|
|
|
+ course.day,
|
|
|
+ course.startSection,
|
|
|
+ course.endSection
|
|
|
+ ].join("__");
|
|
|
+}
|
|
|
+
|
|
|
+function addCourse(courseMap, item, weeks) {
|
|
|
+ const day = parseInt(item.xq, 10);
|
|
|
+ const sections = parseSections(item.jcdm2);
|
|
|
+ const allWeeks = uniqueSortedNumbers(weeks);
|
|
|
+ if (!item.kcmc || sections.length === 0 || allWeeks.length === 0 ||
|
|
|
+ isNaN(day) || day < 1 || day > 7) return;
|
|
|
+
|
|
|
+ const course = {
|
|
|
+ name: String(item.kcmc).trim(),
|
|
|
+ teacher: cleanTeacher(item.teaxms),
|
|
|
+ position: resolvePosition(item.jxcdmc || item.jxcdmcs),
|
|
|
+ day,
|
|
|
+ startSection: Math.min(...sections),
|
|
|
+ endSection: Math.max(...sections),
|
|
|
+ weeks: allWeeks
|
|
|
+ };
|
|
|
+
|
|
|
+ const key = courseKey(course);
|
|
|
+ const existing = courseMap.get(key);
|
|
|
+ if (existing) {
|
|
|
+ existing.weeks = uniqueSortedNumbers([...existing.weeks, ...course.weeks]);
|
|
|
+ } else {
|
|
|
+ courseMap.set(key, course);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function sortCourses(courses) {
|
|
|
+ return courses.sort((a, b) =>
|
|
|
+ a.day - b.day ||
|
|
|
+ a.startSection - b.startSection ||
|
|
|
+ a.endSection - b.endSection ||
|
|
|
+ a.name.localeCompare(b.name)
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// 从整学期课表解析课程,作为周课表接口不可用时的回退。
|
|
|
function parseCourseList(kbxx) {
|
|
|
if (!Array.isArray(kbxx)) throw new Error("课表接口返回格式不正确");
|
|
|
const courseMap = new Map();
|
|
|
kbxx.forEach(item => {
|
|
|
- const day = parseInt(item.xq, 10);
|
|
|
- const sections = String(item.jcdm2 || "").split(",")
|
|
|
- .map(s => parseInt(s.trim(), 10)).filter(s => !isNaN(s));
|
|
|
- const allWeeks = parseWeeks(item.zcs);
|
|
|
- if (!item.kcmc || sections.length === 0 || allWeeks.length === 0 || isNaN(day) || day < 1 || day > 7) return;
|
|
|
-
|
|
|
- const course = {
|
|
|
- name: item.kcmc.trim(),
|
|
|
- teacher: String(item.teaxms || "").trim() || "未知",
|
|
|
- position: resolvePosition(item.jxcdmcs),
|
|
|
- day,
|
|
|
- startSection: Math.min(...sections),
|
|
|
- endSection: Math.max(...sections),
|
|
|
- weeks: allWeeks
|
|
|
- };
|
|
|
+ addCourse(courseMap, item, parseWeeks(item.zcs));
|
|
|
+ });
|
|
|
+ return sortCourses(Array.from(courseMap.values()));
|
|
|
+}
|
|
|
|
|
|
- const key = [course.name, course.teacher, course.position, course.day,
|
|
|
- course.startSection, course.endSection].join("__");
|
|
|
- const existing = courseMap.get(key);
|
|
|
- if (existing) existing.weeks = [...new Set([...existing.weeks, ...course.weeks])].sort((a, b) => a - b);
|
|
|
- else courseMap.set(key, course);
|
|
|
+// 日历接口按周返回精确教室;同一课程换教室时,按周次拆成多个课程项。
|
|
|
+function parseWeeklyCourseList(rows) {
|
|
|
+ if (!Array.isArray(rows)) return [];
|
|
|
+ const courseMap = new Map();
|
|
|
+ rows.forEach(({ item, week }) => {
|
|
|
+ const actualWeek = parseInt(week, 10);
|
|
|
+ if (isNaN(actualWeek) || actualWeek < 1) return;
|
|
|
+ addCourse(courseMap, item, [actualWeek]);
|
|
|
});
|
|
|
- return Array.from(courseMap.values()).sort((a, b) =>
|
|
|
- a.day - b.day || a.startSection - b.startSection || a.endSection - b.endSection || a.name.localeCompare(b.name)
|
|
|
- );
|
|
|
+ return sortCourses(Array.from(courseMap.values()));
|
|
|
+}
|
|
|
+
|
|
|
+function maxWeekOf(kbxx) {
|
|
|
+ return kbxx.reduce((maxWeek, item) => {
|
|
|
+ const weeks = parseWeeks(item.zcs);
|
|
|
+ return Math.max(maxWeek, ...(weeks.length ? weeks : [0]));
|
|
|
+ }, 0);
|
|
|
}
|
|
|
|
|
|
// 读取课表页中的学期下拉框
|
|
|
@@ -86,11 +168,16 @@ function extractSemesterOptions(doc) {
|
|
|
|
|
|
// 导入前提示先登录教务系统
|
|
|
async function promptUserToStart() {
|
|
|
- return await window.shiguangBridgePromise.showAlert(
|
|
|
+ const confirmed = await window.shiguangBridgePromise.showAlert(
|
|
|
"济宁医学院教务导入",
|
|
|
"请先确保已登录教务系统,再继续导入。",
|
|
|
"我已登录"
|
|
|
);
|
|
|
+ if (!confirmed) {
|
|
|
+ window.shiguangBridge.showToast("用户取消了导入。");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
// 选择学期
|
|
|
@@ -100,10 +187,12 @@ async function selectSemester(semesterOptions) {
|
|
|
JSON.stringify(semesterOptions.semesters),
|
|
|
semesterOptions.defaultIndex
|
|
|
);
|
|
|
- if (selectedIndex === null || selectedIndex < 0) return null;
|
|
|
+ if (selectedIndex === null || selectedIndex === undefined) return null;
|
|
|
+ const index = Number(selectedIndex);
|
|
|
+ if (!Number.isInteger(index) || index < 0 || index >= semesterOptions.semesters.length) return null;
|
|
|
return {
|
|
|
- label: semesterOptions.semesters[selectedIndex],
|
|
|
- value: semesterOptions.semesterValues[selectedIndex]
|
|
|
+ label: semesterOptions.semesters[index],
|
|
|
+ value: semesterOptions.semesterValues[index]
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -133,11 +222,105 @@ async function fetchCourseData(xnxqdm) {
|
|
|
return kbxx;
|
|
|
}
|
|
|
|
|
|
+function normalizeDate(raw) {
|
|
|
+ const match = String(raw || "").match(/^(\d{4}-\d{2}-\d{2})/);
|
|
|
+ return match ? match[1] : null;
|
|
|
+}
|
|
|
+
|
|
|
+function shiftDate(dateText, dayOffset) {
|
|
|
+ const match = String(dateText || "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
|
+ if (!match) return null;
|
|
|
+ const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
|
+ date.setDate(date.getDate() + dayOffset);
|
|
|
+ return [
|
|
|
+ date.getFullYear(),
|
|
|
+ String(date.getMonth() + 1).padStart(2, "0"),
|
|
|
+ String(date.getDate()).padStart(2, "0")
|
|
|
+ ].join("-");
|
|
|
+}
|
|
|
+
|
|
|
+function extractWeekStartDate(payload) {
|
|
|
+ const dateRows = Array.isArray(payload) && Array.isArray(payload[1]) ? payload[1] : [];
|
|
|
+ const monday = dateRows.find(item => parseInt(item.xqmc, 10) === 1) || dateRows[0];
|
|
|
+ return normalizeDate(monday && monday.rq);
|
|
|
+}
|
|
|
+
|
|
|
+// 获取某一周的日历数据,jxcdmc 是该周实际使用的教室。
|
|
|
+async function fetchWeeklyCourseData(xnxqdm, week) {
|
|
|
+ const response = await fetch(
|
|
|
+ `/xsgrkbcx!getKbRq.action?xnxqdm=${encodeURIComponent(xnxqdm)}&zc=${encodeURIComponent(week)}`,
|
|
|
+ { method: "GET", credentials: "include" }
|
|
|
+ );
|
|
|
+ if (!response.ok) throw new Error(`第${week}周课表请求失败(HTTP ${response.status})`);
|
|
|
+ const payload = await response.json();
|
|
|
+ const rows = Array.isArray(payload) && Array.isArray(payload[0]) ? payload[0] : [];
|
|
|
+ return {
|
|
|
+ rows: rows.map(item => ({ item, week })),
|
|
|
+ weekStartDate: extractWeekStartDate(payload)
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchWeeklyCourses(xnxqdm, kbxx) {
|
|
|
+ const weeks = Array.from({ length: maxWeekOf(kbxx) }, (_, index) => index + 1);
|
|
|
+ if (weeks.length === 0) return { rows: [], successfulWeeks: [], semesterStartDate: null };
|
|
|
+
|
|
|
+ const results = await Promise.all(weeks.map(async week => {
|
|
|
+ try {
|
|
|
+ const data = await fetchWeeklyCourseData(xnxqdm, week);
|
|
|
+ return { week, ...data };
|
|
|
+ } catch (error) {
|
|
|
+ console.warn(`读取第${week}周课表失败`, error);
|
|
|
+ return { week, rows: null, weekStartDate: null };
|
|
|
+ }
|
|
|
+ }));
|
|
|
+
|
|
|
+ const datedWeek = results.find(result => result.weekStartDate);
|
|
|
+ const semesterStartDate = datedWeek
|
|
|
+ ? shiftDate(datedWeek.weekStartDate, -7 * (datedWeek.week - 1))
|
|
|
+ : null;
|
|
|
+ return {
|
|
|
+ rows: results.flatMap(result => result.rows || []),
|
|
|
+ successfulWeeks: results.filter(result => result.rows !== null).map(result => result.week),
|
|
|
+ semesterStartDate
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function mergeWeeklyAndFallbackCourses(kbxx, weeklyData) {
|
|
|
+ if (weeklyData.rows.length === 0) return parseCourseList(kbxx);
|
|
|
+
|
|
|
+ const courseMap = new Map();
|
|
|
+ parseWeeklyCourseList(weeklyData.rows).forEach(course => {
|
|
|
+ courseMap.set(courseKey(course), course);
|
|
|
+ });
|
|
|
+ const successfulWeeks = new Set(weeklyData.successfulWeeks);
|
|
|
+ if (successfulWeeks.size === weeklyData.successfulWeeks.length &&
|
|
|
+ successfulWeeks.size >= maxWeekOf(kbxx)) return sortCourses(Array.from(courseMap.values()));
|
|
|
+
|
|
|
+ // 仅为请求失败的周次使用整学期数据,避免覆盖周课表中的精确教室。
|
|
|
+ kbxx.forEach(item => {
|
|
|
+ const weeks = parseWeeks(item.zcs).filter(week => !successfulWeeks.has(week));
|
|
|
+ if (weeks.length > 0) addCourse(courseMap, item, weeks);
|
|
|
+ });
|
|
|
+ return sortCourses(Array.from(courseMap.values()));
|
|
|
+}
|
|
|
+
|
|
|
+async function saveCourseConfig(config) {
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
|
|
|
+}
|
|
|
+
|
|
|
+async function saveCourses(courses) {
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
+}
|
|
|
+
|
|
|
+async function savePresetTimeSlots() {
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(PRESET_TIME_SLOTS));
|
|
|
+}
|
|
|
+
|
|
|
// 主流程:提示 → 选学期 → 拉课表 → 保存课程与作息
|
|
|
async function runImportFlow() {
|
|
|
try {
|
|
|
- const confirmed = await promptUserToStart();
|
|
|
- if (!confirmed) { window.shiguangBridge.showToast("导入已取消"); return; }
|
|
|
+ const alertConfirmed = await promptUserToStart();
|
|
|
+ if (!alertConfirmed) return;
|
|
|
|
|
|
const pageHtml = await fetchSchedulePage();
|
|
|
const semesterOptions = extractSemesterOptions(new DOMParser().parseFromString(pageHtml, "text/html"));
|
|
|
@@ -147,7 +330,9 @@ async function runImportFlow() {
|
|
|
if (!semester) { window.shiguangBridge.showToast("导入已取消"); return; }
|
|
|
|
|
|
window.shiguangBridge.showToast(`正在获取 ${semester.label} 的课表...`);
|
|
|
- const courses = parseCourseList(await fetchCourseData(semester.value));
|
|
|
+ const allCourseData = await fetchCourseData(semester.value);
|
|
|
+ const weeklyData = await fetchWeeklyCourses(semester.value, allCourseData);
|
|
|
+ const courses = mergeWeeklyAndFallbackCourses(allCourseData, weeklyData);
|
|
|
|
|
|
if (courses.length === 0) {
|
|
|
await window.shiguangBridgePromise.showAlert(
|
|
|
@@ -158,14 +343,26 @@ async function runImportFlow() {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
+ const courseConfig = {
|
|
|
+ semesterTotalWeeks: Math.max(maxWeekOf(allCourseData), 20),
|
|
|
+ firstDayOfWeek: 1
|
|
|
+ };
|
|
|
+ if (weeklyData.semesterStartDate) {
|
|
|
+ courseConfig.semesterStartDate = weeklyData.semesterStartDate;
|
|
|
+ }
|
|
|
+
|
|
|
+ await saveCourseConfig(courseConfig);
|
|
|
+ await saveCourses(courses);
|
|
|
try {
|
|
|
- await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(PRESET_TIME_SLOTS));
|
|
|
+ await savePresetTimeSlots();
|
|
|
} catch (error) {
|
|
|
window.shiguangBridge.showToast(`课程已导入,作息时间导入失败:${error.message}`);
|
|
|
}
|
|
|
|
|
|
window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
|
|
|
+ if (!weeklyData.semesterStartDate) {
|
|
|
+ window.shiguangBridge.showToast("未获取到开学日期,请在应用内手动设置。");
|
|
|
+ }
|
|
|
window.shiguangBridge.notifyTaskCompletion();
|
|
|
} catch (error) {
|
|
|
await window.shiguangBridgePromise.showAlert(
|