Przeglądaj źródła

Merge pull request #602 from XingHeYuZhuan/pending

星河欲转 2 tygodni temu
rodzic
commit
ab6e49d958

+ 5 - 0
index/root_index.yaml

@@ -563,6 +563,11 @@ schools:
     initial: "N"
     resource_folder: "NJTECH"
 
+  - id: "NUIST"
+    name: "南京信息工程大学"
+    initial: "N"
+    resource_folder: "NUIST"
+
   - id: "CAUC"
     name: "中国民航大学"
     initial: "Z"

+ 5 - 7
resources/CMC/cmc_01.js

@@ -15,16 +15,14 @@ function parseWeeks(weekStr) {
 // 解析按周场地字符串
 function parseVenueWeeks(jxcdmc2) {
     const venueMap = new Map();
-    let lastRoom = null;
     String(jxcdmc2 || "").split(",").forEach(part => {
         const match = part.trim().match(/^(.*?)-(\d+)$/);
         if (!match) return;
-        const room = match[1].trim();
         const week = parseInt(match[2], 10);
-        if (room) lastRoom = room;
-        if (!lastRoom || isNaN(week)) return;
-        if (!venueMap.has(lastRoom)) venueMap.set(lastRoom, []);
-        venueMap.get(lastRoom).push(week);
+        if (isNaN(week)) return;
+        const key = match[1].trim() || "不用场地";
+        if (!venueMap.has(key)) venueMap.set(key, []);
+        venueMap.get(key).push(week);
     });
     return venueMap;
 }
@@ -38,7 +36,7 @@ function resolvePosition(item) {
 }
 
 function cleanTeacherName(raw) {
-    return String(raw || "").replace(/\[[^\]]*\]/g, "").trim();
+    return [...new Set(String(raw || "").replace(/\[[^\]]*\]/g, "").split(",").map(n => n.trim()).filter(Boolean))].join(",");
 }
 
 // 课表接口数据

+ 334 - 259
resources/NEU/neu.js

@@ -1,113 +1,208 @@
 // 文件: 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 = ["南湖校区", "浑南校区"];
