| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713 |
- // 郑州汽车工程职业学院 (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 标签(如 <sup style='...'></sup> 颜色标记)
- .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 = /<th[^>]*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();
|