sddfvc.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. // 山东药品食品职业学院(sddfvc.edu.cn) 拾光课程表适配脚本
  2. // 非该大学开发者适配,开发者无法及时发现问题
  3. // 出现问题请提联系开发者或者提交pr更改,这更加快速
  4. // 基础配置
  5. const BASE_URL = "https://jwxt.sddfvc.edu.cn";
  6. // 数据解析函数
  7. /**
  8. * 将周次字符串结合单双周标识解析为数字数组 注:现在没有用了 因为无法获取整个学期的表了
  9. * @param {string} zcString "7-15,17-20"
  10. * @param {number} dsz 0: 全周, 1: 单周, 2: 双周, -1: 全周
  11. */
  12. function parseWeeks(zcString, dsz) {
  13. let weeks = [];
  14. if (!zcString) return weeks;
  15. // 解析基础周次
  16. zcString.split(',').forEach(part => {
  17. if (part.includes('-')) {
  18. const [start, end] = part.split('-').map(Number);
  19. for (let i = start; i <= end; i++) weeks.push(i);
  20. } else {
  21. weeks.push(Number(part));
  22. }
  23. });
  24. // 处理单双周过滤
  25. if (dsz === 1) {
  26. weeks = weeks.filter(w => w % 2 !== 0);
  27. } else if (dsz === 2) {
  28. weeks = weeks.filter(w => w % 2 === 0);
  29. }
  30. return weeks;
  31. }
  32. /**
  33. * 从 pkbmc 中提取课程名称
  34. * 规则:
  35. * 1. 用 "#" 分割
  36. * 2. 若某一边含"班"字,则丢弃那一边,保留另一边
  37. * 3. 若两边都不含"班"字,或没有 "#",则整个 pkbmc 作为课程名(保留 "#")
  38. * @param {string} pkbmc
  39. * @returns {string} 课程名
  40. */
  41. function extractCourseName(pkbmc) {
  42. if (!pkbmc || typeof pkbmc !== "string") return "未知课程";
  43. if (!pkbmc.includes("#")) {
  44. // 没有 "#",整体作为课程名
  45. return pkbmc;
  46. }
  47. const parts = pkbmc.split("#");
  48. // 这里只处理第一个 "#" 的两侧,保留其余部分
  49. const left = parts[0];
  50. const right = parts.slice(1).join("#");
  51. const leftHasClass = left.includes("班");
  52. const rightHasClass = right.includes("班");
  53. if (leftHasClass && !rightHasClass) {
  54. // 左边是班级,丢弃左边
  55. return right;
  56. }
  57. if (rightHasClass && !leftHasClass) {
  58. // 右边是班级,丢弃右边
  59. return left;
  60. }
  61. // 两边都含"班" 或 两边都不含"班",整体作为课程名(保留 "#")
  62. return pkbmc;
  63. }
  64. /**
  65. * 转换课程格式为应用模型
  66. */
  67. function parseCoursesToModel(sourceData) {
  68. const resultCourses = [];
  69. const days = ["xq1", "xq2", "xq3", "xq4", "xq5", "xq6", "xq7"];
  70. const sectionMap = { "1": 1, "3": 2, "5": 3, "7": 4, "9": 5, "11": 6 };
  71. days.forEach((dayKey, index) => {
  72. const dayContent = sourceData[dayKey];
  73. if (!dayContent) return;
  74. Object.keys(dayContent).forEach(slotNum => {
  75. const mappedSection = sectionMap[slotNum];
  76. if (!mappedSection) return;
  77. Object.values(dayContent[slotNum]).forEach(item => {
  78. const courseName = extractCourseName(item.pkbmc);
  79. Object.values(item.pkmx).forEach(detail => {
  80. if (!detail) return;
  81. const computedWeeks = parseWeeks(detail.zc.zc, detail.zc.dsz);
  82. if (computedWeeks.length === 0) return;
  83. resultCourses.push({
  84. "name": courseName,
  85. "teacher": detail.teacher[0]?.xm || "未知教师",
  86. "position": detail.classroom || "未知地点",
  87. "day": index + 1,
  88. "startSection": mappedSection,
  89. "endSection": mappedSection,
  90. "weeks": computedWeeks
  91. });
  92. });
  93. });
  94. });
  95. });
  96. return resultCourses;
  97. }
  98. /**
  99. * 节次与周次合并去重函数(供开发者参考)
  100. * @param {Array<Object>} courses 原始解析课程数组
  101. * @returns {Array<Object>} 合并去重后的课程数组
  102. */
  103. function mergeAndDistinctCourses(courses) {
  104. if (!Array.isArray(courses) || courses.length <= 1) return courses;
  105. // 1. 深拷贝并规范周次数据,过滤无效项
  106. const list = courses.map(c => ({
  107. ...c,
  108. name: c.name || '',
  109. teacher: c.teacher || '',
  110. position: c.position || '',
  111. weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
  112. }));
  113. // 阶段 1:合并连续节次与完全重复记录(前提:名称、教师、地点、星期、周次一致)
  114. list.sort((a, b) => {
  115. return a.name.localeCompare(b.name) ||
  116. a.teacher.localeCompare(b.teacher) ||
  117. a.position.localeCompare(b.position) ||
  118. (a.day || 0) - (b.day || 0) ||
  119. a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
  120. (a.startSection || 0) - (b.startSection || 0);
  121. });
  122. const step1Merged = [];
  123. let current = list[0];
  124. for (let i = 1; i < list.length; i++) {
  125. const next = list[i];
  126. const isSameCourseAndWeeks =
  127. current.name === next.name &&
  128. current.teacher === next.teacher &&
  129. current.position === next.position &&
  130. current.day === next.day &&
  131. current.weeks.join(',') === next.weeks.join(',');
  132. const isContinuous = current.endSection + 1 === next.startSection;
  133. const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
  134. if (isSameCourseAndWeeks && isContinuous) {
  135. // 节次连续:延长结束节次 (如 1-2 节 + 3-4 节 -> 1-4 节)
  136. current.endSection = next.endSection;
  137. } else if (isSameCourseAndWeeks && isDuplicate) {
  138. // 完全重复:跳过
  139. continue;
  140. } else {
  141. step1Merged.push(current);
  142. current = next;
  143. }
  144. }
  145. step1Merged.push(current);
  146. // 阶段 2:合并同节次的周次(前提:名称、教师、地点、星期、开始/结束节次一致)
  147. step1Merged.sort((a, b) => {
  148. return a.name.localeCompare(b.name) ||
  149. a.teacher.localeCompare(b.teacher) ||
  150. a.position.localeCompare(b.position) ||
  151. (a.day || 0) - (b.day || 0) ||
  152. (a.startSection || 0) - (b.startSection || 0) ||
  153. (a.endSection || 0) - (b.endSection || 0);
  154. });
  155. const step2Merged = [];
  156. let cur = step1Merged[0];
  157. for (let i = 1; i < step1Merged.length; i++) {
  158. const nxt = step1Merged[i];
  159. const isSameCourseAndSection =
  160. cur.name === nxt.name &&
  161. cur.teacher === nxt.teacher &&
  162. cur.position === nxt.position &&
  163. cur.day === nxt.day &&
  164. cur.startSection === nxt.startSection &&
  165. cur.endSection === nxt.endSection;
  166. if (isSameCourseAndSection) {
  167. // 周次合并去重 (如 1-8 周 + 9-16 周 -> 1-16 周)
  168. cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
  169. } else {
  170. step2Merged.push(cur);
  171. cur = nxt;
  172. }
  173. }
  174. step2Merged.push(cur);
  175. return step2Merged;
  176. }
  177. // 网络与交互业务函数
  178. /**
  179. * 保存课表全局配置
  180. * @param {number} totalWeeks 学期总周数
  181. * @param {string|null} semesterStartDate 学期开始日期 YYYY-MM-DD
  182. */
  183. async function saveAppConfig(totalWeeks, semesterStartDate) {
  184. const config = {
  185. "semesterTotalWeeks": totalWeeks,
  186. "defaultClassDuration": 90,
  187. "defaultBreakDuration": 15,
  188. "firstDayOfWeek": 1
  189. };
  190. if (semesterStartDate) {
  191. config.semesterStartDate = semesterStartDate;
  192. }
  193. return await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  194. }
  195. /**
  196. * 保存时间段配置
  197. */
  198. async function saveAppTimeSlots() {
  199. const timeSlots = [
  200. { "number": 1, "startTime": "08:30", "endTime": "10:00" },
  201. { "number": 2, "startTime": "10:15", "endTime": "11:45" },
  202. { "number": 3, "startTime": "13:30", "endTime": "15:00" },
  203. { "number": 4, "startTime": "15:15", "endTime": "16:45" },
  204. { "number": 5, "startTime": "19:00", "endTime": "19:30" },
  205. { "number": 6, "startTime": "20:00", "endTime": "20:45" }
  206. ];
  207. return await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  208. }
  209. /**
  210. * 获取并让用户选择学期,返回 { id, name }
  211. */
  212. async function getSelectedSemester(apiToken) {
  213. const xqRes = await fetch(`${BASE_URL}/mobile/student/mobile_kcb_xq?api_token=${apiToken}`, {
  214. "credentials": "include"
  215. });
  216. const xqJson = await xqRes.json();
  217. const xqList = xqJson.data.xq_all;
  218. const currentXq = xqJson.data.xq_current;
  219. const xqNames = xqList.map(item => item.xqmc);
  220. const defaultIdx = xqList.findIndex(item => item.id === currentXq.id);
  221. const selectedIdx = await window.shiguangBridgePromise.showSingleSelection(
  222. "请确认导入学期",
  223. JSON.stringify(xqNames),
  224. defaultIdx !== -1 ? defaultIdx : xqNames.length - 1
  225. );
  226. if (selectedIdx === null) return null;
  227. return { id: xqList[selectedIdx].id, name: xqList[selectedIdx].xqmc };
  228. }
  229. /**
  230. * 获取学期周次列表,并推算学期开始日期
  231. * @param {string} apiToken
  232. * @param {string} semesterId
  233. * @param {string} semesterName 形如 "2026-2027 学年度第一学期"
  234. * @returns {{ weeks: Array, semesterStartDate: string|null, totalWeeks: number }}
  235. */
  236. async function fetchSemesterWeeks(apiToken, semesterId, semesterName) {
  237. const res = await fetch(`${BASE_URL}/mobile/student/mobile_kcb_weeks?xq=${semesterId}&api_token=${apiToken}`, {
  238. "credentials": "include"
  239. });
  240. const json = await res.json();
  241. const weekList = json?.data?.week_list || [];
  242. let semesterStartDate = null;
  243. const firstWeek = weekList.find(w => String(w.id) === '1') || weekList[0];
  244. if (firstWeek && firstWeek.start_date && semesterName) {
  245. const yearMatch = semesterName.match(/(\d{4})-(\d{4})/);
  246. if (yearMatch) {
  247. const startYear = Number(yearMatch[1]);
  248. const isSecondTerm = semesterName.includes('第二学期');
  249. const year = isSecondTerm ? startYear + 1 : startYear;
  250. const [mm, dd] = firstWeek.start_date.split('/');
  251. semesterStartDate = `${year}-${mm.padStart(2, '0')}-${dd.padStart(2, '0')}`;
  252. }
  253. }
  254. return {
  255. weeks: weekList,
  256. semesterStartDate: semesterStartDate,
  257. totalWeeks: weekList.length
  258. };
  259. }
  260. /**
  261. * 遍历抓取该学期所有周的数据
  262. * @param {string} apiToken
  263. * @param {string} semesterId
  264. * @param {Array} weekList 来自 mobile_kcb_weeks 的 week_list
  265. */
  266. async function fetchFullSemesterData(apiToken, semesterId, weekList) {
  267. let allCourses = [];
  268. for (const week of weekList) {
  269. const w = week.id;
  270. window.shiguangBridge.showToast(`正在获取第 ${w}/${weekList.length} 周...`);
  271. try {
  272. const res = await fetch(`${BASE_URL}/mobile/student/mobile_kcb?api_token=${apiToken}&xq=${semesterId}&week=${w}`, {
  273. "credentials": "include"
  274. });
  275. const json = await res.json();
  276. if (json.data) {
  277. const weekCourses = parseCoursesToModel(json.data);
  278. allCourses = allCourses.concat(weekCourses);
  279. }
  280. } catch (e) {
  281. console.warn(`第 ${w} 周数据抓取失败`, e);
  282. }
  283. }
  284. // 调用合并函数
  285. return mergeAndDistinctCourses(allCourses);
  286. }
  287. // 流程控制
  288. async function runImportFlow() {
  289. try {
  290. const urlParams = new URLSearchParams(window.location.search);
  291. const apiToken = urlParams.get('api_token');
  292. if (!apiToken) {
  293. console.error("当前 URL 中未找到 api_token 参数:", window.location.href);
  294. window.shiguangBridge.showToast("未检测到登录 Token,请确保在课表页面运行");
  295. return;
  296. }
  297. const semester = await getSelectedSemester(apiToken);
  298. if (!semester) {
  299. window.shiguangBridge.showToast("导入已取消");
  300. return;
  301. }
  302. // 先拿周次列表,用于驱动循环 + 推算开学日期
  303. window.shiguangBridge.showToast("正在获取学期周次...");
  304. const { weeks, semesterStartDate, totalWeeks } = await fetchSemesterWeeks(apiToken, semester.id, semester.name);
  305. if (!weeks.length) {
  306. window.shiguangBridge.showToast("未获取到周次列表,无法导入");
  307. return;
  308. }
  309. // 直接进入周遍历模式
  310. window.shiguangBridge.showToast("开始抓取周课表数据...");
  311. const finalCourses = await fetchFullSemesterData(apiToken, semester.id, weeks);
  312. if (finalCourses.length === 0) {
  313. window.shiguangBridge.showToast("未发现任何课程数据");
  314. return;
  315. }
  316. // 保存逻辑
  317. window.shiguangBridge.showToast("正在保存配置...");
  318. await saveAppConfig(totalWeeks, semesterStartDate);
  319. await saveAppTimeSlots();
  320. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(finalCourses));
  321. window.shiguangBridge.showToast(`成功导入 ${finalCourses.length} 门课程`);
  322. window.shiguangBridge.notifyTaskCompletion();
  323. } catch (error) {
  324. window.shiguangBridge.showToast("异常: " + error.message);
  325. }
  326. }
  327. runImportFlow();