| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- // 大连理工大学本科教务系统适配脚本
- // 适配人: dffzcqs78694
- //
- // 接口链路(与页面实际请求一致):
- // 1. GET /student/for-std/course-table → 解析学期下拉框 + 提取 stdPersonId
- // 2. GET /student/ws/schedule-table/get-data?bizTypeId=2&semesterId={id} → lessonIds / timeTableLayoutId / weekIndices
- // 3. POST /student/ws/schedule-table/datum → lessonList(课程) + scheduleList(排课明细)
- // 4. POST /student/ws/schedule-table/timetable-layout → 节次时间定义
- // 5. GET /student/ws/semester/get/{semesterId} → 学期起止日期(用于推算开学日期, 失败则回退)
- // ---- 节次时间表兜底数据(接口失败时使用, 与大工 timetable-layout 一致) ----
- const FALLBACK_TIME_SLOTS = [
- { "number": 1, "startTime": "08:00", "endTime": "08:45" },
- { "number": 2, "startTime": "08:50", "endTime": "09:35" },
- { "number": 3, "startTime": "10:05", "endTime": "10:50" },
- { "number": 4, "startTime": "10:55", "endTime": "11:40" },
- { "number": 5, "startTime": "13:30", "endTime": "14:15" },
- { "number": 6, "startTime": "14:20", "endTime": "15:05" },
- { "number": 7, "startTime": "15:35", "endTime": "16:20" },
- { "number": 8, "startTime": "16:25", "endTime": "17:10" },
- { "number": 9, "startTime": "18:00", "endTime": "18:45" },
- { "number": 10, "startTime": "18:50", "endTime": "19:35" },
- { "number": 11, "startTime": "19:40", "endTime": "20:25" },
- { "number": 12, "startTime": "20:30", "endTime": "21:15" }
- ];
- // 时间数字格式化: 1800 -> "18:00"
- function formatTimeNum(timeNum) {
- if (timeNum === null || timeNum === undefined) return "";
- const str = String(timeNum).padStart(4, '0');
- return `${str.slice(0, 2)}:${str.slice(2, 4)}`;
- }
- // 判断时间数字是否在 [start, end] 区间内(用于节次映射)
- function timeInRange(timeNum, startNum, endNum) {
- return timeNum >= startNum && timeNum <= endNum;
- }
- // 构建节次列表(数字格式): 优先用接口返回的 courseUnitList, 失败时用内置兜底作息表
- function buildCourseUnits(layout) {
- if (layout && Array.isArray(layout.courseUnitList) && layout.courseUnitList.length > 0) {
- return layout.courseUnitList;
- }
- // 兜底: 内置节次表(与大工 timetable-layout 实际数据一致)
- return FALLBACK_TIME_SLOTS.map(slot => ({
- indexNo: slot.number,
- startTime: parseInt(slot.startTime.replace(':', ''), 10),
- endTime: parseInt(slot.endTime.replace(':', ''), 10)
- }));
- }
- // ---- 纯函数: 根据课程原始排课明细解析为拾光课程格式 ----
- // printData: datum 响应中的 result 对象
- // layout: timetable-layout 响应中的 result 对象
- // 返回课程数组
- function parseCourses(printData, layout) {
- const lessonList = printData.lessonList || [];
- const scheduleList = printData.scheduleList || [];
- const lessonById = {};
- lessonList.forEach(lesson => { lessonById[lesson.id] = lesson; });
- // 从 layout 中构建节次列表 (用于 startTime/endTime -> 节次号)
- const units = buildCourseUnits(layout);
- // 按 (lessonId, weekday, startTime, endTime) 分组, 收集周次
- const groups = {};
- scheduleList.forEach(schedule => {
- const key = `${schedule.lessonId}|${schedule.weekday}|${schedule.startTime}|${schedule.endTime}`;
- if (!groups[key]) {
- groups[key] = { schedule: schedule, weeks: [] };
- }
- if (schedule.weekIndex !== null && schedule.weekIndex !== undefined) {
- groups[key].weeks.push(schedule.weekIndex);
- }
- });
- const courses = [];
- Object.keys(groups).forEach(key => {
- const group = groups[key];
- const schedule = group.schedule;
- const lesson = lessonById[schedule.lessonId] || {};
- // 星期几
- const day = schedule.weekday;
- // 节次映射: 找到所有 startTime>=调度开始 && endTime<=调度结束 的节次
- let startSection = null;
- let endSection = null;
- if (units.length > 0 && schedule.startTime !== null && schedule.endTime !== null) {
- const matched = units.filter(unit =>
- timeInRange(unit.startTime, schedule.startTime, schedule.endTime) &&
- timeInRange(unit.endTime, schedule.startTime, schedule.endTime)
- );
- if (matched.length > 0) {
- startSection = matched[0].indexNo;
- endSection = matched[matched.length - 1].indexNo;
- }
- }
- // 教师: 优先取排课明细中的教师, 否则取课程下的教师列表
- let teacher = schedule.personName || "";
- if (!teacher && lesson.teacherAssignmentList) {
- teacher = lesson.teacherAssignmentList.map(t => t.name).filter(Boolean).join(" ");
- }
- // 教室
- let position = "";
- if (schedule.room && schedule.room.nameZh) {
- position = schedule.room.nameZh;
- } else if (schedule.customPlace) {
- position = schedule.customPlace;
- }
- // 周次去重排序
- const weeks = Array.from(new Set(group.weeks)).sort((a, b) => a - b);
- const course = {
- name: lesson.courseName || "",
- teacher: teacher,
- position: position,
- day: day,
- startSection: startSection,
- endSection: endSection,
- weeks: weeks
- };
- courses.push(course);
- });
- return courses;
- }
- // ---- 纯函数: 生成预设时间段 ----
- // layout: timetable-layout 响应中的 result 对象
- function buildTimeSlots(layout) {
- if (layout && Array.isArray(layout.courseUnitList) && layout.courseUnitList.length > 0) {
- return layout.courseUnitList.map(unit => ({
- number: unit.indexNo,
- startTime: formatTimeNum(unit.startTime),
- endTime: formatTimeNum(unit.endTime)
- }));
- }
- return FALLBACK_TIME_SLOTS;
- }
- // ---- 纯函数: 推算学期开始日期(第1周周一) ----
- // 排课明细的 date + weekIndex 已知, 用最小的 date 算出其所在周周一,
- // 再按 weekIndex 差值回退到第 1 周周一。
- // 例: 第3周周一=2026-09-14, 则第1周周一 = 09-14 - 2周 = 2026-08-31
- function calcSemesterStartDate(printData) {
- let minDate = null;
- let minWeekIndex = null;
- (printData.scheduleList || []).forEach(s => {
- if (s.date && s.date.length >= 10 && s.weekIndex !== null && s.weekIndex !== undefined) {
- if (!minDate || s.date < minDate) {
- minDate = s.date;
- minWeekIndex = s.weekIndex;
- }
- }
- });
- if (!minDate || !minWeekIndex) return null;
- // 该日期所在周的周一
- const dt = new Date(minDate);
- const day = dt.getDay(); // 0=周日
- const diff = (day === 0 ? -6 : 1 - day);
- dt.setDate(dt.getDate() + diff);
- // 回退到第1周
- dt.setDate(dt.getDate() - (minWeekIndex - 1) * 7);
- return dt.toISOString().split('T')[0];
- }
- // ---- 桥接封装 ----
- async function bridgeSaveCourses(courses) {
- const result = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
- if (result === true) {
- window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
- return true;
- }
- window.shiguangBridge.showToast("课程导入未成功,请查看日志。");
- return false;
- }
- async function bridgeSaveTimeSlots(timeSlots) {
- const result = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
- if (result === true) {
- window.shiguangBridge.showToast(`成功导入 ${timeSlots.length} 个时间段!`);
- return true;
- }
- window.shiguangBridge.showToast("时间段导入失败,请查看日志。");
- return false;
- }
- async function bridgeSaveConfig(config) {
- const result = await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
- if (result === true) {
- window.shiguangBridge.showToast("课表配置导入成功!");
- return true;
- }
- window.shiguangBridge.showToast("课表配置导入失败,请查看日志。");
- return false;
- }
- // ---- 1. 公告 ----
- async function promptUserToStart() {
- try {
- const confirmed = await window.shiguangBridgePromise.showAlert(
- "导入提醒",
- "请确保您已登录教务系统,导入前请确认当前学期正确。",
- "好的,开始导入"
- );
- return confirmed === true;
- } catch (e) {
- console.error("公告弹窗出错:", e);
- window.shiguangBridge.showToast("显示公告出错: " + e.message);
- return false;
- }
- }
- // ---- 2. 获取学期列表 ----
- async function getSemesterOptions() {
- try {
- const response = await fetch(`/student/for-std/course-table`);
- const htmlString = await response.text();
- const parser = new DOMParser();
- const dom = parser.parseFromString(htmlString, 'text/html');
- const selectElement = dom.getElementById('semesters') || dom.getElementById('allSemesters');
- if (!selectElement) throw new Error("页面中未找到学期选择框");
- const options = Array.from(selectElement.options);
- const validOptions = options.filter(opt => opt.value && opt.value !== "all");
- if (validOptions.length === 0) throw new Error("未解析到有效的学期列表");
- const semesterTexts = validOptions.map(opt => opt.text.trim());
- const semesterValues = validOptions.map(opt => opt.value);
- return { semesterTexts, semesterValues };
- } catch (e) {
- console.error("获取学期列表失败:", e);
- window.shiguangBridge.showToast("获取学期列表失败: " + e.message);
- return null;
- }
- }
- // ---- 3. 获取 stdPersonId (从页面 JS 变量中提取) ----
- async function fetchStdPersonId() {
- try {
- const response = await fetch(`/student/for-std/course-table`);
- const htmlString = await response.text();
- // 常见模式: stdPersonId = 3284653 或 stdPersonId: 3284653 或 stdPersonId":"3284653"
- const patterns = [
- /stdPersonId\s*[:=]\s*["']?(\d+)["']?/,
- /std_person_id\s*[:=]\s*["']?(\d+)["']?/,
- /stdPersonId["']?\s*[:=]\s*["']?(\d+)["']?/
- ];
- for (const pattern of patterns) {
- const match = htmlString.match(pattern);
- if (match) return match[1];
- }
- return null;
- } catch (e) {
- console.error("提取 stdPersonId 失败:", e);
- return null;
- }
- }
- // ---- 4. 获取课程基础数据 (get-data) ----
- async function fetchGetData(semesterId) {
- const url = `/student/for-std/course-table/get-data?bizTypeId=2&semesterId=${semesterId}`;
- const response = await fetch(url);
- if (!response.ok) throw new Error(`get-data 请求失败, 状态码: ${response.status}`);
- return await response.json();
- }
- // ---- 5. 获取排课明细 (datum) ----
- async function fetchDatum(lessonIds, stdPersonId) {
- const body = {
- lessonIds: lessonIds,
- studentId: null,
- stdPersonId: stdPersonId,
- weekIndex: null
- };
- const response = await fetch(`/student/ws/schedule-table/datum`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(body)
- });
- if (!response.ok) throw new Error(`datum 请求失败, 状态码: ${response.status}`);
- return await response.json();
- }
- // ---- 6. 获取节次时间表 (timetable-layout) ----
- async function fetchTimeTableLayout(timeTableLayoutId) {
- try {
- const response = await fetch(`/student/ws/schedule-table/timetable-layout`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ id: timeTableLayoutId })
- });
- if (!response.ok) throw new Error(`timetable-layout 请求失败, 状态码: ${response.status}`);
- const json = await response.json();
- if (!json || !json.result || !json.result.courseUnitList) throw new Error("timetable-layout 响应结构异常");
- return json.result;
- } catch (e) {
- console.warn("获取节次时间表失败, 使用内置兜底数据:", e);
- return { courseUnitList: null };
- }
- }
- // ---- 7. 获取学期日期信息 (用于推算开学日期, 可选) ----
- async function fetchSemesterInfo(semesterId) {
- try {
- const response = await fetch(`/student/ws/semester/get/${semesterId}`);
- if (!response.ok) return null;
- return await response.json();
- } catch (e) {
- return null;
- }
- }
- // ---- 主流程 ----
- async function runImportFlow() {
- window.shiguangBridge.showToast("课程导入流程即将开始...");
- // 1. 公告确认
- const alertConfirmed = await promptUserToStart();
- if (!alertConfirmed) {
- return;
- }
- // 2. 选择学期
- const semesterOptions = await getSemesterOptions();
- if (!semesterOptions) return;
- const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
- "选择学期",
- JSON.stringify(semesterOptions.semesterTexts),
- 0
- );
- if (selectedIndex === null || selectedIndex < 0) {
- window.shiguangBridge.showToast("导入已取消。");
- return;
- }
- const semesterId = semesterOptions.semesterValues[selectedIndex];
- window.shiguangBridge.showToast(`已选择: ${semesterOptions.semesterTexts[selectedIndex]}`);
- try {
- // 3. 获取课程基础数据
- window.shiguangBridge.showToast("正在获取课程数据...");
- const getData = await fetchGetData(semesterId);
- const lessonIds = getData.lessonIds || [];
- const timeTableLayoutId = getData.timeTableLayoutId;
- // 4. 获取 stdPersonId
- let stdPersonId = await fetchStdPersonId();
- // 5. 获取排课明细
- window.shiguangBridge.showToast("正在获取排课明细...");
- const datum = await fetchDatum(lessonIds, stdPersonId);
- const result = datum.result;
- if (!result || !result.lessonList || !result.scheduleList) {
- throw new Error("datum 响应结构异常, 请检查是否已登录");
- }
- // 6. 获取节次时间表
- const layout = await fetchTimeTableLayout(timeTableLayoutId);
- // 7. 解析课程
- const courses = parseCourses(result, layout);
- if (courses.length === 0) {
- window.shiguangBridge.showToast("未解析到课程数据。");
- return;
- }
- // 8. 生成时间段
- const timeSlots = buildTimeSlots(layout);
- // 9. 生成课表配置
- const config = {
- semesterStartDate: null,
- semesterTotalWeeks: 20,
- defaultClassDuration: 45,
- defaultBreakDuration: 5,
- firstDayOfWeek: 1
- };
- // 优先尝试从学期接口获取开学日期与周数
- const semesterInfo = await fetchSemesterInfo(semesterId);
- if (semesterInfo && semesterInfo.startDate) {
- config.semesterStartDate = semesterInfo.startDate;
- if (semesterInfo.endDate) {
- const start = new Date(semesterInfo.startDate);
- const end = new Date(semesterInfo.endDate);
- const diffDays = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
- const weeks = Math.ceil(diffDays / 7);
- if (weeks > 0) config.semesterTotalWeeks = weeks;
- }
- } else {
- // 回退: 从排课明细推算开学日期
- const startDate = calcSemesterStartDate(result);
- if (startDate) config.semesterStartDate = startDate;
- // 回退: 从课程实际最大周次推算总周数(避免出现大量空白周)
- if (courses.length > 0) {
- let maxWeek = 0;
- courses.forEach(c => c.weeks.forEach(w => { if (w > maxWeek) maxWeek = w; }));
- if (maxWeek > 0) config.semesterTotalWeeks = maxWeek;
- }
- }
- // 10. 依次保存
- const timeSlotOk = await bridgeSaveTimeSlots(timeSlots);
- if (!timeSlotOk) return;
- const configOk = await bridgeSaveConfig(config);
- if (!configOk) return;
- const coursesOk = await bridgeSaveCourses(courses);
- if (!coursesOk) return;
- // 11. 完成
- window.shiguangBridge.showToast("课程导入完成!");
- window.shiguangBridge.notifyTaskCompletion();
- } catch (e) {
- console.error("导入流程发生错误:", e);
- window.shiguangBridge.showToast("导入失败: " + e.message);
- }
- }
- // 启动
- runImportFlow();
|