// 郑州汽车工程职业学院 (zzvcae.edu.cn) 拾光课程表适配脚本
// 平台:树维 EAMS 教务(与郑州航空工业管理学院 jwglxt.zua.edu.cn 同平台)
// 参考:郑州航院 zua.js(树维 EAMS Fetch 解析流程),针对本校 CAS 单点登录与作息做适配
//
// 使用说明:
// 1. 在 App 内本适配的入口地址会打开 CAS 统一身份认证登录页
// 2. 登录 CAS 成功后,进入【服务大厅】→【学生课表查询】,等待课表页面加载完成
// 3. 回到 App 点击【执行导入】即可
// 注意:教务系统禁止从 jw.zzvcae.edu.cn 直接登录,必须经由 CAS 登录并在服务大厅访问过一次课表,
// 这样 jw.zzvcae.edu.cn 域下才会建立有效的教务会话,脚本的接口请求才能成功。
const BASE_URL = "https://jw.zzvcae.edu.cn";
const MAX_SUPPORTED_WEEK = 60;
// 本校作息兜底(学校未发布公开作息表时使用;正常情况下脚本会优先从课表页动态解析作息)
// 这里暂按常见高职作息填写占位,实测后以教务系统课表页解析出的作息为准
const ZZQCC_TIME_SLOTS_FALLBACK = [
{ 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:30", endTime: "20:15" },
{ number: 10, startTime: "20:25", endTime: "21:10" }
];
function powerSplit(paramsRaw) {
const args = [];
let current = "";
let depth = 0;
let inQuote = false;
let quoteChar = "";
for (let i = 0; i < paramsRaw.length; i++) {
const char = paramsRaw[i];
if ((char === '"' || char === "'") && (i === 0 || paramsRaw[i - 1] !== "\\")) {
if (!inQuote) {
inQuote = true;
quoteChar = char;
} else if (char === quoteChar) {
inQuote = false;
}
}
if (!inQuote) {
if (char === "(" || char === "[" || char === "{") depth++;
if (char === ")" || char === "]" || char === "}") depth--;
}
if (char === "," && depth === 0 && !inQuote) {
args.push(cleanArg(current));
current = "";
} else {
current += char;
}
}
args.push(cleanArg(current));
return args;
}
function cleanArg(value) {
const trimmed = value.trim();
if (trimmed === "null") return null;
return trimmed.replace(/^["']|["']$/g, "");
}
function cleanCourseName(name) {
return String(name || "未知课程")
.replace(/<[^>]*>/g, "") // 去掉教务页嵌入的 HTML 标签(如 颜色标记)
.replace(/ | /gi, " ")
.replace(/\s+/g, " ")
.replace(/\([^()]*\)\s*$/, "") // 去除尾部纯括号说明(如"(二)")
.trim();
}
function cleanPosition(position) {
return String(position || "未知地点")
.replace(/<[^>]*>/g, "")
.replace(/ | /gi, " ")
.replace(/\s+/g, " ")
.trim();
}
function parseWeeksBitmap(bitmap) {
const weeks = [];
const value = String(bitmap || "");
// 树维 EAMS 位图的下标就是周次,下标 0 是占位符;拾光使用 1 基周次。
for (let week = 1; week < value.length && week <= MAX_SUPPORTED_WEEK; week++) {
if (value[week] === "1") weeks.push(week);
}
return weeks;
}
/**
* 按周次矩阵合并算法:只有课程名、教师、地点和星期均相同
* 且在同一周内节次连续时才合并。
*/
function mergeContinuousLessons(lessons) {
if (!lessons || lessons.length === 0) return [];
const groups = {};
lessons.forEach(lesson => {
const key = `${lesson.name}|${lesson.teacher}|${lesson.position}|${lesson.day}`;
if (!groups[key]) {
groups[key] = {
name: lesson.name,
teacher: lesson.teacher,
position: lesson.position,
day: lesson.day,
weeksMatrix: Array.from({ length: MAX_SUPPORTED_WEEK + 1 }, () => new Set())
};
}
if (Array.isArray(lesson.weeks)) {
lesson.weeks.forEach(week => {
if (Number.isInteger(week) && week > 0 && week <= MAX_SUPPORTED_WEEK) {
for (let section = lesson.startSection; section <= lesson.endSection; section++) {
groups[key].weeksMatrix[week].add(section);
}
}
});
}
});
const merged = [];
for (const key in groups) {
const group = groups[key];
const blockMap = {};
for (let week = 1; week < group.weeksMatrix.length; week++) {
const sections = Array.from(group.weeksMatrix[week]).sort((a, b) => a - b);
if (sections.length === 0) continue;
let start = sections[0];
let previous = sections[0];
for (let i = 1; i < sections.length; i++) {
const current = sections[i];
if (current === previous + 1) {
previous = current;
} else {
const blockKey = `${start}-${previous}`;
if (!blockMap[blockKey]) blockMap[blockKey] = [];
blockMap[blockKey].push(week);
start = current;
previous = current;
}
}
const blockKey = `${start}-${previous}`;
if (!blockMap[blockKey]) blockMap[blockKey] = [];
blockMap[blockKey].push(week);
}
for (const blockKey in blockMap) {
const [startSection, endSection] = blockKey.split("-").map(Number);
merged.push({
name: group.name,
teacher: group.teacher,
position: group.position,
day: group.day,
startSection,
endSection,
weeks: blockMap[blockKey]
});
}
}
merged.sort((a, b) => {
if (a.day !== b.day) return a.day - b.day;
if (a.startSection !== b.startSection) return a.startSection - b.startSection;
if (a.name !== b.name) return a.name.localeCompare(b.name);
return a.position.localeCompare(b.position);
});
return merged;
}
function parseTeacherName(block) {
const teachersMatch = block.match(/actTeachers\s*=\s*\[([\s\S]*?)\]\s*;/);
if (!teachersMatch) return "未知教师";
const names = [];
const nameRegex = /\bname\s*:\s*"([^"]+)"/g;
let match;
while ((match = nameRegex.exec(teachersMatch[1])) !== null) {
if (!names.includes(match[1])) names.push(match[1]);
}
return names.length > 0 ? names.join(",") : "未知教师";
}
function parseTaskActivities(html) {
const rawResults = [];
const unitCountMatch = html.match(/\bunitCount\s*=\s*(\d+)\s*;/);
const unitCount = unitCountMatch ? parseInt(unitCountMatch[1], 10) : 14;
const indexRegex = new RegExp(
`index\\s*=\\s*(\\d+)\\s*\\*\\s*(?:unitCount|${unitCount})\\s*\\+\\s*(\\d+)\\s*;`,
"g"
);
const blocks = html.split(/var\s+teachers\s*=/);
for (let i = 1; i < blocks.length; i++) {
const block = blocks[i];
const teacher = parseTeacherName(block);
const activityRegex = /new\s+TaskActivity\(([\s\S]*?)\)\s*;/g;
const activities = [];
let activityMatch;
while ((activityMatch = activityRegex.exec(block)) !== null) {
activities.push({
argsRaw: activityMatch[1],
start: activityMatch.index,
end: activityRegex.lastIndex
});
}
for (let activityIndex = 0; activityIndex < activities.length; activityIndex++) {
const activity = activities[activityIndex];
const args = powerSplit(activity.argsRaw);
if (args.length < 7) continue;
const name = cleanCourseName(args[3]);
const position = cleanPosition(args[5]);
const weeks = parseWeeksBitmap(args[6]);
if (weeks.length === 0) continue;
const nextActivityStart = activityIndex + 1 < activities.length
? activities[activityIndex + 1].start
: block.length;
const activityScope = block.slice(activity.end, nextActivityStart);
indexRegex.lastIndex = 0;
let indexMatch;
while ((indexMatch = indexRegex.exec(activityScope)) !== null) {
const rawDay = parseInt(indexMatch[1], 10);
const rawSection = parseInt(indexMatch[2], 10);
if (rawDay < 0 || rawDay > 6 || rawSection < 0 || rawSection >= unitCount) continue;
const day = rawDay + 1;
const section = rawSection + 1;
rawResults.push({
name,
teacher,
position,
day,
startSection: section,
endSection: section,
weeks: [...weeks]
});
}
}
}
return mergeContinuousLessons(rawResults);
}
function parseParameters(html) {
// 多种可能的 ids 表达方式
const idsPatterns = [
/bg\.form\.addInput\(\s*form\s*,\s*["']ids["']\s*,\s*["'](\d+)["']\s*\)/,
/["']ids["']\s*,\s*["'](\d+)["']/,
/name=["']ids["'][^>]*value=["'](\d+)["']/i,
/value=["'](\d+)["'][^>]*name=["']ids["']/i,
/ids\s*=\s*["'](\d+)["']/
];
let ids = null;
for (const pattern of idsPatterns) {
const match = html.match(pattern);
if (match && match[1]) {
ids = match[1];
break;
}
}
// 学期标签 id 匹配:优先 semesterBar\d+Semester,其次任意含 semester 的 id
let tagIdMatch = html.match(/id=["'](semesterBar\d+Semester)["']/);
if (!tagIdMatch) {
tagIdMatch = html.match(/id=["']([^"']*[Ss]emester[^"']*)["']/);
}
if (!ids || !tagIdMatch) return null;
const tagId = tagIdMatch[1];
const escapedTagId = tagId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const elementMatch = html.match(new RegExp(`<[^>]*\\bid=["']${escapedTagId}["'][^>]*>`, "i"));
const valueMatch = elementMatch ? elementMatch[0].match(/\bvalue=["'](\d+)["']/i) : null;
return {
ids,
tagId,
currentSemesterId: valueMatch ? valueMatch[1] : null
};
}
/**
* 从课表页 HTML 中动态解析作息时间(树维 EAMS 的节次表头为 th[id="0_节次"],文本形如 "(08:00-08:45)")
*/
function parseTimeSlotsFromHtml(html) {
const timeSlots = [];
const thRegex = /
]*id=["']0_(\d+)["'][^>]*>([\s\S]*?)<\/th>/gi;
let match;
while ((match = thRegex.exec(html)) !== null) {
const number = parseInt(match[1], 10);
const text = match[2].replace(/<[^>]*>/g, " ").replace(/ | /gi, " ").replace(/\u00a0/g, " ");
const timeMatch = text.match(/(\d{1,2}:\d{2})\s*[-~至]\s*(\d{1,2}:\d{2})/);
if (timeMatch && !isNaN(number)) {
timeSlots.push({
number,
startTime: timeMatch[1].padStart(5, "0"),
endTime: timeMatch[2].padStart(5, "0")
});
}
}
timeSlots.sort((a, b) => a.number - b.number);
// 校验编号从 1 开始连续
for (let i = 0; i < timeSlots.length; i++) {
if (timeSlots[i].number !== i + 1) return [];
}
return timeSlots;
}
/**
* 从学期对象中提取日期字段(兼容多种字段名与格式)
*/
function extractDateFromObj(obj, keys) {
for (const k of keys) {
const v = obj ? obj[k] : null;
if (v !== null && v !== undefined && v !== "") {
const m = String(v).match(/(\d{4})[-/年](\d{1,2})[-/月](\d{1,2})日?/);
if (m) {
const d = normalizeNumericDate(m[1], m[2], m[3]);
if (d) return d;
}
}
}
// 兜底:扫描对象所有字段值里是否含日期
for (const k in obj || {}) {
const v = obj[k];
if (typeof v === "string") {
const m = v.match(/(\d{4})[-/年](\d{1,2})[-/月](\d{1,2})日?/);
if (m) {
const d = normalizeNumericDate(m[1], m[2], m[3]);
if (d) return d;
}
}
}
return null;
}
function parseSemesterResponse(raw) {
const data = Function(`return (${raw});`)();
const semesters = [];
for (const key of Object.keys(data.semesters || {})) {
const entries = Array.isArray(data.semesters[key]) ? data.semesters[key] : [];
entries.forEach(semester => {
if (semester && semester.id !== undefined) {
const term = String(semester.name || "").trim();
const label = /^第.*学期$/.test(term) ? term : `第${term}学期`;
semesters.push({
id: String(semester.id),
schoolYear: String(semester.schoolYear || ""),
term,
name: `${semester.schoolYear} ${label}`.trim(),
startDate: extractDateFromObj(semester, ["startDate", "beginDate", "start", "startTime", "dateBegin"]),
endDate: extractDateFromObj(semester, ["endDate", "end", "endTime", "finishDate", "dateEnd"])
});
}
});
}
semesters.sort((a, b) => {
const yearCompare = b.schoolYear.localeCompare(a.schoolYear);
if (yearCompare !== 0) return yearCompare;
return b.term.localeCompare(a.term, undefined, { numeric: true });
});
return {
semesters,
currentSemesterId: data.semesterId === undefined ? null : String(data.semesterId)
};
}
function normalizeNumericDate(year, month, day) {
const yearNumber = Number(year);
const monthNumber = Number(month);
const dayNumber = Number(day);
const date = new Date(Date.UTC(yearNumber, monthNumber - 1, dayNumber));
if (
date.getUTCFullYear() !== yearNumber ||
date.getUTCMonth() + 1 !== monthNumber ||
date.getUTCDate() !== dayNumber
) {
return null;
}
return [
String(yearNumber).padStart(4, "0"),
String(monthNumber).padStart(2, "0"),
String(dayNumber).padStart(2, "0")
].join("-");
}
function parseCalendarInfo(html) {
const text = String(html || "")
.replace(/<[^>]*>/g, " ")
.replace(/ | |*A0;/gi, " ")
.replace(/\u00a0/g, " ")
.replace(/\s+/g, " ");
// 兼容多种表达:开始/结束日期、开始~结束、起止、YYYY-M-D / YYYY年M月D日 等
const patterns = [
/开始\s*\/\s*结束日期\s*[::]?\s*(\d{4})[-/年]\s*(\d{1,2})[-/月]\s*(\d{1,2})日?\s*[~—~至到]\s*(\d{4})[-/年]\s*(\d{1,2})[-/月]\s*(\d{1,2})日?\s*\(?\s*(\d+)\s*周?\s*\)?/i,
/开始\s*[::]?\s*(\d{4})[-/年]\s*(\d{1,2})[-/月]\s*(\d{1,2})日?\s*[~—~至到结束]?\s*(\d{4})?[-/年]?\s*(\d{1,2})?[-/月]?\s*(\d{1,2})?日?\s*[^0-9]{0,10}(\d+)\s*周?/i,
/(\d{4})[-/年]\s*(\d{1,2})[-/月]\s*(\d{1,2})日?\s*[~—~至到]\s*(\d{4})[-/年]\s*(\d{1,2})[-/月]\s*(\d{1,2})日?/
];
let match = null;
for (const pattern of patterns) {
match = text.match(pattern);
if (match) break;
}
if (!match) return null;
// 计算总周数:优先取显式数字,否则按起止日期估算
let semesterTotalWeeks = null;
const explicitWeeks = match[7] ? Number(match[7]) : NaN;
if (Number.isInteger(explicitWeeks) && explicitWeeks >= 1 && explicitWeeks <= MAX_SUPPORTED_WEEK) {
semesterTotalWeeks = explicitWeeks;
}
const semesterStartDate = normalizeNumericDate(match[1], match[2], match[3]);
if (!semesterStartDate) return null;
// 结束日期用于估算周数(可选)
let semesterEndDate = null;
if (match[4] && match[5] && match[6]) {
semesterEndDate = normalizeNumericDate(match[4], match[5], match[6]);
}
if (!semesterTotalWeeks && semesterEndDate && semesterEndDate >= semesterStartDate) {
const days = Math.round((new Date(semesterEndDate) - new Date(semesterStartDate)) / 86400000) + 1;
const estimated = Math.round(days / 7);
if (estimated >= 1 && estimated <= MAX_SUPPORTED_WEEK) semesterTotalWeeks = estimated;
}
if (!semesterTotalWeeks) return null;
return {
semesterStartDate,
semesterEndDate,
semesterTotalWeeks,
firstDayOfWeek: 1
};
}
async function request(url, options = {}) {
const response = await fetch(url, { credentials: "include", ...options });
if (!response.ok) throw new Error(`网络请求失败: ${response.status}`);
return await response.text();
}
async function detectParameters() {
const html = await request(`${BASE_URL}/eams/courseTableForStd.action`);
const params = parseParameters(html);
if (!params) {
console.warn("[参数识别失败] 未能从课表页识别 ids 与 semesterBar");
throw new Error("未能识别教务参数,请确认已通过服务大厅登录并访问过课表页");
}
return params;
}
async function getSelectedSemester(tagId, currentSemesterId) {
const form = new URLSearchParams();
form.set("tagId", tagId);
form.set("dataType", "semesterCalendar");
if (currentSemesterId) form.set("value", currentSemesterId);
form.set("empty", "false");
const raw = await request(`${BASE_URL}/eams/dataQuery.action`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
body: form.toString()
});
const parsed = parseSemesterResponse(raw);
if (parsed.semesters.length === 0) throw new Error("未获取到可选学期");
const selectedId = currentSemesterId || parsed.currentSemesterId;
const defaultIndex = parsed.semesters.findIndex(semester => semester.id === selectedId);
const index = await window.shiguangBridgePromise.showSingleSelection(
"选择学期",
JSON.stringify(parsed.semesters.map(semester => semester.name)),
defaultIndex
);
return Number.isInteger(index) && index >= 0 && index < parsed.semesters.length
? parsed.semesters[index]
: null;
}
async function fetchAndParseCourses(semesterId, ids) {
const form = new URLSearchParams();
form.set("ignoreHead", "1");
form.set("setting.kind", "std");
form.set("startWeek", "");
form.set("semester.id", String(semesterId));
form.set("ids", String(ids));
const html = await request(`${BASE_URL}/eams/courseTableForStd!courseTable.action`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
body: form.toString()
});
// 如果返回了登录页,说明教务会话未建立
if (/loginExt|登录/i.test(html) && !html.includes("courseTableForStd") && html.length < 30000) {
throw new Error("教务会话未建立(请求被重定向到登录页),请先在服务大厅打开过课表页面");
}
return parseTaskActivities(html);
}
async function fetchCalendarInfo(semesterId) {
const form = new URLSearchParams();
form.set("version", "1");
form.set("semesterId", String(semesterId));
const html = await request(`${BASE_URL}/eams/base/calendar-info.action`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
body: form.toString()
});
const calendarInfo = parseCalendarInfo(html);
if (!calendarInfo) {
const cleaned = String(html || "")
.replace(/<[^>]*>/g, " ")
.replace(/ | /gi, " ")
.replace(/\u00a0/g, " ")
.replace(/\s+/g, " ").trim();
console.warn(`[学期日历] 未能解析学期日历,原文(清理后前400字): ${cleaned.slice(0, 400)}`);
throw new Error("未能解析学期日历");
}
return calendarInfo;
}
/**
* 获取作息时间:优先从课表页 HTML 动态解析,失败则使用兜底作息
*/
async function fetchTimeSlots() {
try {
const html = await request(`${BASE_URL}/eams/courseTableForStd.action`);
const parsed = parseTimeSlotsFromHtml(html);
if (parsed && parsed.length > 0) {
console.log(`[作息] 已从课表页解析出 ${parsed.length} 个节次时间段`);
return parsed;
}
} catch (error) {
console.warn(`[作息] 课表页解析失败,使用兜底作息: ${error.message}`);
}
console.log(`[作息] 使用兜底作息(${ZZQCC_TIME_SLOTS_FALLBACK.length} 个节次)`);
return ZZQCC_TIME_SLOTS_FALLBACK.map(slot => ({ ...slot }));
}
async function trySaveCalendarInfo(semesterId) {
try {
const calendarInfo = await fetchCalendarInfo(semesterId);
const saveResult = await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
semesterStartDate: calendarInfo.semesterStartDate,
semesterTotalWeeks: calendarInfo.semesterTotalWeeks,
firstDayOfWeek: calendarInfo.firstDayOfWeek
}));
return saveResult === true;
} catch (error) {
console.warn(`[学期信息设置失败] ${error.message}`);
return false;
}
}
async function trySaveTimeSlots() {
try {
const timeSlots = await fetchTimeSlots();
const saveResult = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
return saveResult === true;
} catch (error) {
console.warn(`[作息时间设置失败] ${error.message}`);
return false;
}
}
function buildCompletionMessage(calendarSaved, timeSlotsSaved) {
if (calendarSaved && timeSlotsSaved) return "成功导入课表、学期信息和本校作息";
if (!calendarSaved && !timeSlotsSaved) {
return "课表已导入,学期日期和作息时间设置失败,请在设置中确认";
}
if (!calendarSaved) return "课表已导入,学期日期获取失败,请在设置中确认";
return "课表已导入,作息时间设置失败,请在设置中确认";
}
/**
* 从选中学期的数据中保存学期配置(开始日期 + 总周数=课表最大周次),失败返回 false
*/
async function trySaveSemesterConfig(semester, courses) {
try {
const startDate = semester && semester.startDate;
if (!startDate) {
console.warn("[学期配置] 学期数据中未找到开始日期,改用 calendar-info 兜底");
return false;
}
let maxWeek = 0;
(courses || []).forEach(course => {
(course.weeks || []).forEach(w => {
if (Number.isInteger(w) && w > maxWeek) maxWeek = w;
});
});
const totalWeeks = maxWeek >= 1 ? maxWeek : 20;
const config = { semesterStartDate: startDate, semesterTotalWeeks: totalWeeks, firstDayOfWeek: 1 };
console.log(`[学期配置] 保存: ${JSON.stringify(config)}`);
const saveResult = await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
return saveResult === true;
} catch (error) {
console.warn(`[学期配置] 保存失败: ${error.message}`);
return false;
}
}
/**
* 日期输入校验函数(供 showPrompt 使用)
*/
function validateDateInput(input) {
const m = String(input || "").trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
if (!m) return "请输入 YYYY-MM-DD 格式(如 2026-09-07)";
const d = normalizeNumericDate(m[1], m[2], m[3]);
if (!d) return "日期无效,请检查月份和日期";
return false;
}
/**
* 最终兜底:让用户输入本学期开学日期,并保存学期配置
*/
async function promptSemesterStartDate(courses) {
try {
const input = await window.shiguangBridgePromise.showPrompt(
"设置本学期开学日期",
"未能自动获取学期日期,请输入本学期开学日期(格式 YYYY-MM-DD,例如 2026-09-07)",
"2026-09-07",
"validateDateInput"
);
if (!input) {
window.shiguangBridge.showToast("已跳过开学日期设置");
return false;
}
const m = String(input).trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
if (!m) return false;
const startDate = normalizeNumericDate(m[1], m[2], m[3]);
if (!startDate) return false;
let maxWeek = 0;
(courses || []).forEach(course => {
(course.weeks || []).forEach(w => {
if (Number.isInteger(w) && w > maxWeek) maxWeek = w;
});
});
const totalWeeks = maxWeek >= 1 ? maxWeek : 20;
const config = { semesterStartDate: startDate, semesterTotalWeeks: totalWeeks, firstDayOfWeek: 1 };
console.log(`[学期配置] 用户输入开学日期保存: ${JSON.stringify(config)}`);
const saveResult = await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
return saveResult === true;
} catch (error) {
console.warn(`[学期配置] 用户输入保存失败: ${error.message}`);
return false;
}
}
async function runImportFlow() {
try {
const alertConfirmed = await window.shiguangBridgePromise.showAlert(
"郑州汽车工程职业学院课表导入",
"请确认已完成以下步骤:\n1. 已在页面上通过统一身份认证(CAS)登录\n2. 已从【服务大厅】→【学生课表查询】打开过课表页面\n3. 当前页面处于 jw.zzvcae.edu.cn 域下\n\n确认后开始同步课表。",
"开始同步"
);
if (!alertConfirmed) return;
window.shiguangBridge.showToast("开始探测教务参数...");
const params = await detectParameters();
if (!params) throw new Error("未能识别教务参数,请确认已通过服务大厅登录并访问过课表页");
const semester = await getSelectedSemester(params.tagId, params.currentSemesterId);
if (!semester) return;
window.shiguangBridge.showToast("正在同步课表...");
const courses = await fetchAndParseCourses(semester.id, params.ids);
if (!courses || courses.length === 0) {
console.warn("[课程解析] TaskActivity 解析结果为空,请检查上方[诊断]输出中的 TaskActivity/var teachers 标记");
throw new Error("未解析到课程数据(请把控制台[诊断]信息反馈给开发者)");
}
console.log(`[课程解析] 成功解析 ${courses.length} 门课程时段`);
const saveResult = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
if (!saveResult) throw new Error("课程保存失败");
// 学期配置:优先学期数据 → calendar-info 兜底 → 用户手动输入兜底
let calendarSaved = await trySaveSemesterConfig(semester, courses);
if (!calendarSaved) {
calendarSaved = await trySaveCalendarInfo(semester.id);
}
if (!calendarSaved) {
calendarSaved = await promptSemesterStartDate(courses);
}
const timeSlotsSaved = await trySaveTimeSlots();
window.shiguangBridge.showToast(buildCompletionMessage(calendarSaved, timeSlotsSaved));
window.shiguangBridge.notifyTaskCompletion();
} catch (error) {
console.error(`[郑州汽车工程职业学院课表导入异常] ${error && error.stack ? error.stack : error}`);
try {
await window.shiguangBridgePromise.showAlert("导入失败", error.message, "知道了");
} catch (dialogError) {
window.shiguangBridge.showToast(`导入失败: ${error.message}`);
}
}
}
runImportFlow();
|