-    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-不导入
  */
 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) {
     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) {
     const url = `https://jwxt.neu.edu.cn/jwapp/sys/homeapp/api/home/student/exams.do?termCode=${encodeURIComponent(termCode)}`;
     const response = await fetch(url, {
         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}`);
     const data = await response.json();
@@ -165,7 +261,6 @@ async function fetchExamsFromAPI(termCode) {
             console.warn("解析考试时间失败,跳过:", desc);
             continue;
         }
-        const weeks = [15];  // 考试固定在第15周(测试功能)
         lessons.push({
             name: name,
             teacher: teacher,
@@ -173,7 +268,7 @@ async function fetchExamsFromAPI(termCode) {
             day: day,
             startSection: undefined,
             endSection: undefined,
-            weeks: weeks,
+            weeks: [15],
             isCustomTime: true,
             customStartTime: startTime,
             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 xnxqdm = semesterCode;
-    const xqdm = '';//神秘参数,设为空就啥时候都能获得课表数据,尼东教务系统真是神了。
-    for (let i=1; i<=retries; i++) {
+    const xqdm = ''; // 神秘参数,设为空可获取课表数据
+
+    for (let i = 1; i <= retries; i++) {
         try {
             const ctrl = new AbortController();
-            const tid = setTimeout(()=>ctrl.abort(), 10000);
+            const tid = setTimeout(() => ctrl.abort(), 10000);
             const res = await fetch(url, {
                 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 }),
                 signal: ctrl.signal
             });
@@ -326,86 +312,175 @@ async function fetchCoursesFromAPI(semesterCode, retries=2) {
             if (data.code !== '0') throw new Error(`API error ${data.code}`);
             const list = data?.datas?.getMyScheduleDetail?.arrangedList || [];
             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;
-    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("开始导入课表...");
-    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("正在获取课表数据...");
     let lessons;
     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;
     }
-    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("课表导入完成!");
-    
+
+    // 9. [可选] 导入考试时间
     const importExams = await askImportExams();
     if (importExams) {
         window.shiguangBridge.showToast("正在获取考试数据...");
         try {
-            const examLessons = await fetchExamsFromAPI(semester);
+            const examLessons = await fetchExamsFromAPI(semesterCode);
             if (examLessons.length === 0) {
                 window.shiguangBridge.showToast("未获取到考试数据");
             } else {
                 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);
+            // 考试导入失败不影响主流程完成
         }
     }
-    
+
+    // 10. 流程完全成功,发送结束信号
+    window.shiguangBridge.showToast("所有任务已完成!");
     window.shiguangBridge.notifyTaskCompletion();
 }
 
-// 启动流程
-runAllDemosSequentially();
+// 启动导入流程
+runImportFlow();

+ 8 - 0
resources/NUIST/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "NUIST_01"
+    adapter_name: "南京信息工程大学教务课表"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "nuist.js"
+    import_url: "https://jwxt.nuist.edu.cn/"
+    maintainer: "社区贡献者"
+    description: "南京信息工程大学金智教务系统课表适配"

+ 263 - 0
resources/NUIST/nuist.js

@@ -0,0 +1,263 @@
+// 南京信息工程大学教务系统(jwxt.nuist.edu.cn)拾光课程表适配脚本
+
+function parseWeeks(skzc) {
+    const value = String(skzc || "");
+    const weeks = [];
+    for (let i = 0; i < value.length; i++) {
+        if (value[i] === "1") weeks.push(i + 1);
+    }
+    return weeks;
+}
+
+// jc.do 不可用时的 NUIST 默认作息(已由接口确认)。
+const DEFAULT_TIME_SLOTS = [
+    { number: 1, startTime: "08:00", endTime: "08:45" },
+    { number: 2, startTime: "08:55", endTime: "09:40" },
+    { number: 3, startTime: "10:10", endTime: "10:55" },
+    { number: 4, startTime: "11:05", endTime: "11:50" },
+    { number: 5, startTime: "13:45", endTime: "14:30" },
+    { number: 6, startTime: "14:40", endTime: "15:25" },
+    { number: 7, startTime: "15:55", endTime: "16:40" },
+    { number: 8, startTime: "16:50", endTime: "17:35" },
+    { number: 9, startTime: "18:45", endTime: "19:30" },
+    { number: 10, startTime: "19:40", endTime: "20:25" },
+    { number: 11, startTime: "20:35", endTime: "21:20" },
+    { number: 12, startTime: "21:25", endTime: "22:00" }
+];
+
+function getErrorMessage(error) {
+    if (error && typeof error.message === "string" && error.message.trim()) return error.message;
+    if (typeof error === "string" && error.trim()) return error;
+    try {
+        const serialized = JSON.stringify(error);
+        if (serialized && serialized !== "{}") return serialized;
+    } catch (_) {
+        // Ignore serialization failures and use the generic fallback below.
+    }
+    return "未知错误";
+}
+
+function parsePosition(row) {
+    const campus = String(row.XXXQDM_DISPLAY || "").trim();
+    const room = String(row.JASMC || "").trim();
+    if (campus && room) return `${room}(${campus})`;
+    return room || campus || "待定";
+}
+
+function parseCourse(row) {
+    const day = Number(row.SKXQ);
+    const startSection = Number(row.KSJC);
+    const endSection = Number(row.JSJC);
+    const weeks = parseWeeks(row.SKZC);
+    if (!row.KCM || !day || day < 1 || day > 7 || !startSection || !endSection ||
+        startSection > endSection || weeks.length === 0) return null;
+
+    return {
+        name: String(row.KCM).trim(),
+        teacher: String(row.SKJS || "").split(/[\\/、,,]/)[0].trim() || "未知",
+        position: parsePosition(row),
+        day,
+        startSection,
+        endSection,
+        weeks
+    };
+}
+
+async function fetchCourses(xnxqdm) {
+    const response = await fetch("/jwapp/sys/wdkb/modules/xskcb/cxxszhxqkb.do", {
+        method: "POST",
+        headers: {
+            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
+            "X-Requested-With": "XMLHttpRequest"
+        },
+        body: `XNXQDM=${encodeURIComponent(xnxqdm)}`,
+        credentials: "include"
+    });
+    if (!response.ok) throw new Error(`课表接口请求失败(HTTP ${response.status})`);
+
+    const payload = await response.json();
+    const table = payload && payload.datas && payload.datas.cxxszhxqkb;
+    if (!table) throw new Error("教务系统返回数据格式异常。");
+    if (table.extParams && Number(table.extParams.code) !== 1) {
+        throw new Error(table.extParams.msg || "教务系统未发布该学期课表。");
+    }
+
+    const rows = Array.isArray(table.rows) ? table.rows : [];
+    const courses = rows.map(parseCourse).filter(Boolean);
+    if (courses.length === 0) throw new Error("未找到包含有效时间的课程。");
+    courses.sort((a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name));
+    return { courses, xnxqdm };
+}
+
+async function fetchCurrentSemester() {
+    const response = await fetch("/jwapp/sys/wdkb/modules/jshkcb/dqxnxq.do", {
+        method: "POST",
+        headers: {
+            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
+            "X-Requested-With": "XMLHttpRequest"
+        },
+        body: "",
+        credentials: "include"
+    });
+    if (!response.ok) throw new Error(`当前学期接口请求失败(HTTP ${response.status})`);
+    const payload = await response.json();
+    const rows = payload && payload.datas && payload.datas.dqxnxq && payload.datas.dqxnxq.rows;
+    const term = Array.isArray(rows) && rows.length > 0 ? rows[0] : null;
+    if (!term || !term.DM) throw new Error("未获取到当前学期信息。");
+    return {
+        code: String(term.DM),
+        name: String(term.MC || term.DM)
+    };
+}
+
+async function fetchSemesterList() {
+    const response = await fetch("/jwapp/sys/wdkb/modules/jshkcb/xnxqcx.do", {
+        method: "POST",
+        headers: {
+            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
+            "X-Requested-With": "XMLHttpRequest"
+        },
+        body: "*order=-DM",
+        credentials: "include"
+    });
+    if (!response.ok) throw new Error(`学期列表接口请求失败(HTTP ${response.status})`);
+    const payload = await response.json();
+    const rows = payload && payload.datas && payload.datas.xnxqcx && payload.datas.xnxqcx.rows;
+    if (!Array.isArray(rows)) throw new Error("学期列表返回数据格式异常。");
+    return rows.filter(item => item && item.DM).map(item => ({
+        code: String(item.DM),
+        name: String(item.MC || item.DM),
+        year: item.XNDM,
+        term: item.XQDM
+    }));
+}
+
+async function selectSemester() {
+    let semesters;
+    try {
+        semesters = await fetchSemesterList();
+    } catch (error) {
+        window.shiguangBridge.showToast(`获取学期列表失败:${getErrorMessage(error)}`);
+        return null;
+    }
+    if (semesters.length === 0) {
+        window.shiguangBridge.showToast("未查询到可用学期。");
+        return null;
+    }
+
+    let currentCode = null;
+    try {
+        currentCode = (await fetchCurrentSemester()).code;
+    } catch (error) {
+        console.warn("获取当前学期失败,将不设置默认选项", error);
+    }
+
+    const visibleSemesters = semesters.slice(0, 10);
+    const defaultIndex = currentCode
+        ? visibleSemesters.findIndex(item => item.code === currentCode)
+        : -1;
+    const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
+        "请选择学期",
+        JSON.stringify(visibleSemesters.map(item => item.name)),
+        defaultIndex
+    );
+    if (selectedIndex === null || selectedIndex < 0 || !visibleSemesters[selectedIndex]) return null;
+    return visibleSemesters[selectedIndex];
+}
+
+async function fetchTimeSlots() {
+    const response = await fetch("/jwapp/sys/wdkb/modules/jshkcb/jc.do", {
+        method: "POST",
+        headers: {
+            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
+            "X-Requested-With": "XMLHttpRequest"
+        },
+        body: "",
+        credentials: "include"
+    });
+    if (!response.ok) throw new Error(`节次时间接口请求失败(HTTP ${response.status})`);
+    const payload = await response.json();
+    const rows = payload && payload.datas && payload.datas.jc && payload.datas.jc.rows;
+    if (!Array.isArray(rows) || rows.length === 0) return [];
+    return rows.map(row => ({
+        number: Number(row.DM),
+        startTime: String(row.KSSJ || ""),
+        endTime: String(row.JSSJ || "")
+    })).filter(slot => slot.number > 0 && slot.startTime && slot.endTime)
+      .sort((a, b) => a.number - b.number);
+}
+
+async function fetchSemesterConfig(xnxqdm) {
+    const parts = String(xnxqdm || "").split("-");
+    if (parts.length < 3) return { semesterStartDate: null, semesterTotalWeeks: 20 };
+    const response = await fetch("/jwapp/sys/wdkb/modules/jshkcb/cxjcs.do", {
+        method: "POST",
+        headers: {
+            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
+            "X-Requested-With": "XMLHttpRequest"
+        },
+        body: `XN=${encodeURIComponent(`${parts[0]}-${parts[1]}`)}&XQ=${encodeURIComponent(parts[2])}`,
+        credentials: "include"
+    });
+    if (!response.ok) throw new Error(`学期配置接口请求失败(HTTP ${response.status})`);
+    const payload = await response.json();
+    const rows = payload && payload.datas && payload.datas.cxjcs && payload.datas.cxjcs.rows;
+    const row = Array.isArray(rows) ? rows[0] : null;
+    if (!row) return { semesterStartDate: null, semesterTotalWeeks: 20 };
+    const rawDate = row.XQKSRQ || row.XQKSRQ_DISPLAY || row.KSRQ;
+    const startDate = rawDate ? String(rawDate).split(/[ T]/)[0] : null;
+    const totalWeeks = Number.parseInt(row.ZZC || row.ZCZ || row.ZC, 10);
+    return {
+        semesterStartDate: /^\d{4}-\d{2}-\d{2}$/.test(startDate || "") ? startDate : null,
+        semesterTotalWeeks: Number.isFinite(totalWeeks) && totalWeeks > 0 ? totalWeeks : 20
+    };
+}
+
+async function runImportFlow() {
+    try {
+        const confirmed = await window.shiguangBridgePromise.showAlert(
+            "南京信息工程大学课表导入",
+            "请先在当前教务页面完成登录,再开始导入。",
+            "开始导入"
+        );
+        if (!confirmed) return;
+
+        const semester = await selectSemester();
+        if (!semester) {
+            window.shiguangBridge.showToast("导入已取消。");
+            return;
+        }
+        window.shiguangBridge.showToast(`已选择学期:${semester.name}`);
+        const result = await fetchCourses(semester.code);
+        let semesterConfig = { semesterStartDate: null, semesterTotalWeeks: 20 };
+        try {
+            semesterConfig = await fetchSemesterConfig(semester.code);
+        } catch (error) {
+            console.warn("获取学期配置失败,将使用默认配置", error);
+            window.shiguangBridge.showToast("开学日期获取失败,已使用默认配置继续导入。");
+        }
+        let timeSlots = DEFAULT_TIME_SLOTS;
+        try {
+            const fetchedTimeSlots = await fetchTimeSlots();
+            if (fetchedTimeSlots.length > 0) timeSlots = fetchedTimeSlots;
+        } catch (error) {
+            console.warn("NUIST time slot request failed, using defaults", error);
+            window.shiguangBridge.showToast("作息时间获取失败,已使用默认时间继续导入。");
+        }
+        await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
+            ...semesterConfig,
+            defaultClassDuration: 45,
+            defaultBreakDuration: 10,
+            firstDayOfWeek: 1
+        }));
+        await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(result.courses));
+        await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+        window.shiguangBridge.showToast(`成功导入 ${result.courses.length} 条课程记录(${result.xnxqdm})。`);
+        window.shiguangBridge.notifyTaskCompletion();
+    } catch (error) {
+        window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
+        console.error("NUIST adapter error", error);
+    }
+}
+
+runImportFlow();