// 依赖接口:
// 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: 2, startTime: "08:55", endTime: "09:40" },
{ number: 3, startTime: "10:00", endTime: "10:45" },
{ number: 4, startTime: "10:55", endTime: "11:40" },
{ number: 5, startTime: "14:30", endTime: "15:15" },
{ number: 6, startTime: "15:25", endTime: "16:10" },
{ number: 7, startTime: "16:30", endTime: "17:15" },
{ number: 8, startTime: "17:25", endTime: "18:10" },
{ number: 9, startTime: "19:00", endTime: "19:45" },
{ number: 10, startTime: "19:55", endTime: "20:40" }
];
// 冬令时(每年 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}`;
}
// 夏令时:本年 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);
}
// 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 内按
分行依次为 课程名/教师/周次[起止节次]/地点
function parseCourseTableHtml(htmlText) {
const doc = new DOMParser().parseFromString(htmlText, "text/html");
const rawItems = [];
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();
uniqueItems.forEach(item => {
const key = `${item.name}|${item.teacher}|${item.position}|${item.day}`;
if (!groupMap.has(key)) groupMap.set(key, {});
const weekMap = groupMap.get(key);
item.weeks.forEach(w => {
if (!weekMap[w]) weekMap[w] = new Set();
for (let s = item.startSection; s <= item.endSection; s++) weekMap[w].add(s);
});
});
const finalCourses = [];
groupMap.forEach((weekMap, key) => {
const [name, teacher, position, day] = key.split("|");
const patternMap = new Map();
Object.keys(weekMap).forEach(w => {
const week = parseInt(w);
const sections = Array.from(weekMap[week]).sort((a, b) => a - b);
if (sections.length === 0) return;
let start = sections[0];
for (let i = 0; i < sections.length; i++) {
if (i === sections.length - 1 || sections[i + 1] !== sections[i] + 1) {
const pKey = `${start}-${sections[i]}`;
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((weekSet, pKey) => {
const [sStart, sEnd] = pKey.split("-").map(Number);
finalCourses.push({
name, teacher, position,
day: parseInt(day),
startSection: sStart,
endSection: sEnd,
weeks: Array.from(weekSet).sort((a, b) => a - b)
});
});
});
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(/]*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(/
\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() {
const confirmed = await window.shiguangBridgePromise.showAlert(
"青果教务导入",
"将从教务接口直接获取课表数据,请确保已在当前浏览器登录教务系统。",
"开始导入"
);
if (!confirmed) {
window.shiguangBridge.showToast("导入已取消。");
return;
}
// 1. 学期列表 + 用户选择(列表拉取失败大概率是未登录)
let semester;
try {
semester = await selectSemester();
} catch (error) {
window.shiguangBridge.showToast("获取学期列表失败,请确认已登录教务系统");
return;
}
if (semester === null) {
window.shiguangBridge.showToast("导入已取消。");
return;
}
// 2. 内部学号
let studentId;
try {
studentId = await getInternalStudentId();
} catch (error) {
studentId = null;
}
if (!studentId) {
window.shiguangBridge.showToast("获取学号失败,请稍后重试或向维护者反馈");
return;
}
// 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;
}
// 4. 开学日期:接口自动获取 → 用户确认(取消则终止)
let apiStartDate = null;
try {
apiStartDate = await fetchSemesterStartDate(semester.xn, semester.xq);
} catch (error) {
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();