// 武汉工程大学强智教务。接口与结构依据本校课表响应验证。
// 开发参考:拾光官方强智适配案例(YSSDUFE,星河欲转)。
(async function () {
const lock = '__witImportRunning';
if (window[lock]) return;
window[lock] = true;
const bridge = window.shiguangBridgePromise;
const toast = message => window.shiguangBridge.showToast(message);
const endpoint = '/jsxsd/xskb/xskb_list.do';
async function semesterInfo(semester) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 20000);
try {
const response = await fetch('/jsxsd/jxzl/jxzl_query', {
method: 'POST', credentials: 'same-origin', signal: controller.signal,
body: new URLSearchParams({ xnxq01id: semester })
});
if (!response.ok) throw new Error('教学周历请求失败');
const doc = new DOMParser().parseFromString(await response.text(), 'text/html');
const selected = doc.querySelector('#xnxq01id');
if (selected && selected.value !== semester) throw new Error('教学周历学期不匹配');
const rows = [...doc.querySelectorAll('#kbtable tr')].filter(row => row.querySelector('td'));
const match = rows[0]?.querySelector('td[title]')?.getAttribute('title')?.match(/^(\d{4})年(\d{2})月(\d{2})日?$/);
const weeks = rows.map(row => parseInt(row.querySelector('td').textContent.trim(), 10)).filter(n => n > 0 && n <= 100);
if (!match || !weeks.length) throw new Error('无法解析教学周历');
const date = `${match[1]}-${match[2]}-${match[3]}`;
const parsed = new Date(`${date}T00:00:00Z`);
if (isNaN(parsed) || parsed.toISOString().slice(0, 10) !== date) throw new Error('教学周历日期无效');
return { semesterStartDate: date, semesterTotalWeeks: Math.max(...weeks), firstDayOfWeek: 1 };
} finally { clearTimeout(timer); }
}
const timeSlots = [
['08:00','08:45'], ['08:50','09:35'], ['09:55','10:40'], ['10:45','11:30'],
['11:35','12:20'], ['14:00','14:45'], ['14:50','15:35'], ['15:55','16:40'],
['16:45','17:30'], ['19:00','19:45'], ['19:50','20:35'], ['20:40','21:25']
].map(([startTime, endTime], i) => ({ number: i + 1, startTime, endTime }));
async function request(options) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 20000);
try {
const response = await fetch(endpoint, { ...options, credentials: 'same-origin', signal: controller.signal });
if (!response.ok) throw new Error(`教务请求失败:${response.status}`);
const doc = new DOMParser().parseFromString(await response.text(), 'text/html');
if (!doc.querySelector('#kbtable') || !doc.querySelector('#xnxq01id')) {
throw new Error('登录已失效或课表结构不匹配,请重新进入教务课表页面');
}
return doc;
} finally { clearTimeout(timer); }
}
function parseWeeks(text) {
const odd = /单/.test(text), even = /双/.test(text);
const source = text.replace(/(/g, '(').split('[')[0]
.replace(/\([^)]*\)/g, '').replace(/[周单双\s]/g, '').replace(/,/g, ',');
const weeks = new Set();
for (const part of source.split(',')) {
const match = part.match(/^(\d+)(?:-(\d+))?$/);
if (!match) throw new Error('无法识别周次');
const start = Number(match[1]), end = Number(match[2] || match[1]);
if (start < 1 || end < start || end > 100) throw new Error('周次超出有效范围');
for (let w = start; w <= end; w++) if ((!odd || w % 2 === 1) && (!even || w % 2 === 0)) weeks.add(w);
}
if (!weeks.size) throw new Error('周次为空');
return [...weeks].sort((a, b) => a - b);
}
try {
if (location.hostname !== 'jwxt.wit.edu.cn') throw new Error('请先进入武汉工程大学教务系统');
const initial = await request();
const select = initial.querySelector('#xnxq01id');
const semesters = [...select.options].filter(option => option.value);
const index = await bridge.showSingleSelection('选择学期', JSON.stringify(semesters.map(o => o.textContent.trim())), semesters.findIndex(o => o.selected));
if (index === null) return;
if (!semesters[index]) throw new Error('学期选择无效');
const semester = semesters[index].value;
const body = new URLSearchParams();
for (const name of ['jx0404id', 'cj0701id', 'demo', 'kbjcmsid']) body.set(name, initial.querySelector(`[name="${name}"]`)?.value || '');
body.set('xnxq01id', semester);
body.set('zc', '');
const detail = initial.querySelector('[name="sfFD"]');
if (detail?.checked) body.set('sfFD', detail.value);
const doc = await request({ method: 'POST', body });
if (doc.querySelector('#xnxq01id').value !== semester || doc.querySelector('#zc')?.value) throw new Error('返回学期或周次与查询条件不一致');
const table = doc.querySelector('#kbtable');
const headers = [...table.rows[0].cells].map(c => c.textContent.trim());
const days = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'];
const courses = new Map();
let invalid = 0;
for (const row of [...table.rows].slice(1)) {
for (let column = 0; column < row.cells.length; column++) {
const cell = row.cells[column];
for (const div of cell.querySelectorAll('div.kbcontent')) {
if (!div.textContent.trim()) continue;
for (const html of div.innerHTML.split(/(?:
\s*)?-{5,}(?:\s*
)?/i)) {
const block = doc.createElement('div'); block.innerHTML = html;
if (!block.textContent.trim()) continue;
try {
const day = days.indexOf(headers[column]) + 1;
const timing = block.querySelector('font[title="周次(节次)"]')?.textContent || '';
const section = timing.match(/\[([\d,,\-\s]+)节\]/);
const name = [...block.childNodes].filter(n => n.nodeType === 3).map(n => n.textContent.trim()).find(Boolean);
if (!name || !day || !section) throw new Error('课程信息不完整');
const weeks = parseWeeks(timing);
const teacher = block.querySelector('font[title="老师"],font[title="教师"]')?.textContent.trim() || '';
const position = block.querySelector('font[title="教室"]')?.textContent.trim() || '';
for (const part of section[1].replace(/,/g, ',').split(',')) {
const match = part.trim().match(/^(\d+)(?:-(\d+))?$/);
if (!match) throw new Error('节次无法识别');
const startSection = Number(match[1]), endSection = Number(match[2] || match[1]);
if (startSection < 1 || endSection < startSection || endSection > 50) throw new Error('节次无效');
const key = JSON.stringify([name, teacher, position, day, startSection, endSection]);
const old = courses.get(key);
courses.set(key, { name, teacher, position, day, startSection, endSection, weeks: [...new Set([...(old?.weeks || []), ...weeks])].sort((a,b) => a-b) });
}
} catch (_) { invalid++; }
}
}
}
}
if (invalid) throw new Error(`${invalid} 条课程无法完整解析,已停止导入,请反馈页面样本`);
if (!courses.size) {
await bridge.showAlert('该学期暂无课程', `${semester} 没有已排定时间的课程,本次未导入数据。可重新运行脚本选择其他学期。`, '知道了');
return;
}
const remarks = [...table.rows].find(r => r.cells[0]?.textContent.includes('备注'))?.cells[1]?.textContent.trim();
let config = null;
try { config = await semesterInfo(semester); } catch (_) { /* 保留现有学期设置,提示用户核对。 */ }
if (!await bridge.showAlert('导入前确认', `将导入 ${courses.size} 条课程安排,并写入学校作息。${config ? `教学周历:${config.semesterStartDate} 开始,共 ${config.semesterTotalWeeks} 周。` : '未能获取教学周历,请在 App 内手动核对开学日期和总周数。'}请核对节次作息。${remarks ? '\n课表含备注,请回教务核对其中未排定时间的课程并手动添加。' : ''}`, '确认导入')) return;
if (config) await bridge.saveCourseConfig(JSON.stringify(config));
await bridge.savePresetTimeSlots(JSON.stringify(timeSlots));
await bridge.saveImportedCourses(JSON.stringify([...courses.values()]));
toast(`成功导入 ${courses.size} 条课程安排`);
window.shiguangBridge.notifyTaskCompletion();
} catch (error) { toast(`导入停止:${error.message}`); }
finally { window[lock] = false; }
})();