|
@@ -1,4 +1,12 @@
|
|
|
-const SECTION_TIMES = [
|
|
|
|
|
|
|
+// 依赖接口:
|
|
|
|
|
+// POST /frame/droplist/getDropLists.action 学期列表(JSON)
|
|
|
|
|
+// GET /student/xkjg.wdkb.jsp?menucode=S20301 内部学号 xh(GBK HTML)
|
|
|
|
|
+// GET /student/wsxk.xskcb10319.jsp?params=base64 课表数据(GBK HTML)
|
|
|
|
|
+// POST /frame/desk/showLessonScheduleInfosV14.action 第一周周一日期
|
|
|
|
|
+
|
|
|
|
|
+// ===== 作息时间 =====
|
|
|
|
|
+// 夏令时(每年 5月1日–10月7日):作为基础时间段(骨架)提交
|
|
|
|
|
+const SUMMER_TIME_SLOTS = [
|
|
|
{ number: 1, startTime: "08:00", endTime: "08:45" },
|
|
{ number: 1, startTime: "08:00", endTime: "08:45" },
|
|
|
{ number: 2, startTime: "08:55", endTime: "09:40" },
|
|
{ number: 2, startTime: "08:55", endTime: "09:40" },
|
|
|
{ number: 3, startTime: "10:00", endTime: "10:45" },
|
|
{ number: 3, startTime: "10:00", endTime: "10:45" },
|
|
@@ -8,135 +16,138 @@ const SECTION_TIMES = [
|
|
|
{ number: 7, startTime: "16:30", endTime: "17:15" },
|
|
{ number: 7, startTime: "16:30", endTime: "17:15" },
|
|
|
{ number: 8, startTime: "17:25", endTime: "18:10" },
|
|
{ number: 8, startTime: "17:25", endTime: "18:10" },
|
|
|
{ number: 9, startTime: "19:00", endTime: "19:45" },
|
|
{ number: 9, startTime: "19:00", endTime: "19:45" },
|
|
|
- { number: 10, startTime: "19:55", endTime: "20:40" },
|
|
|
|
|
|
|
+ { number: 10, startTime: "19:55", endTime: "20:40" }
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
-function parseWeeks(weekStr, parity) {
|
|
|
|
|
- const weeks = [];
|
|
|
|
|
- weekStr.split(',').forEach(part => {
|
|
|
|
|
- if (part.includes('-')) {
|
|
|
|
|
- const [start, end] = part.split('-').map(Number);
|
|
|
|
|
- for (let i = start; i <= end; i++) weeks.push(i);
|
|
|
|
|
- } else {
|
|
|
|
|
- const w = parseInt(part);
|
|
|
|
|
- if (!isNaN(w)) weeks.push(w);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- if (parity === '单') return weeks.filter(w => w % 2 === 1);
|
|
|
|
|
- if (parity === '双') return weeks.filter(w => w % 2 === 0);
|
|
|
|
|
- return weeks;
|
|
|
|
|
|
|
+// 冬令时(每年 10月8日–次年4月30日)差量:仅下午 5–8 节整体提前 30 分钟,
|
|
|
|
|
+// 上午 1–4 节与晚间 9–10 节由骨架自动补齐
|
|
|
|
|
+const WINTER_DIFF_SLOTS = [
|
|
|
|
|
+ { number: 5, startTime: "14:00", endTime: "14:45" },
|
|
|
|
|
+ { number: 6, startTime: "14:55", endTime: "15:40" },
|
|
|
|
|
+ { number: 7, startTime: "16:00", endTime: "16:45" },
|
|
|
|
|
+ { number: 8, startTime: "16:55", endTime: "17:40" }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+// ===== 日期工具 =====
|
|
|
|
|
+function formatDate(date) {
|
|
|
|
|
+ const year = date.getFullYear();
|
|
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
+ const day = String(date.getDate()).padStart(2, "0");
|
|
|
|
|
+ return `${year}-${month}-${day}`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function findTable(win) {
|
|
|
|
|
- const t = Array.from(win.document.querySelectorAll('table'))
|
|
|
|
|
- .find(x => x.innerText.includes("星期一") && x.innerText.includes("["));
|
|
|
|
|
- if (t) return t;
|
|
|
|
|
- for (let i = 0; i < win.frames.length; i++) {
|
|
|
|
|
- try {
|
|
|
|
|
- const st = findTable(win.frames[i]);
|
|
|
|
|
- if (st) return st;
|
|
|
|
|
- } catch (e) {}
|
|
|
|
|
- }
|
|
|
|
|
- return null;
|
|
|
|
|
|
|
+// 夏令时:本年 5月1日–10月7日;冬令时:10月8日–次年4月30日(1–4月导入时自上一年起算)
|
|
|
|
|
+function buildComboDateRanges() {
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const year = now.getFullYear();
|
|
|
|
|
+ const month = now.getMonth() + 1;
|
|
|
|
|
+ const winterStartYear = month <= 4 ? year - 1 : year;
|
|
|
|
|
+ return {
|
|
|
|
|
+ summer: { startDate: `${year}-05-01`, endDate: `${year}-10-07` },
|
|
|
|
|
+ winter: { startDate: `${winterStartYear}-10-08`, endDate: `${winterStartYear + 1}-04-30` }
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 开学日期弹窗默认值的兜底:本周周一(周日归上一周)
|
|
|
|
|
+function getMondayOfCurrentWeek() {
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const offset = now.getDay() === 0 ? -6 : 1 - now.getDay();
|
|
|
|
|
+ const monday = new Date(now);
|
|
|
|
|
+ monday.setDate(now.getDate() + offset);
|
|
|
|
|
+ return formatDate(monday);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function fetchAndParseCourses() {
|
|
|
|
|
- const table = findTable(window);
|
|
|
|
|
- if (!table) {
|
|
|
|
|
- throw new Error("未检测到课表数据,请确保已切换到显示课表的页面!");
|
|
|
|
|
|
|
+// showPrompt 的全局验证函数:false=通过,字符串=错误文案
|
|
|
|
|
+function validateDateInput(input) {
|
|
|
|
|
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(input)) {
|
|
|
|
|
+ return "请输入 YYYY-MM-DD 格式的日期!";
|
|
|
|
|
+ }
|
|
|
|
|
+ const date = new Date(input + "T00:00:00");
|
|
|
|
|
+ if (isNaN(date.getTime()) || formatDate(date) !== input) {
|
|
|
|
|
+ return "该日期不存在,请检查!";
|
|
|
}
|
|
}
|
|
|
|
|
+ return false;
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
+// ===== 周次解析(支持 12-16 / 8 / 1-8单 / 9-17双 / 1,3,5)=====
|
|
|
|
|
+function parseWeeks(weekStr) {
|
|
|
|
|
+ const weeks = [];
|
|
|
|
|
+ weekStr.split(",").forEach(group => {
|
|
|
|
|
+ const g = group.trim();
|
|
|
|
|
+ const isSingle = g.includes("单");
|
|
|
|
|
+ const isDouble = g.includes("双");
|
|
|
|
|
+ const rangeMatch = g.match(/(\d+)\s*-\s*(\d+)/);
|
|
|
|
|
+ if (rangeMatch) {
|
|
|
|
|
+ for (let i = parseInt(rangeMatch[1]); i <= parseInt(rangeMatch[2]); i++) {
|
|
|
|
|
+ if (isSingle && i % 2 === 0) continue;
|
|
|
|
|
+ if (isDouble && i % 2 !== 0) continue;
|
|
|
|
|
+ weeks.push(i);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const num = parseInt(g.replace(/[^\d]/g, ""));
|
|
|
|
|
+ if (!isNaN(num)) weeks.push(num);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return Array.from(new Set(weeks)).sort((a, b) => a - b);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===== 课表 HTML 解析 =====
|
|
|
|
|
+// 输入:xskcb10319.jsp 返回的 GBK 解码后 HTML
|
|
|
|
|
+// 结构:每个星期列是 td.td;每门课一个 <div style='padding-bottom:5px...'>,
|
|
|
|
|
+// div 内按 <br> 分行依次为 课程名/教师/周次[起止节次]/地点
|
|
|
|
|
+function parseCourseTableHtml(htmlText) {
|
|
|
|
|
+ const doc = new DOMParser().parseFromString(htmlText, "text/html");
|
|
|
const rawItems = [];
|
|
const rawItems = [];
|
|
|
- Array.from(table.rows).forEach(row => {
|
|
|
|
|
- const cells = Array.from(row.cells);
|
|
|
|
|
- if (cells.length < 7) return;
|
|
|
|
|
-
|
|
|
|
|
- cells.forEach((cell, colIndex) => {
|
|
|
|
|
- const distanceToLast = cells.length - 1 - colIndex;
|
|
|
|
|
- if (distanceToLast > 6) return;
|
|
|
|
|
- const day = 7 - distanceToLast;
|
|
|
|
|
- const rawText = cell.innerText.trim();
|
|
|
|
|
- if (!rawText.includes('[')) return;
|
|
|
|
|
-
|
|
|
|
|
- // 过滤空行、清理隐藏的乱码字符
|
|
|
|
|
- const lines = rawText.split('\n').map(l => l.replace(/[\s\u200B-\u200D\uFEFF]/g, '').trim()).filter(l => l);
|
|
|
|
|
-
|
|
|
|
|
- // 找出所有时间的行号(锚点)
|
|
|
|
|
- const timeIndices = [];
|
|
|
|
|
- lines.forEach((line, index) => {
|
|
|
|
|
- if (/([\d\-,]+)\s*(?:单|双)?\s*\[(\d+)-(\d+)\]/.test(line)) {
|
|
|
|
|
- timeIndices.push(index);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
|
|
|
|
|
- timeIndices.forEach((currTimeIdx, k) => {
|
|
|
|
|
- const nextTimeIdx = (k + 1 < timeIndices.length) ? timeIndices[k + 1] : lines.length;
|
|
|
|
|
- const match = lines[currTimeIdx].match(/([\d\-,]+)\s*(单|双)?\s*\[(\d+)-(\d+)\]/);
|
|
|
|
|
-
|
|
|
|
|
- if (match) {
|
|
|
|
|
- // 1. 抓取课名和老师
|
|
|
|
|
- let name = "未知课程";
|
|
|
|
|
- let teacher = "未知教师";
|
|
|
|
|
- let nameTeacherLines = (k === 0) ? lines.slice(0, currTimeIdx) : lines.slice(timeIndices[k - 1] + 1, currTimeIdx);
|
|
|
|
|
-
|
|
|
|
|
- if (nameTeacherLines.length > 2) {
|
|
|
|
|
- nameTeacherLines = nameTeacherLines.slice(nameTeacherLines.length - 2);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (nameTeacherLines.length >= 2) {
|
|
|
|
|
- name = nameTeacherLines[0];
|
|
|
|
|
- teacher = nameTeacherLines[1];
|
|
|
|
|
- } else if (nameTeacherLines.length === 1) {
|
|
|
|
|
- name = nameTeacherLines[0];
|
|
|
|
|
- teacher = "";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 2. 抓取地点
|
|
|
|
|
- let position = "";
|
|
|
|
|
- const gap = nextTimeIdx - currTimeIdx - 1;
|
|
|
|
|
-
|
|
|
|
|
- if (k === timeIndices.length - 1) {
|
|
|
|
|
- if (gap > 0) position = lines.slice(currTimeIdx + 1).join(' ');
|
|
|
|
|
- } else {
|
|
|
|
|
- if (gap === 3) {
|
|
|
|
|
- position = lines[currTimeIdx + 1];
|
|
|
|
|
- } else if (gap > 3) {
|
|
|
|
|
- position = lines.slice(currTimeIdx + 1, nextTimeIdx - 2).join(' ');
|
|
|
|
|
- } else {
|
|
|
|
|
- position = "";
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- rawItems.push({
|
|
|
|
|
- name: name,
|
|
|
|
|
- teacher: teacher,
|
|
|
|
|
- position: position,
|
|
|
|
|
- day: day,
|
|
|
|
|
- startSection: parseInt(match[3]),
|
|
|
|
|
- endSection: parseInt(match[4]),
|
|
|
|
|
- weeks: parseWeeks(match[1], match[2])
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ doc.querySelectorAll("tr").forEach(row => {
|
|
|
|
|
+ const cells = row.querySelectorAll("td.td");
|
|
|
|
|
+ cells.forEach((cell, dayIndex) => {
|
|
|
|
|
+ const day = dayIndex + 1; // 一行内 td.td 依次对应 星期一~星期日
|
|
|
|
|
+ const divs = cell.querySelectorAll("div[style*='padding-bottom:5px']");
|
|
|
|
|
+ divs.forEach(div => {
|
|
|
|
|
+ const lines = Array.from(div.childNodes)
|
|
|
|
|
+ .map(n => n.textContent.trim())
|
|
|
|
|
+ .filter(t => t.length > 0);
|
|
|
|
|
+ if (lines.length < 3) return;
|
|
|
|
|
+ const timeMatch = lines[2].match(/^(.+?)\s*\[(\d+)-(\d+)\]\s*$/);
|
|
|
|
|
+ if (!timeMatch) return;
|
|
|
|
|
+ rawItems.push({
|
|
|
|
|
+ name: lines[0],
|
|
|
|
|
+ teacher: lines[1] || "",
|
|
|
|
|
+ position: lines[3] || "未知地点",
|
|
|
|
|
+ day: day,
|
|
|
|
|
+ startSection: parseInt(timeMatch[2]),
|
|
|
|
|
+ endSection: parseInt(timeMatch[3]),
|
|
|
|
|
+ weeks: parseWeeks(timeMatch[1])
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // 完全重复项去重(青果网格可能对跨节次课程重复渲染)
|
|
|
|
|
+ const seen = new Set();
|
|
|
|
|
+ const uniqueItems = rawItems.filter(item => {
|
|
|
|
|
+ const key = `${item.name}|${item.teacher}|${item.position}|${item.day}|${item.startSection}|${item.endSection}|${item.weeks.join(",")}`;
|
|
|
|
|
+ if (seen.has(key)) return false;
|
|
|
|
|
+ seen.add(key);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 合并去重:同(名|师|地|星期)按周次展开为节次矩阵,再按连续节次聚合
|
|
|
const groupMap = new Map();
|
|
const groupMap = new Map();
|
|
|
- rawItems.forEach(item => {
|
|
|
|
|
|
|
+ uniqueItems.forEach(item => {
|
|
|
const key = `${item.name}|${item.teacher}|${item.position}|${item.day}`;
|
|
const key = `${item.name}|${item.teacher}|${item.position}|${item.day}`;
|
|
|
if (!groupMap.has(key)) groupMap.set(key, {});
|
|
if (!groupMap.has(key)) groupMap.set(key, {});
|
|
|
const weekMap = groupMap.get(key);
|
|
const weekMap = groupMap.get(key);
|
|
|
item.weeks.forEach(w => {
|
|
item.weeks.forEach(w => {
|
|
|
if (!weekMap[w]) weekMap[w] = new Set();
|
|
if (!weekMap[w]) weekMap[w] = new Set();
|
|
|
- for (let s = item.startSection; s <= item.endSection; s++) {
|
|
|
|
|
- weekMap[w].add(s);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ for (let s = item.startSection; s <= item.endSection; s++) weekMap[w].add(s);
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const finalCourses = [];
|
|
const finalCourses = [];
|
|
|
groupMap.forEach((weekMap, key) => {
|
|
groupMap.forEach((weekMap, key) => {
|
|
|
- const [name, teacher, position, day] = key.split('|');
|
|
|
|
|
|
|
+ const [name, teacher, position, day] = key.split("|");
|
|
|
const patternMap = new Map();
|
|
const patternMap = new Map();
|
|
|
Object.keys(weekMap).forEach(w => {
|
|
Object.keys(weekMap).forEach(w => {
|
|
|
const week = parseInt(w);
|
|
const week = parseInt(w);
|
|
@@ -144,49 +155,243 @@ async function fetchAndParseCourses() {
|
|
|
if (sections.length === 0) return;
|
|
if (sections.length === 0) return;
|
|
|
let start = sections[0];
|
|
let start = sections[0];
|
|
|
for (let i = 0; i < sections.length; i++) {
|
|
for (let i = 0; i < sections.length; i++) {
|
|
|
- if (i === sections.length - 1 || sections[i+1] !== sections[i] + 1) {
|
|
|
|
|
|
|
+ if (i === sections.length - 1 || sections[i + 1] !== sections[i] + 1) {
|
|
|
const pKey = `${start}-${sections[i]}`;
|
|
const pKey = `${start}-${sections[i]}`;
|
|
|
- if (!patternMap.has(pKey)) patternMap.set(pKey, []);
|
|
|
|
|
- patternMap.get(pKey).push(week);
|
|
|
|
|
- if (i < sections.length - 1) start = sections[i+1];
|
|
|
|
|
|
|
+ if (!patternMap.has(pKey)) patternMap.set(pKey, new Set());
|
|
|
|
|
+ patternMap.get(pKey).add(week);
|
|
|
|
|
+ if (i < sections.length - 1) start = sections[i + 1];
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
- patternMap.forEach((weeks, pKey) => {
|
|
|
|
|
- const [sStart, sEnd] = pKey.split('-').map(Number);
|
|
|
|
|
|
|
+ patternMap.forEach((weekSet, pKey) => {
|
|
|
|
|
+ const [sStart, sEnd] = pKey.split("-").map(Number);
|
|
|
finalCourses.push({
|
|
finalCourses.push({
|
|
|
name, teacher, position,
|
|
name, teacher, position,
|
|
|
day: parseInt(day),
|
|
day: parseInt(day),
|
|
|
startSection: sStart,
|
|
startSection: sStart,
|
|
|
endSection: sEnd,
|
|
endSection: sEnd,
|
|
|
- weeks: weeks.sort((a, b) => a - b)
|
|
|
|
|
|
|
+ weeks: Array.from(weekSet).sort((a, b) => a - b)
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
return finalCourses;
|
|
return finalCourses;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ===== 接口封装 =====
|
|
|
|
|
+const JWXT_BASE = window.location.origin;
|
|
|
|
|
+
|
|
|
|
|
+// 学期列表 → 用户选择 → { xn, xq, name }
|
|
|
|
|
+async function selectSemester() {
|
|
|
|
|
+ window.shiguangBridge.showToast("正在获取学期列表...");
|
|
|
|
|
+ const resp = await fetch(JWXT_BASE + "/frame/droplist/getDropLists.action", {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
|
|
|
|
|
+ body: "comboBoxName=StMsXnxqDxDesc¶mValue=&isYXB=0&isCDDW=0&isXQ=0&isDJKSLB=0&isZY=0",
|
|
|
|
|
+ credentials: "include"
|
|
|
|
|
+ });
|
|
|
|
|
+ const list = await resp.json();
|
|
|
|
|
+ if (!Array.isArray(list) || list.length === 0) throw new Error("学期列表为空");
|
|
|
|
|
+ const names = list.map(item => item.name);
|
|
|
|
|
+ const selectedIndex = await window.shiguangBridgePromise.showSingleSelection("选择导入学期", JSON.stringify(names), 0);
|
|
|
|
|
+ if (selectedIndex === null) return null;
|
|
|
|
|
+ const [xn, xq] = list[selectedIndex].code.split("-");
|
|
|
|
|
+ return { xn, xq, name: list[selectedIndex].name };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 内部学号(≠登录账号):wdkb.jsp 的隐藏域,实时 DOM 兜底
|
|
|
|
|
+async function getInternalStudentId() {
|
|
|
|
|
+ const resp = await fetch(JWXT_BASE + "/student/xkjg.wdkb.jsp?menucode=S20301", { credentials: "include" });
|
|
|
|
|
+ const html = new TextDecoder("gbk").decode(await resp.arrayBuffer());
|
|
|
|
|
+ const tagMatch = html.match(/<input[^>]*id=["']xh["'][^>]*>/i);
|
|
|
|
|
+ if (tagMatch) {
|
|
|
|
|
+ const valueMatch = tagMatch[0].match(/value=["']([^"']+)["']/i);
|
|
|
|
|
+ if (valueMatch) return valueMatch[1];
|
|
|
|
|
+ }
|
|
|
|
|
+ let fromDom = null;
|
|
|
|
|
+ (function walk(win) {
|
|
|
|
|
+ if (fromDom) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const el = win.document.getElementById("xh");
|
|
|
|
|
+ if (el && el.value) { fromDom = el.value; return; }
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ for (let i = 0; i < win.frames.length; i++) { try { walk(win.frames[i]); } catch (e) {} }
|
|
|
|
|
+ })(window);
|
|
|
|
|
+ return fromDom;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 课表 HTML(GBK)
|
|
|
|
|
+async function fetchCourseHtml(xn, xq, xh) {
|
|
|
|
|
+ const params = btoa(`xn=${xn}&xq=${xq}&xh=${xh}`);
|
|
|
|
|
+ const resp = await fetch(`${JWXT_BASE}/student/wsxk.xskcb10319.jsp?params=${params}`, { credentials: "include" });
|
|
|
|
|
+ return new TextDecoder("gbk").decode(await resp.arrayBuffer());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 第一周周一日期(教学日历接口,jxz=1 即第 1 周,周一列头日期即开学日期)
|
|
|
|
|
+// 注意:泰山该接口返回 UTF-8(课表接口才是 GBK);失败返回 null,由调用方兜底
|
|
|
|
|
+async function fetchSemesterStartDate(xn, xq) {
|
|
|
|
|
+ const resp = await fetch(`${JWXT_BASE}/frame/desk/showLessonScheduleInfosV14.action?xn=${xn}&xq=${xq}&jxz=1`, {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ headers: { "x-requested-with": "XMLHttpRequest" },
|
|
|
|
|
+ credentials: "include"
|
|
|
|
|
+ });
|
|
|
|
|
+ const html = await resp.text();
|
|
|
|
|
+ const match = html.match(/<br\s*\/?>\s*(\d{2})-(\d{2})/);
|
|
|
|
|
+ if (!match) return null;
|
|
|
|
|
+ const year = xq === "1" ? String(parseInt(xn) + 1) : String(xn);
|
|
|
|
|
+ return `${year}-${match[1]}-${match[2]}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===== 导入步骤 =====
|
|
|
|
|
+async function saveCourseConfig(startDate) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
|
|
|
|
|
+ semesterStartDate: startDate
|
|
|
|
|
+ }));
|
|
|
|
|
+ window.shiguangBridge.showToast("开学日期已保存");
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast("保存开学日期失败: " + error.message);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function importTimeSlots() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(SUMMER_TIME_SLOTS));
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入时间段失败: " + error.message);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function importComboSchedule() {
|
|
|
|
|
+ const ranges = buildComboDateRanges();
|
|
|
|
|
+ const comboSchedule = {
|
|
|
|
|
+ name: "泰山学院夏冬作息方案",
|
|
|
|
|
+ publicSchedules: [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: "夏令时",
|
|
|
|
|
+ startDate: ranges.summer.startDate,
|
|
|
|
|
+ endDate: ranges.summer.endDate,
|
|
|
|
|
+ timeSlots: [] // 与骨架一致,全部由骨架补齐
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: "冬令时",
|
|
|
|
|
+ startDate: ranges.winter.startDate,
|
|
|
|
|
+ endDate: ranges.winter.endDate,
|
|
|
|
|
+ timeSlots: WINTER_DIFF_SLOTS // 仅提交下午 5–8 节差异
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.saveComboSchedule(JSON.stringify(comboSchedule));
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast("组合作息导入失败: " + error.message);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===== 流程编排 =====
|
|
|
|
|
+// notifyTaskCompletion() 只在成功后调用;任何取消/失败立即终止
|
|
|
async function runImportFlow() {
|
|
async function runImportFlow() {
|
|
|
|
|
+ const confirmed = await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ "青果教务导入",
|
|
|
|
|
+ "将从教务接口直接获取课表数据,请确保已在当前浏览器登录教务系统。",
|
|
|
|
|
+ "开始导入"
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!confirmed) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入已取消。");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 学期列表 + 用户选择(列表拉取失败大概率是未登录)
|
|
|
|
|
+ let semester;
|
|
|
try {
|
|
try {
|
|
|
- window.shiguangBridge.showToast("泰山学院引擎启动,抓取数据中...");
|
|
|
|
|
- const courses = await fetchAndParseCourses();
|
|
|
|
|
- if (!courses || courses.length === 0) {
|
|
|
|
|
- window.shiguangBridge.showToast("解析完成,但当前课表为空");
|
|
|
|
|
- window.shiguangBridge.notifyTaskCompletion();
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ semester = await selectSemester();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast("获取学期列表失败,请确认已登录教务系统");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (semester === null) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入已取消。");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const saveResult = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
|
|
- if (saveResult !== true) return;
|
|
|
|
|
|
|
+ // 2. 内部学号
|
|
|
|
|
+ let studentId;
|
|
|
|
|
+ try {
|
|
|
|
|
+ studentId = await getInternalStudentId();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ studentId = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!studentId) {
|
|
|
|
|
+ window.shiguangBridge.showToast("获取学号失败,请稍后重试或向维护者反馈");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(SECTION_TIMES));
|
|
|
|
|
|
|
+ // 3. 拉取并解析课表
|
|
|
|
|
+ window.shiguangBridge.showToast(`正在抓取「${semester.name}」课表数据...`);
|
|
|
|
|
+ let courses;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const html = await fetchCourseHtml(semester.xn, semester.xq, studentId);
|
|
|
|
|
+ courses = parseCourseTableHtml(html);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入失败:" + error.message);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!courses || courses.length === 0) {
|
|
|
|
|
+ window.shiguangBridge.showToast("未解析到课程数据,请确认所选学期有课程安排");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- window.shiguangBridge.showToast(`导入大成功!合并生成 ${courses.length} 个课块`);
|
|
|
|
|
- window.shiguangBridge.notifyTaskCompletion();
|
|
|
|
|
|
|
+ // 4. 开学日期:接口自动获取 → 用户确认(取消则终止)
|
|
|
|
|
+ let apiStartDate = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ apiStartDate = await fetchSemesterStartDate(semester.xn, semester.xq);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- window.shiguangBridge.showToast("⚠️ " + error.message);
|
|
|
|
|
- window.shiguangBridge.notifyTaskCompletion();
|
|
|
|
|
|
|
+ apiStartDate = null;
|
|
|
}
|
|
}
|
|
|
|
|
+ const startDate = await window.shiguangBridgePromise.showPrompt(
|
|
|
|
|
+ "确认开学日期",
|
|
|
|
|
+ "请确认本学期第一周周一的日期(用于校准周数)",
|
|
|
|
|
+ apiStartDate || getMondayOfCurrentWeek(),
|
|
|
|
|
+ "validateDateInput"
|
|
|
|
|
+ );
|
|
|
|
|
+ if (startDate === null) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入已取消。");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 保存开学日期
|
|
|
|
|
+ const configSaved = await saveCourseConfig(startDate);
|
|
|
|
|
+ if (!configSaved) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 保存课程
|
|
|
|
|
+ try {
|
|
|
|
|
+ const saveResult = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
|
|
+ if (saveResult !== true) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入失败:课程数据未能保存");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入失败:" + error.message);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 时间段(骨架)→ 组合作息
|
|
|
|
|
+ const timeSlotSaved = await importTimeSlots();
|
|
|
|
|
+ if (timeSlotSaved) {
|
|
|
|
|
+ await importComboSchedule();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ window.shiguangBridge.showToast("时间段导入失败,跳过组合作息。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 8. 全部成功
|
|
|
|
|
+ window.shiguangBridge.showToast(`导入成功,共 ${courses.length} 门课程,夏/冬令时将按日期自动切换`);
|
|
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
runImportFlow();
|
|
runImportFlow();
|