starlink.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. /**
  2. * 星链课表分享导入脚本
  3. */
  4. // 工具函数
  5. // 输入验证
  6. function validateInput(input) {
  7. if (!input || input.trim().length === 0) return "请输入分享码!";
  8. return false;
  9. }
  10. // 提取分享码
  11. function extractShareCode(text) {
  12. const structuredRegex = /输入:\s*([^(\s]+)/;
  13. const fallbackRegex = /([a-zA-Z0-9-]{5,20})/;
  14. const matchA = text.match(structuredRegex);
  15. if (matchA && matchA[1]) return matchA[1].trim();
  16. const matchB = text.match(fallbackRegex);
  17. if (matchB) return matchB[1].trim();
  18. return text.trim();
  19. }
  20. // 课程合并与去重函数
  21. /**
  22. * 节次与周次合并去重函数
  23. * @param {Array<Object>} courses 原始解析课程数组
  24. * @returns {Array<Object>} 合并去重后的课程数组
  25. */
  26. function mergeAndDistinctCourses(courses) {
  27. if (!Array.isArray(courses) || courses.length <= 1) return courses;
  28. // 1. 深拷贝并规范周次数据,过滤无效项
  29. const list = courses.map(c => ({
  30. ...c,
  31. name: c.name || '',
  32. teacher: c.teacher || '',
  33. position: c.position || '',
  34. weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
  35. }));
  36. // 阶段 1:合并连续节次与完全重复记录
  37. list.sort((a, b) => {
  38. return a.name.localeCompare(b.name) ||
  39. a.teacher.localeCompare(b.teacher) ||
  40. a.position.localeCompare(b.position) ||
  41. (a.day || 0) - (b.day || 0) ||
  42. a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
  43. (a.startSection || 0) - (b.startSection || 0);
  44. });
  45. const step1Merged = [];
  46. let current = list[0];
  47. for (let i = 1; i < list.length; i++) {
  48. const next = list[i];
  49. const isSameCourseAndWeeks =
  50. current.name === next.name &&
  51. current.teacher === next.teacher &&
  52. current.position === next.position &&
  53. current.day === next.day &&
  54. current.weeks.join(',') === next.weeks.join(',');
  55. const isContinuous = current.endSection + 1 === next.startSection;
  56. const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
  57. if (isSameCourseAndWeeks && isContinuous) {
  58. // 节次连续:延长结束节次
  59. current.endSection = next.endSection;
  60. } else if (isSameCourseAndWeeks && isDuplicate) {
  61. // 完全重复:跳过
  62. continue;
  63. } else {
  64. step1Merged.push(current);
  65. current = next;
  66. }
  67. }
  68. step1Merged.push(current);
  69. // 阶段 2:合并同节次的周次
  70. step1Merged.sort((a, b) => {
  71. return a.name.localeCompare(b.name) ||
  72. a.teacher.localeCompare(b.teacher) ||
  73. a.position.localeCompare(b.position) ||
  74. (a.day || 0) - (b.day || 0) ||
  75. (a.startSection || 0) - (b.startSection || 0) ||
  76. (a.endSection || 0) - (b.endSection || 0);
  77. });
  78. const step2Merged = [];
  79. let cur = step1Merged[0];
  80. for (let i = 1; i < step1Merged.length; i++) {
  81. const nxt = step1Merged[i];
  82. const isSameCourseAndSection =
  83. cur.name === nxt.name &&
  84. cur.teacher === nxt.teacher &&
  85. cur.position === nxt.position &&
  86. cur.day === nxt.day &&
  87. cur.startSection === nxt.startSection &&
  88. cur.endSection === nxt.endSection;
  89. if (isSameCourseAndSection) {
  90. // 周次合并去重
  91. cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
  92. } else {
  93. step2Merged.push(cur);
  94. cur = nxt;
  95. }
  96. }
  97. step2Merged.push(cur);
  98. return step2Merged;
  99. }
  100. // 时间转换函数
  101. // 将分钟数转换为 HH:mm 格式
  102. function minutesToTime(minutes) {
  103. const hours = Math.floor(minutes / 60);
  104. const mins = minutes % 60;
  105. return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
  106. }
  107. // 将星链的 sectionMinutes 转换为标准时间段数据
  108. function convertTimeSlots(sectionMinutes) {
  109. if (!sectionMinutes) return [];
  110. const timeSlots = [];
  111. for (const [number, [startMin, endMin]] of Object.entries(sectionMinutes)) {
  112. timeSlots.push({
  113. number: parseInt(number),
  114. startTime: minutesToTime(startMin),
  115. endTime: minutesToTime(endMin)
  116. });
  117. }
  118. timeSlots.sort((a, b) => a.number - b.number);
  119. return timeSlots;
  120. }
  121. // 主流程
  122. async function runStarlinkImport() {
  123. try {
  124. // 获取用户输入
  125. const userInput = await window.shiguangBridgePromise.showPrompt(
  126. "导入星链课表",
  127. "请粘贴分享文案(包含分享码)",
  128. "",
  129. "validateInput"
  130. );
  131. if (!userInput) return;
  132. const shareCode = extractShareCode(userInput);
  133. const apiUrl = `https://api.starlinkkb.cn/share/curriculum/${shareCode}`;
  134. window.shiguangBridge.showToast("正在同步云端数据...");
  135. // 请求数据
  136. const response = await fetch(apiUrl);
  137. if (!response.ok) throw new Error("分享码已失效或网络异常");
  138. const resJson = await response.json();
  139. const data = resJson.data;
  140. if (!data || !data.courses || data.courses.length === 0) {
  141. throw new Error("未获取到课程数据");
  142. }
  143. // 数据映射 - 转换为标准格式
  144. const rawCourses = data.courses.map(c => ({
  145. name: c.name || "未命名课程",
  146. teacher: (c.teacher && c.teacher !== "无") ? c.teacher : "未知教师",
  147. position: (c.location && c.location.replace(/^@/, '').trim() !== "")
  148. ? c.location.replace(/^@/, '').trim()
  149. : "未排地点",
  150. day: c.weekday || 1,
  151. startSection: c.startSection || 1,
  152. endSection: c.endSection || 1,
  153. weeks: c.weeks || []
  154. }));
  155. // 使用官方合并去重函数处理课程数据
  156. const finalCourses = mergeAndDistinctCourses(rawCourses);
  157. // 构建标准课程数据
  158. const standardCourses = finalCourses.map(c => ({
  159. name: c.name,
  160. teacher: c.teacher,
  161. position: c.position,
  162. day: c.day,
  163. startSection: c.startSection,
  164. endSection: c.endSection,
  165. weeks: c.weeks
  166. }));
  167. // 构建配置数据
  168. const config = {
  169. semesterStartDate: data.startDate ? data.startDate.substring(0, 10) : null,
  170. semesterTotalWeeks: data.totalWeeks || 20
  171. };
  172. // 转换时间段数据
  173. const timeSlots = convertTimeSlots(data.sectionMinutes || {});
  174. // 保存配置
  175. if (config.semesterStartDate) {
  176. try {
  177. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  178. window.shiguangBridge.showToast("课表配置已更新");
  179. } catch (e) {
  180. console.warn("保存配置失败:", e.message);
  181. }
  182. }
  183. // 保存时间段
  184. if (timeSlots.length > 0) {
  185. try {
  186. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  187. window.shiguangBridge.showToast("已导入 " + timeSlots.length + " 个时间段");
  188. } catch (e) {
  189. console.warn("保存时间段失败:", e.message);
  190. }
  191. }
  192. // 保存课程数据
  193. const success = await window.shiguangBridgePromise.saveImportedCourses(
  194. JSON.stringify(standardCourses)
  195. );
  196. if (success) {
  197. window.shiguangBridge.showToast("成功导入 " + standardCourses.length + " 门课程");
  198. window.shiguangBridge.notifyTaskCompletion();
  199. }
  200. } catch (e) {
  201. await window.shiguangBridgePromise.showAlert(
  202. "导入失败",
  203. e.message || "未知错误,请检查网络或分享码",
  204. "确定"
  205. );
  206. }
  207. }
  208. // 启动导入
  209. runStarlinkImport();