Просмотр исходного кода

add:重庆财经学院、安徽中医药高等专科学校教务适配 (#304)

* 增加了重庆财经学院的适配

* 增加了重庆财经学院

* 适配了安徽中医药高等专科学校

* 适配了安徽中医药高等专科学校

* Delete AHZYYGZ directory

* 适配了安徽中医药高等专科学校
goulinxin 1 месяц назад
Родитель
Сommit
c5617fd1b8

+ 11 - 1
index/root_index.yaml

@@ -480,4 +480,14 @@ schools:
   - id: "STCNCHU"
     name: "南昌航空大学科技学院"
     initial: "N"
-    resource_folder: "STCNCHU"
+    resource_folder: "STCNCHU"
+
+  - id: "CFEC"
+    name: "重庆财经学院"
+    initial: "C"
+    resource_folder: "CFEC"
+
+  - id: "AHZYYGZ"
+    name: "安徽中医药高等专科学校"
+    initial: "A"
+    resource_folder: "AHZYYGZ"

+ 423 - 0
resources/AHZYYGZ/AHZYYGZ.js

@@ -0,0 +1,423 @@
+const BASE = `${window.location.origin}/ahzyygzjw`;
+const CONTROL_PAGE = '/student/xkjg.wdkb.jsp?menucode=S20301';
+const TIMETABLE_PAGE = '/student/wsxk.xskcb10319.jsp?params=';
+const TIME_SLOTS = [
+  { number: 1, startTime: '08:00', endTime: '08:40' },
+  { number: 2, startTime: '08:50', endTime: '09:30' },
+  { number: 3, startTime: '09:45', endTime: '10:25' },
+  { number: 4, startTime: '10:35', endTime: '11:15' },
+  { number: 5, startTime: '11:25', endTime: '12:05' },
+  { number: 6, startTime: '14:00', endTime: '14:40' },
+  { number: 7, startTime: '14:50', endTime: '15:30' },
+  { number: 8, startTime: '15:45', endTime: '16:25' },
+  { number: 9, startTime: '16:35', endTime: '17:15' },
+  { number: 10, startTime: '19:00', endTime: '20:00' },
+  { number: 11, startTime: '20:00', endTime: '21:00' },
+  { number: 12, startTime: '21:40', endTime: '22:30' }
+];
+
+function cleanText(value) {
+  return String(value || '')
+    .replace(/[​-‍]/g, '')
+    .replace(/ /g, ' ')
+    .trim();
+}
+
+function encodeParams(xn, xq, xh) {
+  return btoa(`xn=${xn}&xq=${xq}&xh=${xh}`);
+}
+
+function parseWeeks(weekStr) {
+  const weeks = [];
+  String(weekStr || '')
+    .replace(/\s+/g, '')
+    .split(/[,,]/)
+    .forEach((part) => {
+      if (!part) return;
+      const isSingle = part.includes('单');
+      const isDouble = part.includes('双');
+      const rangeMatch = part.match(/(\d+)-(\d+)/);
+      if (rangeMatch) {
+        const start = parseInt(rangeMatch[1], 10);
+        const end = parseInt(rangeMatch[2], 10);
+        for (let i = start; i <= end; i++) {
+          if (isSingle && i % 2 === 0) continue;
+          if (isDouble && i % 2 !== 0) continue;
+          weeks.push(i);
+        }
+      } else {
+        const num = parseInt(part.replace(/[^\d]/g, ''), 10);
+        if (!Number.isNaN(num)) weeks.push(num);
+      }
+    });
+  return [...new Set(weeks)].sort((a, b) => a - b);
+}
+
+function decodeParams(encoded) {
+  try {
+    return atob(encoded);
+  } catch (_) {
+    return '';
+  }
+}
+
+function parseXhFromEncodedParams(encoded) {
+  const decoded = decodeParams(encoded);
+  if (!decoded) return '';
+  const search = new URLSearchParams(decoded);
+  return String(search.get('xh') || '').trim();
+}
+
+function extractParamsFromHtml(html) {
+  const match = String(html || '').match(/wsxk\.xskcb10319\.jsp\?params=([^"'&\s>]+)/);
+  return match ? decodeURIComponent(match[1]) : '';
+}
+
+function findControlFrame(win) {
+  try {
+    if (win.document.querySelector('#xnxq')) return win;
+  } catch (_) {}
+  for (let i = 0; i < win.frames.length; i++) {
+    try {
+      const found = findControlFrame(win.frames[i]);
+      if (found) return found;
+    } catch (_) {}
+  }
+  return null;
+}
+
+function findTimetableFrame(win) {
+  try {
+    if (win.document.getElementById('mytable')) return win;
+  } catch (_) {}
+  for (let i = 0; i < win.frames.length; i++) {
+    try {
+      const found = findTimetableFrame(win.frames[i]);
+      if (found) return found;
+    } catch (_) {}
+  }
+  return null;
+}
+
+async function fetchControlDoc() {
+  const res = await fetch(`${BASE}${CONTROL_PAGE}`, { credentials: 'include' });
+  if (!res.ok) throw new Error(`课表控制页请求失败: ${res.status}`);
+  const html = await res.text();
+  return new DOMParser().parseFromString(html, 'text/html');
+}
+
+async function fetchTimetableDoc(xn, xq, xh) {
+  const params = encodeParams(xn, xq, xh);
+  const res = await fetch(`${BASE}${TIMETABLE_PAGE}${encodeURIComponent(params)}`, {
+    method: 'GET',
+    credentials: 'include'
+  });
+  if (!res.ok) throw new Error(`课表页面请求失败: ${res.status}`);
+  const buffer = await res.arrayBuffer();
+  let html = '';
+  try {
+    html = new TextDecoder('gbk').decode(buffer);
+  } catch (_) {
+    html = new TextDecoder('utf-8').decode(buffer);
+  }
+  return new DOMParser().parseFromString(html, 'text/html');
+}
+
+function parseSelectOptions(selectEl) {
+  if (!selectEl) return { options: [], defaultIndex: 0 };
+  const options = [];
+  let defaultIndex = 0;
+  Array.from(selectEl.querySelectorAll('option')).forEach((opt) => {
+    const value = String(opt.value || '').trim();
+    if (!value) return;
+    const text = cleanText(opt.textContent) || value;
+    if (opt.selected) defaultIndex = options.length;
+    options.push({ value, text });
+  });
+  return { options, defaultIndex };
+}
+
+async function resolveTermSelection() {
+  let controlDoc = null;
+  let controlFrame = findControlFrame(window);
+
+  if (controlFrame) {
+    controlDoc = controlFrame.document;
+  } else {
+    controlDoc = await fetchControlDoc();
+  }
+
+  const select = controlDoc.querySelector('#xnxq');
+  if (!select) {
+    throw new Error('未找到学期选择器,请先登录并打开“学生个人课表”页面');
+  }
+
+  const { options, defaultIndex } = parseSelectOptions(select);
+  if (!options.length) {
+    throw new Error('未读取到学期列表,请先进入“学生个人课表”页面');
+  }
+
+  const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
+    '选择学期',
+    JSON.stringify(options.map(item => item.text)),
+    defaultIndex
+  );
+  if (selectedIndex === null || selectedIndex === -1) {
+    throw new Error('已取消学期选择');
+  }
+
+  const selected = options[selectedIndex];
+  const [xn, xq] = String(selected.value).split('-');
+  if (!xn || typeof xq === 'undefined') {
+    throw new Error(`学期值解析失败: ${selected.value}`);
+  }
+
+  let encodedParams = extractParamsFromHtml(controlDoc.documentElement.outerHTML);
+  if (!encodedParams) {
+    const timetableFrame = findTimetableFrame(window);
+    if (timetableFrame) {
+      const url = new URL(timetableFrame.location.href);
+      encodedParams = url.searchParams.get('params') || '';
+    }
+  }
+
+  const xh = parseXhFromEncodedParams(encodedParams);
+  if (!xh) {
+    throw new Error('未读取到学号参数,请先点击进入“学生个人课表”后再导入');
+  }
+
+  return { xn, xq, xh, selectedValue: selected.value };
+}
+
+function findTable(doc) {
+  return doc.getElementById('mytable')
+    || Array.from(doc.querySelectorAll('table')).find(table => {
+      const text = cleanText(table.innerText);
+      return text.includes('星期一') && text.includes('[');
+    })
+    || null;
+}
+
+function parseCourseFromLines(lines, day) {
+  const joined = lines.join('\n');
+  const match = joined.match(/([\d,-单双]+)\[(\d+)-(\d+)\]/);
+  if (!match) return null;
+
+  const before = joined
+    .slice(0, match.index)
+    .split(/\n+/)
+    .map(cleanText)
+    .filter(Boolean);
+  const after = joined
+    .slice(match.index + match[0].length)
+    .split(/\n+/)
+    .map(cleanText)
+    .filter(Boolean);
+
+  let name = '';
+  let teacher = '';
+  if (before.length >= 2) {
+    name = before[0];
+    teacher = before[1];
+  } else if (before.length === 1) {
+    name = before[0];
+  }
+  if (!name) return null;
+
+  return {
+    name,
+    teacher,
+    position: after.join(' '),
+    day,
+    startSection: parseInt(match[2], 10),
+    endSection: parseInt(match[3], 10),
+    weeks: parseWeeks(match[1])
+  };
+}
+
+function parseCellByDivBlocks(cell, day) {
+  const blocks = Array.from(cell.querySelectorAll('div[style*="padding-bottom:5px"], div[style*="padding-bottom: 5px"]'));
+  if (!blocks.length) return [];
+
+  const items = [];
+  blocks.forEach((block) => {
+    const lines = block.innerText
+      .split(/\n+/)
+      .map(cleanText)
+      .filter(Boolean);
+    const parsed = parseCourseFromLines(lines, day);
+    if (parsed && parsed.weeks.length) items.push(parsed);
+  });
+  return items;
+}
+
+function parseCellByTextFallback(cell, day) {
+  const lines = cell.innerText
+    .split(/\n+/)
+    .map(cleanText)
+    .filter(Boolean);
+  if (!lines.length) return [];
+
+  const timeIndices = [];
+  lines.forEach((line, index) => {
+    if (/([\d,-单双]+)\[(\d+)-(\d+)\]/.test(line)) {
+      timeIndices.push(index);
+    }
+  });
+
+  const items = [];
+  timeIndices.forEach((currentIndex, i) => {
+    const nextIndex = i + 1 < timeIndices.length ? timeIndices[i + 1] : lines.length;
+    let beforeLines = i === 0 ? lines.slice(0, currentIndex) : lines.slice(timeIndices[i - 1] + 1, currentIndex);
+    if (beforeLines.length > 2) beforeLines = beforeLines.slice(beforeLines.length - 2);
+
+    const match = lines[currentIndex].match(/([\d,-单双]+)\[(\d+)-(\d+)\]/);
+    if (!match) return;
+
+    const name = beforeLines[0] || '未知课程';
+    const teacher = beforeLines[1] || '';
+    const positionLines = lines.slice(currentIndex + 1, nextIndex);
+
+    items.push({
+      name,
+      teacher,
+      position: positionLines.join(' '),
+      day,
+      startSection: parseInt(match[2], 10),
+      endSection: parseInt(match[3], 10),
+      weeks: parseWeeks(match[1])
+    });
+  });
+
+  return items;
+}
+
+function parseAndMergeQingguoTable(doc) {
+  const table = findTable(doc);
+  if (!table) {
+    throw new Error('未找到课表表格,请先进入“学生个人课表”页面');
+  }
+
+  const rawItems = [];
+  Array.from(table.rows).forEach((row) => {
+    const cells = Array.from(row.cells);
+    if (cells.length < 7) return;
+
+    cells.forEach((cell, colIndex) => {
+      const distanceToLast = cells.length - 1 - colIndex;
+      if (distanceToLast > 6) return;
+      const day = 7 - distanceToLast;
+      const rawText = cleanText(cell.innerText);
+      if (!rawText.includes('[')) return;
+
+      const divParsed = parseCellByDivBlocks(cell, day);
+      if (divParsed.length) {
+        rawItems.push(...divParsed);
+        return;
+      }
+
+      rawItems.push(...parseCellByTextFallback(cell, day));
+    });
+  });
+
+  const groupMap = new Map();
+  rawItems.forEach((item) => {
+    if (!item || !item.name || !item.weeks.length) return;
+    const key = `${item.name}|${item.teacher}|${item.position}|${item.day}`;
+    if (!groupMap.has(key)) groupMap.set(key, {});
+    const weekMap = groupMap.get(key);
+    item.weeks.forEach((week) => {
+      if (!weekMap[week]) weekMap[week] = new Set();
+      for (let section = item.startSection; section <= item.endSection; section++) {
+        weekMap[week].add(section);
+      }
+    });
+  });
+
+  const finalCourses = [];
+  groupMap.forEach((weekMap, key) => {
+    const [name, teacher, position, day] = key.split('|');
+    const patternMap = new Map();
+
+    Object.keys(weekMap).forEach((weekStr) => {
+      const week = parseInt(weekStr, 10);
+      const sections = Array.from(weekMap[week]).sort((a, b) => a - b);
+      if (!sections.length) return;
+      let start = sections[0];
+      for (let i = 0; i < sections.length; i++) {
+        if (i === sections.length - 1 || sections[i + 1] !== sections[i] + 1) {
+          const pKey = `${start}-${sections[i]}`;
+          if (!patternMap.has(pKey)) patternMap.set(pKey, []);
+          patternMap.get(pKey).push(week);
+          if (i < sections.length - 1) start = sections[i + 1];
+        }
+      }
+    });
+
+    patternMap.forEach((weeks, patternKey) => {
+      const [startSection, endSection] = patternKey.split('-').map(Number);
+      finalCourses.push({
+        name,
+        teacher,
+        position,
+        day: parseInt(day, 10),
+        startSection,
+        endSection,
+        weeks: weeks.sort((a, b) => a - b)
+      });
+    });
+  });
+
+  return finalCourses;
+}
+
+async function loadTimetableDoc(term) {
+  const currentTableFrame = findTimetableFrame(window);
+  if (currentTableFrame && currentTableFrame.document.getElementById('mytable')) {
+    const currentUrl = new URL(currentTableFrame.location.href);
+    const currentParams = currentUrl.searchParams.get('params') || '';
+    const decoded = decodeParams(currentParams);
+    if (decoded.includes(`xn=${term.xn}`) && decoded.includes(`xq=${term.xq}`)) {
+      return currentTableFrame.document;
+    }
+  }
+  return await fetchTimetableDoc(term.xn, term.xq, term.xh);
+}
+
+async function runImportFlow() {
+  try {
+    const confirmed = await window.AndroidBridgePromise.showAlert(
+      '安徽中医药高等专科学校教务导入',
+      '请确认你已登录教务系统,并且最好已经打开“学生个人课表”页面。',
+      '确定,开始导入'
+    );
+    if (!confirmed) return;
+
+    const term = await resolveTermSelection();
+    AndroidBridge.showToast('正在提取青果课表数据...');
+    const doc = await loadTimetableDoc(term);
+    const courses = parseAndMergeQingguoTable(doc);
+    if (!courses.length) {
+      throw new Error('未找到有效课程,请确认当前学期课表已正常显示');
+    }
+
+    const allWeeks = courses.flatMap(course => course.weeks);
+    const semesterTotalWeeks = allWeeks.length ? Math.max(...allWeeks) : 20;
+
+    await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({
+      semesterTotalWeeks,
+      semesterStartDate: null,
+      firstDayOfWeek: 1
+    }));
+    await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(TIME_SLOTS));
+    await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
+
+    AndroidBridge.showToast(`导入成功:共 ${courses.length} 门课程`);
+    AndroidBridge.notifyTaskCompletion();
+  } catch (error) {
+    console.error(error);
+    AndroidBridge.showToast(`导入失败: ${error.message}`);
+  }
+}
+
+runImportFlow();

+ 8 - 0
resources/AHZYYGZ/adapters.yaml

@@ -0,0 +1,8 @@
+adapters: 
+  - adapter_id: "AHZYYGZ_01" 
+    adapter_name: "安徽中医药高等专科学校" 
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "AHZYYGZ.js" 
+    import_url: "https://jwxt.ahzyygz.edu.cn/ahzyygzjw/frame/homes.action?v=57985306490269832436839"
+    maintainer: "glxgo"
+    description: "安徽中医药高等专科学校青果教务适配器,非本校开发者适配,如果有误建议提交issues"

+ 224 - 0
resources/CFEC/CFEC.js

@@ -0,0 +1,224 @@
+const BASE = `${window.location.origin}/jwglxt`;
+const INDEX_PATH = '/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default';
+const COURSE_API_PATH = '/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151';
+const TIME_API_PATH = '/kbcx/xskbcx_cxRjc.html?gnmkdm=N2151';
+
+async function req(url, method = 'GET', body) {
+  const res = await fetch(url, {
+    method,
+    credentials: 'include',
+    headers: {
+      'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
+      'x-requested-with': 'XMLHttpRequest',
+      'accept': '*/*'
+    },
+    body
+  });
+  if (!res.ok) throw new Error(`请求失败: ${res.status}`);
+  return await res.text();
+}
+
+function isOnTimetablePage() {
+  return window.location.pathname === '/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html';
+}
+
+function readCurrentPageTerm() {
+  const xnmEl = document.querySelector('#xnm');
+  const xqmEl = document.querySelector('#xqm');
+  const xnm = xnmEl ? String(xnmEl.value || '').trim() : '';
+  const xqm = xqmEl ? String(xqmEl.value || '').trim() : '';
+  if (!xnm || !xqm) throw new Error('当前课表页未读到学年学期,请先选择后再导入');
+  return { xnm, xqm };
+}
+
+function parseSelectOptions(selectEl) {
+  if (!selectEl) return { options: [], defaultIndex: 0 };
+  const options = [];
+  let defaultIndex = 0;
+  Array.from(selectEl.querySelectorAll('option')).forEach((opt) => {
+    const value = String(opt.value || '').trim();
+    if (!value) return;
+    const text = String(opt.textContent || '').trim() || value;
+    if (opt.selected) defaultIndex = options.length;
+    options.push({ value, text });
+  });
+  return { options, defaultIndex };
+}
+
+function parseTermOptionsFromDoc(doc) {
+  const yearData = parseSelectOptions(doc.querySelector('#xnm'));
+  const semesterData = parseSelectOptions(doc.querySelector('#xqm'));
+  if (!yearData.options.length || !semesterData.options.length) {
+    throw new Error('课表页学年学期选项解析失败');
+  }
+  return { yearData, semesterData };
+}
+
+async function fetchIndexDoc() {
+  const html = await fetch(`${BASE}${INDEX_PATH}`, { credentials: 'include' }).then(res => {
+    if (!res.ok) throw new Error(`课表页请求失败: ${res.status}`);
+    return res.text();
+  });
+  return new DOMParser().parseFromString(html, 'text/html');
+}
+
+async function selectTermByUserFromDoc(doc) {
+  const { yearData, semesterData } = parseTermOptionsFromDoc(doc);
+
+  const yearIndex = await window.AndroidBridgePromise.showSingleSelection(
+    '选择学年',
+    JSON.stringify(yearData.options.map(item => item.text)),
+    yearData.defaultIndex
+  );
+  if (yearIndex === null || yearIndex === -1) throw new Error('已取消学年选择');
+
+  const semesterIndex = await window.AndroidBridgePromise.showSingleSelection(
+    '选择学期',
+    JSON.stringify(semesterData.options.map(item => item.text)),
+    semesterData.defaultIndex
+  );
+  if (semesterIndex === null || semesterIndex === -1) throw new Error('已取消学期选择');
+
+  return {
+    xnm: yearData.options[yearIndex].value,
+    xqm: semesterData.options[semesterIndex].value
+  };
+}
+
+async function resolveTerm() {
+  if (isOnTimetablePage()) {
+    return readCurrentPageTerm();
+  }
+  const doc = await fetchIndexDoc();
+  return await selectTermByUserFromDoc(doc);
+}
+
+function parseWeeks(zcd) {
+  if (!zcd) return [];
+  const result = new Set();
+  String(zcd).replace(/\s+/g, '').split(/[,,]/).forEach((seg) => {
+    const odd = seg.includes('单');
+    const even = seg.includes('双');
+    const normalized = seg.replace(/周|\(|\)|单|双/g, '');
+    const match = normalized.match(/(\d+)(?:-(\d+))?/);
+    if (!match) return;
+    const start = Number(match[1]);
+    const end = Number(match[2] || match[1]);
+    for (let week = start; week <= end; week++) {
+      if (odd && week % 2 === 0) continue;
+      if (even && week % 2 !== 0) continue;
+      result.add(week);
+    }
+  });
+  return [...result].sort((a, b) => a - b);
+}
+
+function parseCourses(data) {
+  if (!data || !Array.isArray(data.kbList)) {
+    return { courses: [], xqhId: '1' };
+  }
+
+  const courses = [];
+  let xqhId = '1';
+
+  data.kbList.forEach((course) => {
+    if (course.xqh_id) xqhId = String(course.xqh_id).trim() || xqhId;
+
+    const day = Number(course.xqj);
+    const secRaw = String(course.jcs || course.jc || '').replace(/节/g, '').trim();
+    const sectionNums = (secRaw.match(/\d+/g) || []).map(Number).filter(n => !Number.isNaN(n));
+    const weeks = parseWeeks(course.zcd);
+
+    if (!course.kcmc || !sectionNums.length || !weeks.length || !(day >= 1 && day <= 7)) return;
+
+    courses.push({
+      name: String(course.kcmc).trim(),
+      teacher: String(course.xm || '未知').trim(),
+      position: String(course.cdmc || course.cdbh || '未排地点').trim(),
+      day,
+      startSection: sectionNums[0],
+      endSection: sectionNums[sectionNums.length - 1],
+      weeks
+    });
+  });
+
+  const deduped = new Map();
+  courses.forEach((course) => {
+    const key = `${course.name}|${course.teacher}|${course.position}|${course.day}|${course.startSection}|${course.endSection}|${course.weeks.join(',')}`;
+    if (!deduped.has(key)) deduped.set(key, course);
+  });
+
+  return { courses: [...deduped.values()], xqhId };
+}
+
+function parseTimeSlots(data) {
+  if (!Array.isArray(data) || !data.length) throw new Error('未获取到节次时间数据');
+  return data.map((item) => ({
+    number: Number(item.jcmc),
+    startTime: String(item.qssj || '').trim(),
+    endTime: String(item.jssj || '').trim()
+  })).filter(item => item.number > 0 && item.startTime && item.endTime);
+}
+
+async function fetchCourses(xnm, xqm) {
+  const body = `xnm=${encodeURIComponent(xnm)}&xqm=${encodeURIComponent(xqm)}&kzlx=ck&xsdm=&kclbdm=&kclxdm=`;
+  const text = await req(`${BASE}${COURSE_API_PATH}`, 'POST', body);
+  return JSON.parse(text);
+}
+
+async function fetchTimeSlots(xnm, xqm, xqhId) {
+  const body = `xnm=${encodeURIComponent(xnm)}&xqm=${encodeURIComponent(xqm)}&xqh_id=${encodeURIComponent(xqhId || '1')}`;
+  const text = await req(`${BASE}${TIME_API_PATH}`, 'POST', body);
+  return parseTimeSlots(JSON.parse(text));
+}
+
+function validateSemesterStartDateInput(input) {
+  const value = String(input || '').trim();
+  if (!value) return false;
+  return /^\d{4}-\d{2}-\d{2}$/.test(value) ? false : '请输入 YYYY-MM-DD,例如 2026-02-24';
+}
+
+async function selectSemesterStartDate(xnm, xqm) {
+  const defaultDate = xqm === '3' ? `${xnm}-09-01` : `${Number(xnm) + 1}-03-01`;
+  const picked = await window.AndroidBridgePromise.showPrompt(
+    '选择开学日期',
+    '请输入开学日期(YYYY-MM-DD)',
+    defaultDate,
+    'validateSemesterStartDateInput'
+  );
+  if (picked === null) return null;
+  const value = String(picked).trim();
+  return value || null;
+}
+
+async function run() {
+  try {
+    const { xnm, xqm } = await resolveTerm();
+    AndroidBridge.showToast('正在解析课表数据...');
+
+    const rawData = await fetchCourses(xnm, xqm);
+    const { courses, xqhId } = parseCourses(rawData);
+    if (!courses.length) throw new Error('未获取到课表数据');
+
+    const semesterStartDate = await selectSemesterStartDate(xnm, xqm);
+    const timeSlots = await fetchTimeSlots(xnm, xqm, xqhId);
+    const allWeeks = courses.flatMap(course => course.weeks);
+    const semesterTotalWeeks = allWeeks.length ? Math.max(...allWeeks) : 20;
+
+    await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({
+      semesterTotalWeeks,
+      semesterStartDate,
+      firstDayOfWeek: 1
+    }));
+    await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+    await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
+
+    AndroidBridge.showToast(`导入成功:${courses.length} 门`);
+    AndroidBridge.notifyTaskCompletion();
+  } catch (error) {
+    console.error(error);
+    AndroidBridge.showToast(`导入失败: ${error.message}`);
+  }
+}
+
+run();

+ 8 - 0
resources/CFEC/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+   - adapter_id: "CFEC_01"
+    adapter_name: "重庆财经学院正方教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "CFEC.js" #相对路径
+    import_url: "http://jwmis.cfec.edu.cn/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default"
+    maintainer: "glxgo"
+    description: "重庆财经学院正方教务适配器,非本校开发者适配,如果有误建议提交issues"