// ==UserScript==
// @name SCUT_Lesson_Table
// @namespace http://xsjw2018.jw.scut.edu.cn/SCUT_Lesson_table
// @version 6.1
// @description 华工教务导出课表 - 多格式多选导出 + 课程合并
// @author WanderLandWalker
// @match http://xsjw2018.jw.scut.edu.cn/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=scut.edu.cn
// @grant GM_addStyle
// @license GPL-3.0 License
// ==/UserScript==
(function() {
'use strict';
var PERIOD_TIMES = {
'1': { start: '08:50', end: '09:35' },
'2': { start: '09:40', end: '10:25' },
'3': { start: '10:40', end: '11:25' },
'4': { start: '11:30', end: '12:15' },
'5': { start: '14:00', end: '14:45' },
'6': { start: '14:50', end: '15:35' },
'7': { start: '15:55', end: '16:40' },
'8': { start: '16:45', end: '17:30' },
'9': { start: '19:00', end: '19:45' },
'10': { start: '19:50', end: '20:35' },
'11': { start: '20:40', end: '21:25' }
};
var FIELDS = [
{ key: 'kcmc', label: '课程名称', group: 'basic', defaultOn: true },
{ key: 'kch', label: '课程号', group: 'basic', defaultOn: true },
{ key: 'xm', label: '教师', group: 'basic', defaultOn: true },
{ key: 'zcmc', label: '教师职称', group: 'basic', defaultOn: false },
{ key: 'zfjmc', label: '主辅讲', group: 'basic', defaultOn: false },
{ key: 'xqj_mc', label: '星期', group: 'basic', defaultOn: true },
{ key: 'jcs', label: '节次', group: 'basic', defaultOn: true },
{ key: 'zcd', label: '时间/周次', group: 'basic', defaultOn: true },
{ key: 'cdmc', label: '上课地点', group: 'basic', defaultOn: true },
{ key: 'xf', label: '学分', group: 'basic', defaultOn: true },
{ key: 'jxbmc', label: '教学班', group: 'extended', defaultOn: false },
{ key: 'jxbzc', label: '教学班组成', group: 'extended', defaultOn: false },
{ key: 'xkbz', label: '选课备注', group: 'extended', defaultOn: false },
{ key: 'kcxszc', label: '课程学时组成',group: 'extended', defaultOn: false },
{ key: 'zhxs', label: '周学时', group: 'extended', defaultOn: false },
{ key: 'zxs', label: '总学时', group: 'extended', defaultOn: false },
{ key: 'kczxs', label: '课程总学时', group: 'extended', defaultOn: false },
{ key: 'khfsmc', label: '考核方式', group: 'extended', defaultOn: false },
{ key: 'ksfsmc', label: '考试方式', group: 'extended', defaultOn: false },
{ key: 'skfsmc', label: '授课方式名称',group: 'extended', defaultOn: false },
{ key: 'cxbjmc', label: '重修标记', group: 'extended', defaultOn: false },
{ key: 'kcxz', label: '课程性质', group: 'extended', defaultOn: false },
{ key: 'kcbj', label: '课程标记', group: 'extended', defaultOn: false }
];
var courseData = [];
var studentInfo = null;
var dataReady = false;
var panelCreated = false;
var libCache = {};
var $btn, $overlay, $panel, $status;
/* ================================================================
Utility
================================================================ */
function getFirstMonday(year) {
var d = new Date(year, 0, 1);
var day = d.getDay();
var offset = (day === 0 ? 1 : (day <= 1 ? 1 - day : 8 - day));
d.setDate(d.getDate() + offset);
return d;
}
function getWeekDate(year, weekNum) {
var mon = getFirstMonday(year);
mon.setDate(mon.getDate() + (weekNum - 1) * 7);
return mon;
}
function pad2(n) { return n < 10 ? '0' + n : '' + n; }
function toIcalDate(year, weekNum, dayOfWeek, timeStr) {
var d = getWeekDate(year, weekNum);
d.setDate(d.getDate() + (dayOfWeek - 1));
var parts = timeStr.split(':');
return d.getFullYear() + pad2(d.getMonth() + 1) + pad2(d.getDate()) + 'T' + parts[0] + parts[1] + '00';
}
function getDayNum(xqj) {
var map = { '1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,
'星期一':1,'星期二':2,'星期三':3,'星期四':4,
'星期五':5,'星期六':6,'星期日':7 };
return map[xqj] || parseInt(xqj, 10) || 0;
}
function escapeHtml(s) {
if (!s) return '';
return String(s).replace(/&/g, '&').replace(//g, '>')
.replace(/"/g, '"').replace(/\n/g, ' ');
}
function escapeCsv(s) {
if (!s) return '';
s = String(s).replace(/\r?\n/g, ' ');
if (s.indexOf(',') >= 0 || s.indexOf('"') >= 0) return '"' + s.replace(/"/g, '""') + '"';
return s;
}
function escapeIcalText(s) {
if (!s) return '';
return s.replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\n/g, '\\n');
}
function getVal(item, key) {
if (key === 'xqj_mc') {
var dayNames = { '1':'星期一','2':'星期二','3':'星期三','4':'星期四',
'5':'星期五','6':'星期六','7':'星期日' };
return dayNames[item.xqj] || item.xqj || '';
}
return item[key] || '';
}
function download(content, fileName, mimeType) {
var blob = new Blob([content], { type: mimeType || 'text/plain;charset=utf-8' });
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.download = fileName;
link.href = url;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
function getSelectedFields() {
if (!$panel) return [];
var selected = [];
var boxes = $panel.querySelectorAll('.__chk_grid input[type=checkbox]');
for (var i = 0; i < boxes.length; i++) {
if (boxes[i].checked) {
var key = boxes[i].getAttribute('data-field');
for (var j = 0; j < FIELDS.length; j++) {
if (FIELDS[j].key === key) { selected.push(FIELDS[j]); break; }
}
}
}
return selected;
}
function getSelectedFormats() {
if (!$panel) return [];
var fmts = [];
var boxes = $panel.querySelectorAll('.__fmt_grid input[type=checkbox]');
for (var i = 0; i < boxes.length; i++) {
if (boxes[i].checked) fmts.push(boxes[i].getAttribute('data-fmt'));
}
return fmts;
}
function isMergeEnabled() {
var cb = document.getElementById('__scut_merge_cb');
return cb ? cb.checked : false;
}
function getBaseName() {
var semName = '';
if (studentInfo) {
semName = (studentInfo.XNMC || '').replace(/\s/g, '') + '_' + (studentInfo.XQMMC || '') + '学期';
}
return '课表_' + (semName || 'export');
}
/* ================================================================
Week Parsing (advanced: supports 双/单/不连续周次)
================================================================ */
function parseWeekAdvanced(str) {
if (!str) return [];
var weeks = [];
var seen = {};
var cleaned = str.replace(/周/g, '');
var parts = cleaned.split(/[,,]/);
for (var i = 0; i < parts.length; i++) {
var p = parts[i].trim();
if (!p) continue;
var m = p.match(/^(\d+)(?:\s*[-~]\s*(\d+))?(?:\s*\((单|双)\))?$/);
if (m) {
var start = parseInt(m[1], 10);
var end = m[2] ? parseInt(m[2], 10) : start;
var parity = m[3] || null;
for (var w = start; w <= end; w++) {
if (parity === '双' && w % 2 !== 0) continue;
if (parity === '单' && w % 2 === 0) continue;
if (!seen[w]) { weeks.push(w); seen[w] = true; }
}
} else {
var n = parseInt(p, 10);
if (!isNaN(n) && !seen[n]) { weeks.push(n); seen[n] = true; }
}
}
weeks.sort(function(a, b) { return a - b; });
return weeks;
}
function parseSessions(str) {
if (!str) return [];
var sessions = [];
if (str.indexOf('-') >= 0) {
var range = str.split('-');
var a = parseInt(range[0], 10);
var b = parseInt(range[1], 10);
if (!isNaN(a) && !isNaN(b)) for (var j = a; j <= b; j++) sessions.push(j);
} else {
var n = parseInt(str, 10);
if (!isNaN(n)) sessions.push(n);
}
return sessions;
}
function formatWeekStr(weeks) {
if (!weeks || weeks.length === 0) return '';
var sorted = weeks.slice().sort(function(a, b) { return a - b; });
var ranges = [];
var start = sorted[0];
var end = sorted[0];
for (var i = 1; i < sorted.length; i++) {
if (sorted[i] === end + 1) {
end = sorted[i];
} else {
ranges.push(start === end ? start + '周' : start + '-' + end + '周');
start = sorted[i];
end = sorted[i];
}
}
ranges.push(start === end ? start + '周' : start + '-' + end + '周');
return ranges.join(',');
}
/* ================================================================
Course Merge
Groups by (kch, xqj, jcs, cdmc), merges weeks & teachers
================================================================ */
function mergeCourses(data) {
var groups = {};
for (var i = 0; i < data.length; i++) {
var item = data[i];
var key = (item.kch || '') + '|' + (item.xqj || '') + '|' + (item.jcs || '');
if (!groups[key]) groups[key] = [];
groups[key].push(item);
}
var merged = [];
var keys = Object.keys(groups);
for (var k = 0; k < keys.length; k++) {
var group = groups[keys[k]];
if (group.length === 1) {
merged.push(group[0]);
continue;
}
var allWeeks = [];
var seenWeeks = {};
var teacherSet = {};
var teacherOrder = [];
var zfjSet = {};
var zfjOrder = [];
var zcmcSet = {};
var locationSet = {};
var locationOrder = [];
var base = group[0];
for (var g = 0; g < group.length; g++) {
var entry = group[g];
var wArr = parseWeekAdvanced(entry.zcd);
for (var w = 0; w < wArr.length; w++) {
if (!seenWeeks[wArr[w]]) { allWeeks.push(wArr[w]); seenWeeks[wArr[w]] = true; }
}
var names = (entry.xm || '').split(/[,,]/);
for (var t = 0; t < names.length; t++) {
var name = names[t].trim();
if (name && !teacherSet[name]) { teacherSet[name] = true; teacherOrder.push(name); }
}
if (entry.zfjmc) {
var zfjNames = entry.zfjmc.split(/[,,]/);
for (var z = 0; z < zfjNames.length; z++) {
var zn = zfjNames[z].trim();
if (zn && !zfjSet[zn]) { zfjSet[zn] = true; zfjOrder.push(zn); }
}
}
if (entry.zcmc) {
var zcmcNames = entry.zcmc.split(/[,,]/);
for (var c = 0; c < zcmcNames.length; c++) {
var cn = zcmcNames[c].trim();
if (cn) zcmcSet[cn] = true;
}
}
var loc = (entry.cdmc || '').trim();
if (loc && !locationSet[loc]) { locationSet[loc] = true; locationOrder.push(loc); }
}
allWeeks.sort(function(a, b) { return a - b; });
var mergedEntry = {};
for (var prop in base) {
if (base.hasOwnProperty(prop)) mergedEntry[prop] = base[prop];
}
mergedEntry.xm = teacherOrder.join(',');
mergedEntry.zcd = formatWeekStr(allWeeks);
if (zfjOrder.length > 0) mergedEntry.zfjmc = zfjOrder.join(',');
var zcmcArr = Object.keys(zcmcSet);
if (zcmcArr.length > 0) mergedEntry.zcmc = zcmcArr.join(',');
if (locationOrder.length > 0) mergedEntry.cdmc = locationOrder.join('/');
merged.push(mergedEntry);
}
return merged;
}
function getExportData() {
return isMergeEnabled() ? mergeCourses(courseData) : courseData;
}
/* ================================================================
Library Loader (CDN)
================================================================ */
function loadScript(url) {
if (libCache[url]) return libCache[url];
libCache[url] = new Promise(function(resolve, reject) {
var s = document.createElement('script');
s.src = url;
s.onload = function() { resolve(); };
s.onerror = function() { reject(new Error('Failed: ' + url)); };
document.head.appendChild(s);
});
return libCache[url];
}
function loadXlsx() { return loadScript('https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js'); }
function loadJsPdf() { return loadScript('https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js'); }
function loadH2C() { return loadScript('https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js'); }
/* ================================================================
Preview Table (shared by JPG / PDF / DOCX)
================================================================ */
function createPreviewContainer(fields, data) {
var semTitle = '';
if (studentInfo) {
semTitle = (studentInfo.XNMC || '') + '学年 第' + (studentInfo.XQMMC || '') + '学期 课表';
}
var byDay = {};
for (var d = 1; d <= 7; d++) byDay[d] = [];
for (var i = 0; i < data.length; i++) {
var dn = getDayNum(data[i].xqj);
if (dn >= 1 && dn <= 7) byDay[dn].push(data[i]);
}
for (var d2 = 1; d2 <= 7; d2++) {
byDay[d2].sort(function(a, b) { return (parseSessions(a.jcs)[0] || 0) - (parseSessions(b.jcs)[0] || 0); });
}
var ths = '';
for (var h = 0; h < fields.length; h++) ths += '
' + escapeHtml(fields[h].label) + '
';
var rows = '';
for (var day = 1; day <= 7; day++) {
var list = byDay[day];
for (var ci = 0; ci < list.length; ci++) {
var tds = '';
for (var fi = 0; fi < fields.length; fi++) {
tds += '