|
@@ -1,113 +1,208 @@
|
|
|
// 文件: neu.js
|
|
// 文件: neu.js
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 显示自定义学年学期选择对话框
|
|
|
|
|
- * @returns {Promise<{semesterCode: string, xnxqdm: string, xqdm: string} | null>}
|
|
|
|
|
- * 返回包含学期代码的对象,若取消则返回 null
|
|
|
|
|
|
|
+ * ============================================================
|
|
|
|
|
+ * 1. 用户交互层(使用桥接 API)
|
|
|
|
|
+ * ============================================================
|
|
|
*/
|
|
*/
|
|
|
-async function showCustomSemesterDialog() {
|
|
|
|
|
- return new Promise((resolve) => {
|
|
|
|
|
-
|
|
|
|
|
- const overlay = document.createElement('div');
|
|
|
|
|
- overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center';
|
|
|
|
|
- const dialog = document.createElement('div');
|
|
|
|
|
- dialog.style.cssText = 'background:white;padding:20px;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,0.3);min-width:280px;text-align:center';
|
|
|
|
|
- dialog.innerHTML = `
|
|
|
|
|
- <div style="font-size:18px;margin-bottom:20px;font-weight:bold">选择学年学期</div>
|
|
|
|
|
- <div style="display:flex;align-items:center;justify-content:center;margin-bottom:20px">
|
|
|
|
|
- <input type="number" id="startYear" placeholder="起始年份" value="2025" style="width:80px;padding:5px">
|
|
|
|
|
- <span style="margin:0 5px">—</span>
|
|
|
|
|
- <input type="number" id="endYear" placeholder="结束年份" value="2026" style="width:80px;padding:5px">
|
|
|
|
|
- </div>
|
|
|
|
|
- <div style="margin-bottom:20px">
|
|
|
|
|
- <select id="termSelect" style="width:100%;padding:5px">
|
|
|
|
|
- <option value="fall">秋季学期</option>
|
|
|
|
|
- <option value="spring">春季学期</option>
|
|
|
|
|
- </select>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div style="display:flex;justify-content:space-around">
|
|
|
|
|
- <button id="confirmBtn" style="padding:5px 15px;background:#4CAF50;color:white;border:none;border-radius:4px;cursor:pointer">确定</button>
|
|
|
|
|
- <button id="cancelBtn" style="padding:5px 15px;background:#f44336;color:white;border:none;border-radius:4px;cursor:pointer">取消</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- `;
|
|
|
|
|
- overlay.appendChild(dialog);
|
|
|
|
|
- document.body.appendChild(overlay);
|
|
|
|
|
-
|
|
|
|
|
- const startYearInput = dialog.querySelector('#startYear');
|
|
|
|
|
- const endYearInput = dialog.querySelector('#endYear');
|
|
|
|
|
- const termSelect = dialog.querySelector('#termSelect');
|
|
|
|
|
- const confirmBtn = dialog.querySelector('#confirmBtn');
|
|
|
|
|
- const cancelBtn = dialog.querySelector('#cancelBtn');
|
|
|
|
|
-
|
|
|
|
|
- const cleanup = () => document.body.removeChild(overlay);
|
|
|
|
|
- confirmBtn.onclick = () => {
|
|
|
|
|
- const start = parseInt(startYearInput.value, 10);
|
|
|
|
|
- const end = parseInt(endYearInput.value, 10);
|
|
|
|
|
- if (isNaN(start) || isNaN(end)) { alert('请输入有效年份'); return; }
|
|
|
|
|
- const semesterNum = termSelect.value === 'fall' ? '1' : '2';
|
|
|
|
|
- const semesterCode = `${start}-${end}-${semesterNum}`;
|
|
|
|
|
- cleanup();
|
|
|
|
|
- resolve({ semesterCode, xnxqdm: semesterCode, xqdm: '01' });
|
|
|
|
|
- };
|
|
|
|
|
- cancelBtn.onclick = () => { cleanup(); resolve(null); };
|
|
|
|
|
- });
|
|
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 显示公告并等待用户确认
|
|
|
|
|
+ * @returns {Promise<boolean>} true-继续,false-取消
|
|
|
|
|
+ */
|
|
|
|
|
+async function showStartAlert() {
|
|
|
|
|
+ const confirmed = await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ "课表导入",
|
|
|
|
|
+ "请确保您已登录教务系统(jwxt.neu.edu.cn),且网络连接正常。",
|
|
|
|
|
+ "开始导入"
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!confirmed) {
|
|
|
|
|
+ window.shiguangBridge.showToast("已取消导入");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 全局验证函数:校验学年输入
|
|
|
|
|
+ */
|
|
|
|
|
+function validateYearInput(input) {
|
|
|
|
|
+ if (/^[0-9]{4}$/.test(input)) {
|
|
|
|
|
+ return false; // 验证通过
|
|
|
|
|
+ }
|
|
|
|
|
+ return "请输入四位数字的年份!";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 显示学期选择(封装 showCustomSemesterDialog)
|
|
|
|
|
- * @returns {Promise<string|false>} 返回学期代码字符串,取消则返回 false
|
|
|
|
|
|
|
+ * 获取学年(起始年份)
|
|
|
|
|
+ * @returns {Promise<string|null>} 四位年份字符串,取消返回 null
|
|
|
*/
|
|
*/
|
|
|
-async function showSemesterSelection() {
|
|
|
|
|
- const res = await showCustomSemesterDialog();
|
|
|
|
|
- return res ? res.semesterCode : false;
|
|
|
|
|
|
|
+async function getAcademicYear() {
|
|
|
|
|
+ const year = await window.shiguangBridgePromise.showPrompt(
|
|
|
|
|
+ "选择学年",
|
|
|
|
|
+ "请输入起始年份(如 2025):",
|
|
|
|
|
+ new Date().getFullYear().toString(),
|
|
|
|
|
+ "validateYearInput"
|
|
|
|
|
+ );
|
|
|
|
|
+ if (year === null) {
|
|
|
|
|
+ window.shiguangBridge.showToast("已取消导入");
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return year;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 选择学期(秋季/春季)
|
|
|
|
|
+ * @returns {Promise<{termType: string}|null>}
|
|
|
|
|
+ */
|
|
|
|
|
+async function selectSemester() {
|
|
|
|
|
+ const items = ["秋季学期 (1)", "春季学期 (2)"];
|
|
|
|
|
+ const idx = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
|
|
+ "选择学期",
|
|
|
|
|
+ JSON.stringify(items),
|
|
|
|
|
+ -1
|
|
|
|
|
+ );
|
|
|
|
|
+ if (idx === null) {
|
|
|
|
|
+ window.shiguangBridge.showToast("已取消导入");
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ const termType = idx === 0 ? "1" : "2";
|
|
|
|
|
+ return { termType };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 显示校区选择对话框(通过Android原生弹窗)
|
|
|
|
|
- * @returns {Promise<string|false>} 返回校区名称("南湖校区"或"浑南校区"),取消返回 false
|
|
|
|
|
|
|
+ * 选择校区
|
|
|
|
|
+ * @returns {Promise<string|null>} "南湖校区" 或 "浑南校区"
|
|
|
*/
|
|
*/
|
|
|
-async function showCampusSelection() {
|
|
|
|
|
|
|
+async function selectCampus() {
|
|
|
const campuses = ["南湖校区", "浑南校区"];
|
|
const campuses = ["南湖校区", "浑南校区"];
|
|
|
- try {
|
|
|
|
|
- const idx = await window.shiguangBridgePromise.showSingleSelection("选择你所在的校区", JSON.stringify(campuses), 2);
|
|
|
|
|
- return idx !== -1 ? campuses[idx] : false;
|
|
|
|
|
- } catch(e) {
|
|
|
|
|
- window.shiguangBridge.showToast("显示校区列表出错:" + e.message);
|
|
|
|
|
- return false;
|
|
|
|
|
|
|
+ const idx = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
|
|
+ "选择校区",
|
|
|
|
|
+ JSON.stringify(campuses),
|
|
|
|
|
+ -1
|
|
|
|
|
+ );
|
|
|
|
|
+ if (idx === null) {
|
|
|
|
|
+ window.shiguangBridge.showToast("已取消导入");
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
|
|
+ return campuses[idx];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 弹窗询问用户是否导入考试时间(测试功能)
|
|
|
|
|
|
|
+ * 询问是否导入考试时间
|
|
|
* @returns {Promise<boolean>} true-导入,false-不导入
|
|
* @returns {Promise<boolean>} true-导入,false-不导入
|
|
|
*/
|
|
*/
|
|
|
async function askImportExams() {
|
|
async function askImportExams() {
|
|
|
- return new Promise((resolve) => {
|
|
|
|
|
- const overlay = document.createElement('div');
|
|
|
|
|
- overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center';
|
|
|
|
|
- const dialog = document.createElement('div');
|
|
|
|
|
- dialog.style.cssText = 'background:white;padding:20px;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,0.3);min-width:280px;text-align:center';
|
|
|
|
|
- dialog.innerHTML = `
|
|
|
|
|
- <div style="font-size:16px;margin-bottom:10px;font-weight:bold">是否导入考试时间</div>
|
|
|
|
|
- <div style="font-size:12px;color:gray;margin-bottom:20px">测试功能,周数默认为第15周,需手动调整到对应日期。出错请反馈</div>
|
|
|
|
|
- <div style="display:flex;justify-content:space-around">
|
|
|
|
|
- <button id="yesBtn" style="padding:5px 15px;background:#4CAF50;color:white;border:none;border-radius:4px;cursor:pointer">是</button>
|
|
|
|
|
- <button id="noBtn" style="padding:5px 15px;background:#f44336;color:white;border:none;border-radius:4px;cursor:pointer">否</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- `;
|
|
|
|
|
- overlay.appendChild(dialog);
|
|
|
|
|
- document.body.appendChild(overlay);
|
|
|
|
|
- const cleanup = () => document.body.removeChild(overlay);
|
|
|
|
|
- dialog.querySelector('#yesBtn').onclick = () => { cleanup(); resolve(true); };
|
|
|
|
|
- dialog.querySelector('#noBtn').onclick = () => { cleanup(); resolve(false); };
|
|
|
|
|
|
|
+ const confirmed = await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ "导入考试时间",
|
|
|
|
|
+ "是否导入考试时间?\n(测试功能,考试固定在第15周,出错请反馈)",
|
|
|
|
|
+ "导入"
|
|
|
|
|
+ );
|
|
|
|
|
+ return confirmed;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * ============================================================
|
|
|
|
|
+ * 2. 数据解析层
|
|
|
|
|
+ * ============================================================
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 增强版周次解析:支持 "1-8周", "2-6周(双)", "1,3,5周" 等格式
|
|
|
|
|
+ */
|
|
|
|
|
+function parseWeeksString(weeksStr) {
|
|
|
|
|
+ if (!weeksStr) return [];
|
|
|
|
|
+ const result = [];
|
|
|
|
|
+ const weekParts = weeksStr.split(/[,,]/).map(part => part.trim());
|
|
|
|
|
+
|
|
|
|
|
+ weekParts.forEach(part => {
|
|
|
|
|
+ // 匹配单个数字周,如 "6周" 或 "6周(单)"
|
|
|
|
|
+ const singleMatch = part.match(/^(\d+)周(?:\(([单双])\))?$/);
|
|
|
|
|
+ if (singleMatch) {
|
|
|
|
|
+ const num = parseInt(singleMatch[1]);
|
|
|
|
|
+ const type = singleMatch[2];
|
|
|
|
|
+ if (!type || (type === '单' && num % 2 === 1) || (type === '双' && num % 2 === 0)) {
|
|
|
|
|
+ result.push(num);
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 匹配范围周,如 "1-8周" 或 "2-6周(双)"
|
|
|
|
|
+ const rangeMatch = part.match(/^(\d+)-(\d+)周(?:\(([单双])\))?$/);
|
|
|
|
|
+ if (rangeMatch) {
|
|
|
|
|
+ const start = parseInt(rangeMatch[1]);
|
|
|
|
|
+ const end = parseInt(rangeMatch[2]);
|
|
|
|
|
+ const type = rangeMatch[3];
|
|
|
|
|
+
|
|
|
|
|
+ if (!type) {
|
|
|
|
|
+ for (let i = start; i <= end; i++) result.push(i);
|
|
|
|
|
+ } else if (type === '单') {
|
|
|
|
|
+ for (let i = start; i <= end; i++) {
|
|
|
|
|
+ if (i % 2 === 1) result.push(i);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (type === '双') {
|
|
|
|
|
+ for (let i = start; i <= end; i++) {
|
|
|
|
|
+ if (i % 2 === 0) result.push(i);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
|
|
+ return [...new Set(result)].sort((a, b) => a - b);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 解析考试时间描述字符串,提取星期几、开始时间、结束时间
|
|
|
|
|
- * @param {string} desc 例如 "2026年05月06日 10:10-12:10(星期三第1场)"
|
|
|
|
|
- * @returns {{day: number|null, startTime: string|null, endTime: string|null}}
|
|
|
|
|
- * day: 1~7 对应星期一~星期日,无法解析则为 null
|
|
|
|
|
|
|
+ * 将API返回的课表原始数据转换为标准课程对象数组
|
|
|
|
|
+ * 直接使用 titleWeekTeacherClassroomDetail 数组,为每个安排生成独立课程
|
|
|
|
|
+ */
|
|
|
|
|
+function convertApiResponseToLessons(arrangedList) {
|
|
|
|
|
+ const lessons = [];
|
|
|
|
|
+ for (const item of arrangedList) {
|
|
|
|
|
+ const day = item.dayOfWeek;
|
|
|
|
|
+ const startSection = parseInt(item.beginSection, 10);
|
|
|
|
|
+ const endSection = parseInt(item.endSection, 10);
|
|
|
|
|
+ if (!day || isNaN(startSection) || isNaN(endSection)) continue;
|
|
|
|
|
+
|
|
|
|
|
+ const name = item.courseName || "";
|
|
|
|
|
+ if (!name) continue;
|
|
|
|
|
+
|
|
|
|
|
+ const details = item.titleWeekTeacherClassroomDetail || [];
|
|
|
|
|
+ if (details.length === 0) {
|
|
|
|
|
+ console.warn(`课程 "${name}" 无上课安排,已跳过`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const detail of details) {
|
|
|
|
|
+ if (!detail) continue;
|
|
|
|
|
+ const tokens = detail.trim().split(/\s+/);
|
|
|
|
|
+ if (tokens.length < 1) continue;
|
|
|
|
|
+ const weeksStr = tokens[0];
|
|
|
|
|
+ const teacher = tokens[1] || "";
|
|
|
|
|
+ const position = tokens.slice(2).join(' ');
|
|
|
|
|
+
|
|
|
|
|
+ const weeks = parseWeeksString(weeksStr);
|
|
|
|
|
+ if (weeks.length === 0) {
|
|
|
|
|
+ console.warn(`周次解析失败: "${weeksStr}",课程: ${name}`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ lessons.push({
|
|
|
|
|
+ name: name,
|
|
|
|
|
+ teacher: teacher,
|
|
|
|
|
+ position: position,
|
|
|
|
|
+ day: day,
|
|
|
|
|
+ startSection: startSection,
|
|
|
|
|
+ endSection: endSection,
|
|
|
|
|
+ weeks: weeks,
|
|
|
|
|
+ isCustomTime: false
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return lessons;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 解析考试时间描述字符串
|
|
|
*/
|
|
*/
|
|
|
function parseExamTimeDescription(desc) {
|
|
function parseExamTimeDescription(desc) {
|
|
|
const weekMap = { '星期一': 1, '星期二': 2, '星期三': 3, '星期四': 4, '星期五': 5, '星期六': 6, '星期日': 7 };
|
|
const weekMap = { '星期一': 1, '星期二': 2, '星期三': 3, '星期四': 4, '星期五': 5, '星期六': 6, '星期日': 7 };
|
|
@@ -129,16 +224,17 @@ function parseExamTimeDescription(desc) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 从考试API获取指定学期的考试数据,并转换为课程对象格式
|
|
|
|
|
- * @param {string} termCode 学期代码,如 "2025-2026-1"
|
|
|
|
|
- * @returns {Promise<Array<object>>} 课程对象数组,每个对象包含 name, teacher, position, day, weeks, isCustomTime, customStartTime, customEndTime
|
|
|
|
|
- * @throws 网络或API错误
|
|
|
|
|
|
|
+ * 从考试API获取考试数据
|
|
|
*/
|
|
*/
|
|
|
async function fetchExamsFromAPI(termCode) {
|
|
async function fetchExamsFromAPI(termCode) {
|
|
|
const url = `https://jwxt.neu.edu.cn/jwapp/sys/homeapp/api/home/student/exams.do?termCode=${encodeURIComponent(termCode)}`;
|
|
const url = `https://jwxt.neu.edu.cn/jwapp/sys/homeapp/api/home/student/exams.do?termCode=${encodeURIComponent(termCode)}`;
|
|
|
const response = await fetch(url, {
|
|
const response = await fetch(url, {
|
|
|
method: 'GET',
|
|
method: 'GET',
|
|
|
- headers: { 'Fetch-Api': 'true', 'Referer': 'https://jwxt.neu.edu.cn/jwapp/sys/homeapp/home/index.html', 'User-Agent': navigator.userAgent }
|
|
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Fetch-Api': 'true',
|
|
|
|
|
+ 'Referer': 'https://jwxt.neu.edu.cn/jwapp/sys/homeapp/home/index.html',
|
|
|
|
|
+ 'User-Agent': navigator.userAgent
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
if (!response.ok) throw new Error(`考试API HTTP ${response.status}`);
|
|
if (!response.ok) throw new Error(`考试API HTTP ${response.status}`);
|
|
|
const data = await response.json();
|
|
const data = await response.json();
|
|
@@ -165,7 +261,6 @@ async function fetchExamsFromAPI(termCode) {
|
|
|
console.warn("解析考试时间失败,跳过:", desc);
|
|
console.warn("解析考试时间失败,跳过:", desc);
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
- const weeks = [15]; // 考试固定在第15周(测试功能)
|
|
|
|
|
lessons.push({
|
|
lessons.push({
|
|
|
name: name,
|
|
name: name,
|
|
|
teacher: teacher,
|
|
teacher: teacher,
|
|
@@ -173,7 +268,7 @@ async function fetchExamsFromAPI(termCode) {
|
|
|
day: day,
|
|
day: day,
|
|
|
startSection: undefined,
|
|
startSection: undefined,
|
|
|
endSection: undefined,
|
|
endSection: undefined,
|
|
|
- weeks: weeks,
|
|
|
|
|
|
|
+ weeks: [15],
|
|
|
isCustomTime: true,
|
|
isCustomTime: true,
|
|
|
customStartTime: startTime,
|
|
customStartTime: startTime,
|
|
|
customEndTime: endTime
|
|
customEndTime: endTime
|
|
@@ -183,140 +278,31 @@ async function fetchExamsFromAPI(termCode) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 增强版周次解析:支持 "1-8周", "2-6周(双)", "1,3,5周" 等格式
|
|
|
|
|
- * @param {string} weeksStr 周次字符串,如 "1-8周"
|
|
|
|
|
- * @returns {number[]} 周次数字数组(已去重、排序)
|
|
|
|
|
- */
|
|
|
|
|
-function parseWeeksString(weeksStr) {
|
|
|
|
|
- if (!weeksStr) return [];
|
|
|
|
|
- const result = [];
|
|
|
|
|
- const weekParts = weeksStr.split(/[,,]/).map(part => part.trim());
|
|
|
|
|
-
|
|
|
|
|
- weekParts.forEach(part => {
|
|
|
|
|
- // 匹配单个数字周,如 "6周" 或 "6周(单)"
|
|
|
|
|
- const singleMatch = part.match(/^(\d+)周(?:\(([单双])\))?$/);
|
|
|
|
|
- if (singleMatch) {
|
|
|
|
|
- const num = parseInt(singleMatch[1]);
|
|
|
|
|
- const type = singleMatch[2];
|
|
|
|
|
- if (!type || (type === '单' && num % 2 === 1) || (type === '双' && num % 2 === 0)) {
|
|
|
|
|
- result.push(num);
|
|
|
|
|
- }
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 匹配范围周,如 "1-8周" 或 "2-6周(双)"
|
|
|
|
|
- const rangeMatch = part.match(/^(\d+)-(\d+)周(?:\(([单双])\))?$/);
|
|
|
|
|
- if (rangeMatch) {
|
|
|
|
|
- const start = parseInt(rangeMatch[1]);
|
|
|
|
|
- const end = parseInt(rangeMatch[2]);
|
|
|
|
|
- const type = rangeMatch[3];
|
|
|
|
|
-
|
|
|
|
|
- if (!type) {
|
|
|
|
|
- for (let i = start; i <= end; i++) result.push(i);
|
|
|
|
|
- } else if (type === '单') {
|
|
|
|
|
- for (let i = start; i <= end; i++) {
|
|
|
|
|
- if (i % 2 === 1) result.push(i);
|
|
|
|
|
- }
|
|
|
|
|
- } else if (type === '双') {
|
|
|
|
|
- for (let i = start; i <= end; i++) {
|
|
|
|
|
- if (i % 2 === 0) result.push(i);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- return [...new Set(result)].sort((a, b) => a - b);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 将API返回的课表原始数据(arrangedList)转换为标准课程对象数组
|
|
|
|
|
- * 新逻辑:直接从 titleDetail 解析课程名、周次、教师、地点
|
|
|
|
|
- * @param {Array} arrangedList API返回的课表列表
|
|
|
|
|
- * @returns {Array<object>} 课程对象,包含 name, teacher, position, day, startSection, endSection, weeks, isCustomTime(false)
|
|
|
|
|
|
|
+ * ============================================================
|
|
|
|
|
+ * 3. 网络请求层
|
|
|
|
|
+ * ============================================================
|
|
|
*/
|
|
*/
|
|
|
-function convertApiResponseToLessons(arrangedList) {
|
|
|
|
|
- const lessons = [];
|
|
|
|
|
- for (const item of arrangedList) {
|
|
|
|
|
- const day = item.dayOfWeek;
|
|
|
|
|
- const startSection = item.beginSection;
|
|
|
|
|
- const endSection = item.endSection;
|
|
|
|
|
- if (!day || !startSection || !endSection) continue;
|
|
|
|
|
-
|
|
|
|
|
- // ---- 1. 获取课程名 ----
|
|
|
|
|
- let name = item.courseName || "";
|
|
|
|
|
- // 如果 courseName 为空(极端情况),从 titleDetail 补全
|
|
|
|
|
- if (!name) {
|
|
|
|
|
- const td = item.titleDetail || [];
|
|
|
|
|
- if (td.length >= 2) {
|
|
|
|
|
- // 实验课 td[0] 通常以 "[实]" 开头,直接使用
|
|
|
|
|
- if (td[0].includes("[实]") || td[0].includes("实验")) {
|
|
|
|
|
- name = td[0];
|
|
|
|
|
- } else {
|
|
|
|
|
- // 理论课:取 td[1] 空格前的部分(如 "机器学习")
|
|
|
|
|
- const idx = td[1].indexOf(' ');
|
|
|
|
|
- name = idx !== -1 ? td[1].substring(0, idx) : td[1];
|
|
|
|
|
- }
|
|
|
|
|
- } else if (td.length === 1) {
|
|
|
|
|
- name = td[0];
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ---- 2. 获取周次-教师-地点字符串 ----
|
|
|
|
|
- let weekTeacherPlace = item.titleWeekTeacherClassroomDetail?.[0] || "";
|
|
|
|
|
- // 若该字段缺失,回退到 titleDetail 的对应项
|
|
|
|
|
- if (!weekTeacherPlace) {
|
|
|
|
|
- const td = item.titleDetail || [];
|
|
|
|
|
- if (td.length >= 3) weekTeacherPlace = td[2]; // 理论课
|
|
|
|
|
- else if (td.length === 2) weekTeacherPlace = td[1]; // 实验课
|
|
|
|
|
- }
|
|
|
|
|
- if (!weekTeacherPlace) continue; // 无有效信息则跳过
|
|
|
|
|
-
|
|
|
|
|
- // ---- 3. 解析 tokens ----
|
|
|
|
|
- const tokens = weekTeacherPlace.trim().split(/\s+/);
|
|
|
|
|
- if (tokens.length < 1) continue;
|
|
|
|
|
- const weeksStr = tokens[0]; // 如 "1-8周"
|
|
|
|
|
- const teacher = tokens[1] || "";
|
|
|
|
|
- const position = tokens.slice(2).join(' ');
|
|
|
|
|
-
|
|
|
|
|
- // ---- 4. 解析周次数组 ----
|
|
|
|
|
- const weeks = parseWeeksString(weeksStr);
|
|
|
|
|
- if (weeks.length === 0) {
|
|
|
|
|
- console.warn(`周次解析失败: ${weeksStr}, 课程: ${name}`);
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- lessons.push({
|
|
|
|
|
- name: name,
|
|
|
|
|
- teacher: teacher,
|
|
|
|
|
- position: position,
|
|
|
|
|
- day: day,
|
|
|
|
|
- startSection: startSection,
|
|
|
|
|
- endSection: endSection,
|
|
|
|
|
- weeks: weeks,
|
|
|
|
|
- isCustomTime: false
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- return lessons;
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 从教务API获取指定学期的课表数据(支持重试)
|
|
|
|
|
- * @param {string} semesterCode 学期代码,如 "2025-2026-1"
|
|
|
|
|
- * @param {number} retries 重试次数,默认2次
|
|
|
|
|
- * @returns {Promise<Array<object>>} 课程对象数组
|
|
|
|
|
- * @throws 网络或API错误
|
|
|
|
|
|
|
+ * 从教务API获取课表数据(支持重试)
|
|
|
*/
|
|
*/
|
|
|
-async function fetchCoursesFromAPI(semesterCode, retries=2) {
|
|
|
|
|
|
|
+async function fetchCoursesFromAPI(semesterCode, retries = 2) {
|
|
|
const url = 'https://jwxt.neu.edu.cn/jwapp/sys/kbapp/api/wdkbcx/getMyScheduleDetail.do';
|
|
const url = 'https://jwxt.neu.edu.cn/jwapp/sys/kbapp/api/wdkbcx/getMyScheduleDetail.do';
|
|
|
const xnxqdm = semesterCode;
|
|
const xnxqdm = semesterCode;
|
|
|
- const xqdm = '';//神秘参数,设为空就啥时候都能获得课表数据,尼东教务系统真是神了。
|
|
|
|
|
- for (let i=1; i<=retries; i++) {
|
|
|
|
|
|
|
+ const xqdm = ''; // 神秘参数,设为空可获取课表数据
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 1; i <= retries; i++) {
|
|
|
try {
|
|
try {
|
|
|
const ctrl = new AbortController();
|
|
const ctrl = new AbortController();
|
|
|
- const tid = setTimeout(()=>ctrl.abort(), 10000);
|
|
|
|
|
|
|
+ const tid = setTimeout(() => ctrl.abort(), 10000);
|
|
|
const res = await fetch(url, {
|
|
const res = await fetch(url, {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
- headers: { 'Fetch-Api':'true', 'Referer':'https://jwxt.neu.edu.cn/jwapp/sys/kbapp/home/index.html', 'User-Agent': navigator.userAgent, 'Accept':'application/json' },
|
|
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Fetch-Api': 'true',
|
|
|
|
|
+ 'Referer': 'https://jwxt.neu.edu.cn/jwapp/sys/kbapp/home/index.html',
|
|
|
|
|
+ 'User-Agent': navigator.userAgent,
|
|
|
|
|
+ 'Accept': 'application/json'
|
|
|
|
|
+ },
|
|
|
body: new URLSearchParams({ XNXQDM: xnxqdm, XQDM: xqdm }),
|
|
body: new URLSearchParams({ XNXQDM: xnxqdm, XQDM: xqdm }),
|
|
|
signal: ctrl.signal
|
|
signal: ctrl.signal
|
|
|
});
|
|
});
|
|
@@ -326,86 +312,175 @@ async function fetchCoursesFromAPI(semesterCode, retries=2) {
|
|
|
if (data.code !== '0') throw new Error(`API error ${data.code}`);
|
|
if (data.code !== '0') throw new Error(`API error ${data.code}`);
|
|
|
const list = data?.datas?.getMyScheduleDetail?.arrangedList || [];
|
|
const list = data?.datas?.getMyScheduleDetail?.arrangedList || [];
|
|
|
return convertApiResponseToLessons(list);
|
|
return convertApiResponseToLessons(list);
|
|
|
- } catch(e) {
|
|
|
|
|
- if (i===retries) throw e;
|
|
|
|
|
- await new Promise(r=>setTimeout(r,2000));
|
|
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ if (i === retries) throw e;
|
|
|
|
|
+ await new Promise(r => setTimeout(r, 2000));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 调用Android Bridge保存课程列表(覆盖写入)
|
|
|
|
|
- * @param {Array<object>} lessons 课程对象数组
|
|
|
|
|
|
|
+ * ============================================================
|
|
|
|
|
+ * 4. 数据保存层(使用桥接 API)
|
|
|
|
|
+ * ============================================================
|
|
|
*/
|
|
*/
|
|
|
-async function SaveCourses(lessons) {
|
|
|
|
|
- await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(lessons));
|
|
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 保存课程列表
|
|
|
|
|
+ */
|
|
|
|
|
+async function saveCourses(lessons) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(lessons));
|
|
|
|
|
+ window.shiguangBridge.showToast(`成功导入 ${lessons.length} 门课程`);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast(`保存课程失败: ${error.message}`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 根据校区导入预设的上下课时间表(节次时间)
|
|
|
|
|
- * @param {string} campus "南湖校区" 或 "浑南校区"
|
|
|
|
|
|
|
+ * 保存预设时间段(根据校区)
|
|
|
*/
|
|
*/
|
|
|
-async function importTimeSlotsByCampus(campus) {
|
|
|
|
|
- const hunNan = [{"number":1,"startTime":"08:30","endTime":"09:15"},{"number":2,"startTime":"09:25","endTime":"10:10"},{"number":3,"startTime":"10:30","endTime":"11:15"},{"number":4,"startTime":"11:25","endTime":"12:10"},{"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"},{"number":9,"startTime":"18:30","endTime":"19:15"},{"number":10,"startTime":"19:25","endTime":"20:10"},{"number":11,"startTime":"20:30","endTime":"21:15"},{"number":12,"startTime":"21:15","endTime":"22:10"}];
|
|
|
|
|
- const nanHu = [{"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: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"},{"number":9,"startTime":"18:30","endTime":"19:15"},{"number":10,"startTime":"19:25","endTime":"20:10"},{"number":11,"startTime":"20:20","endTime":"21:05"},{"number":12,"startTime":"21:15","endTime":"22:00"}];
|
|
|
|
|
|
|
+async function saveTimeSlots(campus) {
|
|
|
|
|
+ const hunNan = [
|
|
|
|
|
+ { "number": 1, "startTime": "08:30", "endTime": "09:15" },
|
|
|
|
|
+ { "number": 2, "startTime": "09:25", "endTime": "10:10" },
|
|
|
|
|
+ { "number": 3, "startTime": "10:30", "endTime": "11:15" },
|
|
|
|
|
+ { "number": 4, "startTime": "11:25", "endTime": "12:10" },
|
|
|
|
|
+ { "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" },
|
|
|
|
|
+ { "number": 9, "startTime": "18:30", "endTime": "19:15" },
|
|
|
|
|
+ { "number": 10, "startTime": "19:25", "endTime": "20:10" },
|
|
|
|
|
+ { "number": 11, "startTime": "20:30", "endTime": "21:15" },
|
|
|
|
|
+ { "number": 12, "startTime": "21:15", "endTime": "22:10" }
|
|
|
|
|
+ ];
|
|
|
|
|
+ const nanHu = [
|
|
|
|
|
+ { "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: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" },
|
|
|
|
|
+ { "number": 9, "startTime": "18:30", "endTime": "19:15" },
|
|
|
|
|
+ { "number": 10, "startTime": "19:25", "endTime": "20:10" },
|
|
|
|
|
+ { "number": 11, "startTime": "20:20", "endTime": "21:05" },
|
|
|
|
|
+ { "number": 12, "startTime": "21:15", "endTime": "22:00" }
|
|
|
|
|
+ ];
|
|
|
const slots = campus === "南湖校区" ? nanHu : hunNan;
|
|
const slots = campus === "南湖校区" ? nanHu : hunNan;
|
|
|
- await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(slots));
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(slots));
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast(`导入时间段失败: ${error.message}`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 保存课表全局配置(学期总周数、默认课时长度、课间休息、每周起始日)
|
|
|
|
|
|
|
+ * 保存课表配置(不包含 semesterStartDate)
|
|
|
*/
|
|
*/
|
|
|
-async function SaveConfig() {
|
|
|
|
|
- const cfg = { semesterTotalWeeks:18, defaultClassDuration:45, defaultBreakDuration:10, firstDayOfWeek:7 };
|
|
|
|
|
- await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(cfg));
|
|
|
|
|
|
|
+async function saveConfig() {
|
|
|
|
|
+ const config = {
|
|
|
|
|
+ semesterTotalWeeks: 18,
|
|
|
|
|
+ defaultClassDuration: 45,
|
|
|
|
|
+ defaultBreakDuration: 10,
|
|
|
|
|
+ firstDayOfWeek: 7
|
|
|
|
|
+ };
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast(`保存配置失败: ${error.message}`);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 主流程:依次选择校区、学期,获取课表,保存,可选导入考试并合并保存
|
|
|
|
|
- * 最后通知Android任务完成
|
|
|
|
|
|
|
+ * ============================================================
|
|
|
|
|
+ * 5. 主流程(符合规范的结构化编排)
|
|
|
|
|
+ * ============================================================
|
|
|
*/
|
|
*/
|
|
|
-async function runAllDemosSequentially() {
|
|
|
|
|
|
|
+
|
|
|
|
|
+async function runImportFlow() {
|
|
|
window.shiguangBridge.showToast("开始导入课表...");
|
|
window.shiguangBridge.showToast("开始导入课表...");
|
|
|
- const campus = await showCampusSelection();
|
|
|
|
|
- if (!campus) { window.shiguangBridge.showToast("已取消导入"); return; }
|
|
|
|
|
- const semester = await showSemesterSelection();
|
|
|
|
|
- if (!semester) { window.shiguangBridge.showToast("已取消导入"); return; }
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 公告确认
|
|
|
|
|
+ const alertConfirmed = await showStartAlert();
|
|
|
|
|
+ if (!alertConfirmed) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 获取学年
|
|
|
|
|
+ const academicYear = await getAcademicYear();
|
|
|
|
|
+ if (academicYear === null) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 选择学期
|
|
|
|
|
+ const semesterResult = await selectSemester();
|
|
|
|
|
+ if (semesterResult === null) return;
|
|
|
|
|
+ const { termType } = semesterResult;
|
|
|
|
|
+ const startYear = parseInt(academicYear, 10);
|
|
|
|
|
+ const endYear = startYear + 1;
|
|
|
|
|
+ const semesterCode = `${startYear}-${endYear}-${termType}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 选择校区
|
|
|
|
|
+ const campus = await selectCampus();
|
|
|
|
|
+ if (campus === null) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 获取课表数据
|
|
|
window.shiguangBridge.showToast("正在获取课表数据...");
|
|
window.shiguangBridge.showToast("正在获取课表数据...");
|
|
|
let lessons;
|
|
let lessons;
|
|
|
try {
|
|
try {
|
|
|
- lessons = await fetchCoursesFromAPI(semester);
|
|
|
|
|
- if (!lessons.length) { window.shiguangBridge.showToast("未获取到任何课程"); return; }
|
|
|
|
|
- console.log(`获取到 ${lessons.length} 门课程`);
|
|
|
|
|
- } catch(e) {
|
|
|
|
|
- window.shiguangBridge.showToast("获取课表失败: "+e.message);
|
|
|
|
|
|
|
+ lessons = await fetchCoursesFromAPI(semesterCode);
|
|
|
|
|
+ if (!lessons.length) {
|
|
|
|
|
+ window.shiguangBridge.showToast("未获取到任何课程");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ console.log(`获取到 ${lessons.length} 条课程内容`);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ window.shiguangBridge.showToast("获取课表失败: " + e.message);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- await SaveCourses(lessons);
|
|
|
|
|
- await importTimeSlotsByCampus(campus);
|
|
|
|
|
- await SaveConfig();
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 保存课程
|
|
|
|
|
+ const saveResult = await saveCourses(lessons);
|
|
|
|
|
+ if (!saveResult) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 保存时间段
|
|
|
|
|
+ await saveTimeSlots(campus);
|
|
|
|
|
+
|
|
|
|
|
+ // 8. 保存配置(不包含 semesterStartDate)
|
|
|
|
|
+ const configResult = await saveConfig();
|
|
|
|
|
+ if (!configResult) return;
|
|
|
|
|
+
|
|
|
window.shiguangBridge.showToast("课表导入完成!");
|
|
window.shiguangBridge.showToast("课表导入完成!");
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 9. [可选] 导入考试时间
|
|
|
const importExams = await askImportExams();
|
|
const importExams = await askImportExams();
|
|
|
if (importExams) {
|
|
if (importExams) {
|
|
|
window.shiguangBridge.showToast("正在获取考试数据...");
|
|
window.shiguangBridge.showToast("正在获取考试数据...");
|
|
|
try {
|
|
try {
|
|
|
- const examLessons = await fetchExamsFromAPI(semester);
|
|
|
|
|
|
|
+ const examLessons = await fetchExamsFromAPI(semesterCode);
|
|
|
if (examLessons.length === 0) {
|
|
if (examLessons.length === 0) {
|
|
|
window.shiguangBridge.showToast("未获取到考试数据");
|
|
window.shiguangBridge.showToast("未获取到考试数据");
|
|
|
} else {
|
|
} else {
|
|
|
const allLessons = [...lessons, ...examLessons];
|
|
const allLessons = [...lessons, ...examLessons];
|
|
|
- await SaveCourses(allLessons);
|
|
|
|
|
- window.shiguangBridge.showToast(`已导入 ${examLessons.length} 条考试记录(合并至课表)`);
|
|
|
|
|
|
|
+ await saveCourses(allLessons);
|
|
|
|
|
+ window.shiguangBridge.showToast(`已导入 ${examLessons.length} 条考试记录`);
|
|
|
}
|
|
}
|
|
|
- } catch(e) {
|
|
|
|
|
- window.shiguangBridge.showToast("导入考试失败: "+e.message);
|
|
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入考试失败: " + e.message);
|
|
|
console.error(e);
|
|
console.error(e);
|
|
|
|
|
+ // 考试导入失败不影响主流程完成
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 10. 流程完全成功,发送结束信号
|
|
|
|
|
+ window.shiguangBridge.showToast("所有任务已完成!");
|
|
|
window.shiguangBridge.notifyTaskCompletion();
|
|
window.shiguangBridge.notifyTaskCompletion();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 启动主流程
|
|
|
|
|
-runAllDemosSequentially();
|
|
|
|
|
|
|
+// 启动导入流程
|
|
|
|
|
+runImportFlow();
|