| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- // 文件: HUAT.js - 湖北汽车工业学院课表导入(基于教务系统 API)
- // 数据源:
- // /api/semester/list → 可选学期列表(用于学期选择 + 学期基本信息)
- // /api/teachTask/list → 教学任务列表(按 yearTermId 过滤)
- // ==================== 验证函数 ====================
- function validateDate(dateStr) {
- if (!dateStr || dateStr.trim().length === 0) {
- return "日期不能为空!";
- }
- const datePattern = /^\d{4}-\d{2}-\d{2}$/;
- if (!datePattern.test(dateStr)) {
- return "日期格式必须是 YYYY-MM-DD!";
- }
- const parts = dateStr.split('-');
- const year = parseInt(parts[0]);
- const month = parseInt(parts[1]);
- const day = parseInt(parts[2]);
- const date = new Date(year, month - 1, day);
- if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
- return "请输入有效的日期!";
- }
- return false;
- }
- // ==================== API 数据获取 ====================
- // 用当前页面协议构造 API 基址,避免 HTTP/HTTPS 跨域
- // (http:// 和 https:// 在 CORS 看来是不同 origin,会触发预检并失败)
- const API_BASE = window.location.protocol + '//neweas.huat.edu.cn';
- /**
- * 获取认证 token(X-Token 头的值)
- * 优先级:localStorage / sessionStorage 常见 key → Admin-Token cookie → window 全局
- */
- function getAuthToken() {
- const storageKeys = ['Admin-Token', 'adminToken', 'token', 'X-Token', 'x-token', 'admin_token'];
- for (const key of storageKeys) {
- const v = localStorage.getItem(key) || sessionStorage.getItem(key);
- if (v) return v;
- }
- // 从 cookie 读 Admin-Token
- const cookieMatch = document.cookie.match(/Admin-Token=([^;]+)/);
- if (cookieMatch) return cookieMatch[1];
- // window 全局
- if (window.token) return window.token;
- if (window.userToken) return window.userToken;
- return null;
- }
- /**
- * 解析 JWT payload(仅解码,不验签)
- */
- function decodeJwtPayload(jwt) {
- if (!jwt) return null;
- const parts = jwt.split('.');
- if (parts.length !== 3) return null;
- try {
- const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
- const json = decodeURIComponent(atob(b64).split('').map(c =>
- '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
- ).join(''));
- return JSON.parse(json);
- } catch (e) {
- return null;
- }
- }
- /**
- * 从 JWT 的 sub 字段提取学生 ID
- */
- function getStudentId() {
- const token = getAuthToken();
- if (!token) return null;
- const payload = decodeJwtPayload(token);
- return (payload && payload.sub) || null;
- }
- /**
- * 诊断信息:token 来源 + 预览 + 当前页面 URL
- * 用于手机端排查认证失败问题(手机端通常没 DevTools 可看)
- */
- function getTokenDiagnostic() {
- const storageKeys = ['Admin-Token', 'adminToken', 'token', 'X-Token', 'x-token', 'admin_token'];
- let source = '未找到';
- let token = null;
- for (const key of storageKeys) {
- const v = localStorage.getItem(key);
- if (v) { token = v; source = 'localStorage[' + key + ']'; break; }
- }
- if (!token) {
- for (const key of storageKeys) {
- const v = sessionStorage.getItem(key);
- if (v) { token = v; source = 'sessionStorage[' + key + ']'; break; }
- }
- }
- if (!token) {
- const m = document.cookie.match(/Admin-Token=([^;]+)/);
- if (m) { token = m[1]; source = 'cookie[Admin-Token]'; }
- }
- if (!token && window.token) { token = window.token; source = 'window.token'; }
- if (!token && window.userToken) { token = window.userToken; source = 'window.userToken'; }
- const preview = token
- ? (token.length > 24 ? token.slice(0, 12) + '...' + token.slice(-8) : token)
- : 'null';
- return 'token=' + preview + ' (长度=' + (token ? token.length : 0) + ', source=' + source + ')\nURL=' + window.location.href;
- }
- async function fetchSemesterList() {
- console.log("拉取学期列表...");
- const token = getAuthToken();
- if (!token) {
- throw new Error('无法获取认证 token\n\n诊断:\n' + getTokenDiagnostic());
- }
- // cURL 抓包:POST 无 body(Content-Length: 0),无 Content-Type
- let res;
- try {
- res = await fetch(API_BASE + '/api/semester/list', {
- method: 'POST',
- credentials: 'include',
- headers: {
- 'Accept': 'application/json, text/plain, */*',
- 'X-Token': token
- }
- });
- } catch (fetchErr) {
- // 网络层失败:CORS、URL 不可达、SSL 错误等
- throw new Error('fetch 失败: ' + fetchErr.message + '\n\n诊断:\n' + getTokenDiagnostic());
- }
- if (!res.ok) {
- throw new Error('semester/list HTTP ' + res.status + '\n\n诊断:\n' + getTokenDiagnostic());
- }
- const json = await res.json();
- if (json.code !== 200) {
- throw new Error('semester/list 返回错误 [code=' + json.code + ']: ' + json.msg + '\n\n诊断:\n' + getTokenDiagnostic());
- }
- return json.data || [];
- }
- async function fetchTeachTaskList(yearTermId) {
- console.log("拉取教学任务... yearTermId=" + (yearTermId || '(未指定)'));
- const token = getAuthToken();
- if (!token) {
- throw new Error('无法获取认证 token,请确认已登录教务系统');
- }
- const studId = getStudentId() || '';
- // body 格式来自 cURL 抓包:{"weekNum":"","yearTermId":"20252","studId":"202402417"}
- let res;
- try {
- res = await fetch(API_BASE + '/api/teachTask/list', {
- method: 'POST',
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Token': token,
- 'X-Requested-With': 'XMLHttpRequest'
- },
- body: JSON.stringify({
- weekNum: "",
- yearTermId: yearTermId || "",
- studId: studId
- })
- });
- } catch (fetchErr) {
- throw new Error('fetch 失败: ' + fetchErr.message + '\n\n诊断:\n' + getTokenDiagnostic());
- }
- if (!res.ok) {
- throw new Error('teachTask/list HTTP ' + res.status);
- }
- const json = await res.json();
- if (json.code !== 200) {
- throw new Error('teachTask/list 返回错误 [code=' + json.code + ']: ' + json.msg);
- }
- return json.data || [];
- }
- // ==================== 数据转换 ====================
- /**
- * 把选中的学期对象转成内部用的 semesterInfo
- * semesterStartDate 来自学期 beginDate
- * semesterTotalWeeks 来自 beginWeek..endWeek
- */
- function buildSemesterInfoFromSemester(semester) {
- if (!semester) {
- return { semesterStartDate: '2026-09-07', semesterTotalWeeks: 25, yearTermId: null };
- }
- const beginWeek = semester.beginWeek || 1;
- const endWeek = semester.endWeek || 25;
- return {
- semesterStartDate: semester.beginDate,
- semesterTotalWeeks: endWeek - beginWeek + 1,
- yearTermId: semester.yearTermID
- };
- }
- /**
- * 根据 lessType 把 weekBegin..weekEnd 展开为周次数组
- * "全周" → 所有周
- * "单周" → 仅奇数周
- * "双周" → 仅偶数周
- */
- function buildWeeksArray(weekBegin, weekEnd, lessType) {
- const weeks = [];
- const start = weekBegin || 1;
- const end = weekEnd || start;
- for (let w = start; w <= end; w++) {
- if (lessType === '单周' && w % 2 === 0) continue;
- if (lessType === '双周' && w % 2 === 1) continue;
- weeks.push(w);
- }
- return weeks;
- }
- /**
- * 把 teachTask/list 返回的数组转换成 saveImportedCourses 所需格式
- * 注意:day 直接用 API 的 weekDay(1=周一 ... 7=周日)
- * 若发现课程排错星期,改成 task.weekDay - 1(0=周一)即可
- */
- function transformTeachTaskToCourses(taskList) {
- const courses = [];
- (taskList || []).forEach(task => {
- // 教师名拼接(多个教师用「、」连接)
- const teachers = (task.teacherList || [])
- .map(t => t.teacherName)
- .filter(name => name && String(name).trim().length > 0);
- const teacher = teachers.length > 0 ? teachers.join('、') : '未知教师';
- const course = {
- name: task.courName || '未知课程',
- teacher: teacher,
- position: task.roomId ? String(task.roomId) : '未知教室',
- day: task.weekDay, // API: 1=周一 ... 7=周日
- startSection: task.startSection,
- endSection: task.endSection,
- weeks: buildWeeksArray(task.weekBegin, task.weekEnd, task.lessType),
- isCustomTime: false
- };
- courses.push(course);
- });
- return courses;
- }
- // ========== 根据季节获取时间段(夏季/秋季) ==========
- /**
- * 根据季节返回对应的时间段数组
- * @param {string} season - "summer" 或 "autumn"
- * @returns {Array} 时间段对象数组
- */
- function getSeasonTimeSlots(season) {
- // 上午固定时间(夏秋通用)
- const morning_classes = [
- {"number": 1, "startTime": "08:10", "endTime": "08:55"},
- {"number": 2, "startTime": "09:00", "endTime": "09:45"},
- {"number": 3, "startTime": "10:05", "endTime": "10:50"},
- {"number": 4, "startTime": "10:55", "endTime": "11:40"}
- ];
- // 夏季下午/晚上时间
- const summer_afternoon_evening = [
- {"number": 5, "startTime": "14:30", "endTime": "15:15"},
- {"number": 6, "startTime": "15:20", "endTime": "16:05"},
- {"number": 7, "startTime": "16:25", "endTime": "17:10"},
- {"number": 8, "startTime": "17:15", "endTime": "18:00"},
- {"number": 9, "startTime": "18:45", "endTime": "19:30"},
- {"number": 10, "startTime": "19:35", "endTime": "20:20"},
- {"number": 11, "startTime": "20:25", "endTime": "21:10"}
- ];
- // 秋季下午/晚上时间
- const autumn_afternoon_evening = [
- {"number": 5, "startTime": "14:00", "endTime": "14:45"},
- {"number": 6, "startTime": "14:50", "endTime": "15:35"},
- {"number": 7, "startTime": "15:55", "endTime": "16:40"},
- {"number": 8, "startTime": "16:45", "endTime": "17:30"},
- {"number": 9, "startTime": "18:15", "endTime": "19:00"},
- {"number": 10, "startTime": "19:05", "endTime": "19:50"},
- {"number": 11, "startTime": "19:55", "endTime": "20:40"}
- ];
- if (season === 'autumn') {
- return morning_classes.concat(autumn_afternoon_evening);
- }
- // 默认夏季(兼容旧调用)
- return morning_classes.concat(summer_afternoon_evening);
- }
- // ==================== 弹窗和导入函数 ====================
- async function demoAlert() {
- try {
- const confirmed = await window.shiguangBridgePromise.showAlert(
- "📚 湖北汽车工业学院课表导入",
- "将通过教务系统 API 拉取课表数据并导入到 App\n\n" +
- "📌 请确认已登录教务系统\n" +
- "📌 流程:选择学期 → 选择季节 → 拉取课程",
- "开始导入",
- "取消"
- );
- return confirmed;
- } catch (error) {
- console.error("显示弹窗错误:", error);
- return false;
- }
- }
- async function demoPrompt(semesterInfo) {
- try {
- const defaultDate = (semesterInfo && semesterInfo.semesterStartDate) || '2026-09-07';
- const semesterStart = await window.shiguangBridgePromise.showPrompt(
- "📅 设置开学日期",
- "已从教务系统获取开学日期,如需修改请直接编辑",
- defaultDate,
- "validateDate"
- );
- return semesterStart || defaultDate;
- } catch (error) {
- console.error("日期输入错误:", error);
- return "2026-09-07";
- }
- }
- /**
- * 学期选择 - 用单选列表让用户选学期
- * 默认选中 currYearTerm === 1 的当前学期
- */
- async function pickSemester(semesterList) {
- if (!semesterList || semesterList.length === 0) return null;
- const names = semesterList.map(s => s.yearTermName || s.yearTermID);
- // 默认选中当前学期(currYearTerm === 1)
- let defaultIdx = semesterList.findIndex(s => s.currYearTerm === 1);
- if (defaultIdx < 0) defaultIdx = 0;
- try {
- console.log("即将显示学期选择弹窗...");
- const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
- "选择要导入的学期",
- JSON.stringify(names),
- defaultIdx
- );
- if (selectedIndex !== null && selectedIndex >= 0 && selectedIndex < semesterList.length) {
- const picked = semesterList[selectedIndex];
- console.log("用户选择了: " + picked.yearTermName + " (yearTermID=" + picked.yearTermID + ")");
- window.shiguangBridge.showToast("已选 " + picked.yearTermName);
- return picked;
- }
- console.log("用户取消了学期选择。");
- window.shiguangBridge.showToast("学期选择:用户取消");
- return null;
- } catch (error) {
- console.error("显示学期选择弹窗出错:", error);
- window.shiguangBridge.showToast("学期选择出错:" + error.message);
- return null;
- }
- }
- /**
- * 导入时间段 - 使用单选列表弹窗让用户选择夏季或秋季
- * 参考 demoSingleSelection 的调用模式
- */
- async function importPresetTimeSlots() {
- const seasons = ["夏季", "秋季"];
- try {
- console.log("即将显示季节选择单选列表弹窗...");
- const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
- "选择作息时间",
- JSON.stringify(seasons),
- 0
- );
- if (selectedIndex !== null && selectedIndex >= 0 && selectedIndex < seasons.length) {
- console.log("用户选择了: " + seasons[selectedIndex] + " (索引: " + selectedIndex + ")");
- window.shiguangBridge.showToast("你选择了 " + seasons[selectedIndex]);
- // 索引 0 -> 夏季,索引 1 -> 秋季
- const season = selectedIndex === 0 ? 'summer' : 'autumn';
- // 获取对应季节的时间段
- const presetTimeSlots = getSeasonTimeSlots(season);
- console.log(`选择的季节: ${season},时间段:`, presetTimeSlots);
- // 导入时间段
- const result = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
- if (result === true) {
- console.log("预设时间段导入成功!");
- window.shiguangBridge.showToast(`✅ ${seasons[selectedIndex]}时间段导入成功!`);
- return true; // 成功时返回 true
- } else {
- console.log("预设时间段导入未成功,结果:" + result);
- window.shiguangBridge.showToast("❌ 时间段导入失败,请查看日志。");
- return false;
- }
- } else {
- console.log("用户取消了选择。");
- window.shiguangBridge.showToast("季节选择:用户取消了选择!");
- return false; // 用户取消时返回 false
- }
- } catch (error) {
- console.error("显示季节选择单选列表弹窗时发生错误:", error);
- window.shiguangBridge.showToast("季节选择:显示列表出错!" + error.message);
- return false; // 出现错误时也返回 false
- }
- }
- /**
- * 从 API 拉取课程并导入(按选中的 yearTermId 过滤)
- */
- async function importSchedule(semesterInfo) {
- const result = { success: false, courseCount: 0, errorMsg: null };
- try {
- window.shiguangBridge.showToast("正在从教务系统拉取课表...");
- const yearTermId = semesterInfo && semesterInfo.yearTermId;
- const tasks = await fetchTeachTaskList(yearTermId);
- console.log(`拉取到 ${tasks.length} 条教学任务`);
- const courses = transformTeachTaskToCourses(tasks);
- result.courseCount = courses.length;
- if (courses.length === 0) {
- result.errorMsg = "教务系统未返回任何课程数据";
- await window.shiguangBridgePromise.showAlert(
- "⚠️ 拉取失败",
- result.errorMsg + ",请确认已登录且所选学期有课程",
- "知道了"
- );
- return result;
- }
- // 预览
- const preview = await window.shiguangBridgePromise.showAlert(
- "📊 数据预览",
- `共拉取到 ${courses.length} 条课程记录\n\n` +
- `示例:\n${courses.slice(0, 5).map(c =>
- `• 周${c.day} ${c.name} - 第${c.startSection}-${c.endSection}节`
- ).join('\n')}`,
- "确认导入",
- "取消"
- );
- if (!preview) {
- result.errorMsg = "用户在预览时取消";
- return result;
- }
- // 导入课程
- window.shiguangBridge.showToast("正在导入课程...");
- const saveResult = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
- if (saveResult !== true) {
- result.errorMsg = "saveImportedCourses 返回非 true";
- return result;
- }
- // 设置开学日期(默认值来自学期 API 的 beginDate)
- const semesterDate = await demoPrompt(semesterInfo);
- const totalWeeks = (semesterInfo && semesterInfo.semesterTotalWeeks) || 25;
- // 保存课表配置
- const configResult = await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
- semesterStartDate: semesterDate,
- semesterTotalWeeks: totalWeeks,
- defaultClassDuration: 45,
- defaultBreakDuration: 10,
- firstDayOfWeek: 1
- }));
- if (configResult === true) {
- result.success = true;
- return result;
- }
- result.errorMsg = "saveCourseConfig 返回非 true";
- return result;
- } catch (error) {
- console.error("导入错误:", error);
- result.errorMsg = error.message;
- return result;
- }
- }
- /**
- * 显示最终结果汇总弹窗
- */
- async function showFinalResult(results) {
- const success = results.schedule && results.schedule.success;
- const title = success ? '🎉 导入成功' : '❌ 导入失败';
- let msg = '';
- if (results.semesterName) msg += '📚 学期: ' + results.semesterName + '\n';
- if (results.schedule && results.schedule.courseCount > 0) {
- msg += '📖 课程: ' + results.schedule.courseCount + ' 条\n';
- }
- msg += '\n📋 步骤状态:\n';
- msg += (results.semesterList ? '✅' : '❌') + ' 获取学期列表\n';
- msg += (results.season ? '✅' : '❌') + ' 导入时间段';
- if (results.seasonName) msg += ' (' + results.seasonName + ')';
- msg += '\n';
- msg += (results.schedule ? (results.schedule.success ? '✅' : '❌') : '⏭️') + ' 导入课表';
- if (results.schedule && results.schedule.courseCount > 0) msg += ' (' + results.schedule.courseCount + '条)';
- msg += '\n';
- if (results.errorMsg) {
- msg += '\n❗ 错误: ' + results.errorMsg;
- }
- try {
- await window.shiguangBridgePromise.showAlert(title, msg, '知道了');
- } catch (e) {
- console.error('显示最终结果弹窗失败:', e);
- window.shiguangBridge.showToast(title);
- }
- }
- async function runAllDemosSequentially() {
- window.shiguangBridge.showToast("🚀 课表导入助手启动...");
- const results = {
- semesterList: false,
- semesterName: '',
- season: false,
- seasonName: '',
- schedule: null,
- errorMsg: null
- };
- try {
- // 1. 确认开始
- const start = await demoAlert();
- if (!start) {
- results.errorMsg = '用户取消(开始按钮)';
- await showFinalResult(results);
- return;
- }
- // 2. 拉学期列表
- let semesterList = [];
- try {
- window.shiguangBridge.showToast("正在获取学期列表...");
- semesterList = await fetchSemesterList();
- console.log(`拉取到 ${semesterList.length} 个学期`);
- results.semesterList = true;
- } catch (e) {
- console.error("获取学期列表失败:", e);
- results.errorMsg = e.message;
- await showFinalResult(results);
- return;
- }
- // 3. 让用户选择学期
- const pickedSemester = await pickSemester(semesterList);
- if (!pickedSemester) {
- results.errorMsg = '用户取消了学期选择';
- await showFinalResult(results);
- return;
- }
- results.semesterName = pickedSemester.yearTermName;
- const semesterInfo = buildSemesterInfoFromSemester(pickedSemester);
- // 4. 导入时间段(季节选择:夏季/秋季)
- results.season = await importPresetTimeSlots();
- // 季节名映射(与 importPresetTimeSlots 内部一致)
- // 这里不直接拿名字,留空也行;如果想显示需要改 importPresetTimeSlots 返回值
- // 5. 导入课表(用选中的 yearTermId 拉 teachTask)
- results.schedule = await importSchedule(semesterInfo);
- } catch (e) {
- console.error("运行错误:", e);
- results.errorMsg = e.message;
- }
- await showFinalResult(results);
- window.shiguangBridge.notifyTaskCompletion();
- }
- // 导出函数
- window.validateDate = validateDate;
- window.getAuthToken = getAuthToken;
- window.getStudentId = getStudentId;
- window.decodeJwtPayload = decodeJwtPayload;
- window.fetchSemesterList = fetchSemesterList;
- window.fetchTeachTaskList = fetchTeachTaskList;
- window.pickSemester = pickSemester;
- window.transformTeachTaskToCourses = transformTeachTaskToCourses;
- window.getSeasonTimeSlots = getSeasonTimeSlots;
- window.importPresetTimeSlots = importPresetTimeSlots;
- window.importSchedule = importSchedule;
- // 启动
- runAllDemosSequentially();
|