您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Make ECNU Website Great Again !
// ==UserScript== // @name ECNUPrettier // @namespace http://santonin.top/ // @version 1.1.1 // @description Make ECNU Website Great Again ! // @author Santonin // @match https://applicationnewjw.ecnu.edu.cn/eams/home.action* // @match https://applicationnewjw.ecnu.edu.cn/eams/myPlanCompl.action* // @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/xlsx.full.min.js // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant none // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt // ==/UserScript== let tableData = []; /** 重新累加计算每个非叶子节点的 leafNum */ function accLeafNum(result) { function computeLeafNum(node) { if (!node.children) return; if (node.isLeaf) { if (!node.isFold) node.leafNum = node.children.length; else node.leafNum = 1; return; } node.leafNum = 0; for (const child of node.children) { computeLeafNum(child); node.leafNum += child.leafNum || 1; } } for (const node of result) { computeLeafNum(node); } return result; } /** 表格数据整型 */ function foldItems(items) { const result = []; let lastKind1 = null; let lastKind2 = null; // 第一步:构建树结构 for (const item of items) { if (item.kind === 1) { result.push(item); lastKind1 = item; lastKind2 = null; } else if (item.kind === 2) { if (lastKind1) { lastKind1.children.push(item); lastKind2 = item; } } else if (item.kind === 3) { if (lastKind2) { lastKind2.children.push(item); } } } // 第二步:补全空 children(加一个 null 子节点) function ensureNonEmptyChildren(node) { if (!node.children) return; if (node.children.length === 0) { node.children = [{ isFake: true }]; node.isLeaf = true; node.isFold = false; } else { if (node.children[0].kind === undefined) { node.isLeaf = true; node.isFold = false; } else { node.isLeaf = false; } for (const child of node.children) { if (child) ensureNonEmptyChildren(child); } } } for (const node of result) { ensureNonEmptyChildren(node); } // 第三步:自底向上累加 leafNum return accLeafNum(result); } /** 表格渲染函数 */ function renderTable(data, isExportMode = false) { const $table = $('<table>').css({ width: '100%', borderCollapse: 'collapse', margin: '10px 0px', border: '1px solid #ccc', lineHeight: '20px', fontSize: '14px', }); const thStyle = { border: '1px solid #ccc', padding: '4px 8px' }; const tdStyle = { border: '1px solid #ccc', padding: '4px 8px' }; const $thead = $('<thead>').append( $('<tr>') .css({ backgroundColor: '#F6F6F6' }) .append( $('<th>').text('1').css(thStyle).css({ width: '11%' }), $('<th>').text('2').css(thStyle).css({ width: '9%' }), $('<th>').text('3').css(thStyle).css({ width: '22%' }), $('<th>').text('代码').css(thStyle).css({ width: '13%' }), $('<th>').text('名称').css(thStyle).css({ width: '24%' }), $('<th>').text('学分').css(thStyle).css({ width: '4%' }), $('<th>').text('成绩').css(thStyle).css({ width: '5%' }), $('<th>').text('通过否').css(thStyle).css({ width: '6%' }), $('<th>').text('备注').css(thStyle).css({ width: '6%' }) ) ); const $tbody = $('<tbody>'); function renderCreditUI({ finishedCredit, credit }) { const colors = [ ['#E3FCD1', '#55CB00'], ['#D1EFFC', '#0C92CD'], ['#FEE6E3', '#F1604D'], ]; const colorFlag = finishedCredit - credit >= 0 ? 0 : 2; return $('<div>') .css({ borderRadius: '999px', background: colors[colorFlag][0], color: colors[colorFlag][1], fontWeight: 600, padding: '2px 8px', display: 'flex', justifyContent: 'center', alignItems: 'center', }) .append( $('<p>') .css({ margin: 0, fontSize: '12px', whiteSpace: 'nowrap' }) .text(`${finishedCredit} / ${credit}`) ); } function renderKind(data) { const content = $('<div>') .css({ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px', }) .append($('<p>').css({ margin: 0, fontSize: '14px' }).text(data.name)) .append(renderCreditUI(data)); return $('<div>') .css({ width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center', }) .append(content); } function renderLeafHeader(data) { const content = $('<div>') .css({ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '8px', }) .append($('<p>').css({ margin: 0, fontSize: '14px' }).text(data.name)) .append(renderCreditUI(data)); const res = $('<div>'); res.css({ width: '100%', display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }); const btn = $('<button>'); btn.text(data.isFold ? '展开' : '收纳'); btn.css({ border: 'none', cursor: 'pointer', height: '24px', }); btn.click(() => { data.isFold = !data.isFold; rerender(); }); if (isExportMode) return res.append(content); else return res.append(content).append(btn); } function renderHeader(kind, isLeaf = false) { if (isLeaf) return $('<td>') .html(renderLeafHeader(kind)) .css(tdStyle) .attr('rowspan', kind.leafNum); else return $('<td>') .html(renderKind(kind)) .css(tdStyle) .attr('rowspan', kind.leafNum); } function renderFakeChild(row, child) { const isFake = Boolean(child.children[0].isFake); $tbody.append( row.append( $('<td>') .html(`${isFake ? 0 : child.children.length} 门课程`) .css({ ...tdStyle, textAlign: 'center' }) .attr('colspan', 6) ) ); } function renderTd(row, item) { const { courseSequence, courseCode, courseName, credit, finishedCredit, score, isPass, remark, } = item; $tbody.append( row.append( $('<td>') .css(tdStyle) .html(courseCode ?? ''), $('<td>') .css({ ...tdStyle, color: isPass === '是' ? '' : 'red', }) .html(courseName ?? ''), $('<td>') .css(tdStyle) .html(credit ?? ''), $('<td>') .css(tdStyle) .html(score ?? ''), $('<td>') .css(tdStyle) .html(isPass ?? ''), $('<td>') .css(tdStyle) .html(remark ?? '') ) ); } function addRows(items, $tbody) { items.forEach((kind1, index1) => { kind1.children.forEach((kind2, index2) => { if (kind2.children.length && kind2.children[0].kind === 3) { // 有 kind3 kind2.children.forEach((kind3, index3) => { if (kind3.isFold) { let row = $('<tr>'); if (index2 === 0) { row.append(renderHeader(kind1)); } if (index3 === 0) { row.append(renderHeader(kind2)); } row.append(renderHeader(kind3, true)); // const isFake = Boolean(kind3.children[0].isFake); // $tbody.append( // row.append( // $('<td>') // .html(`${isFake ? 0 : kind3.children.length}门课程`) // .css(tdStyle) // .attr('colspan', 6) // ) // ); renderFakeChild(row, kind3); } else kind3.children.forEach((item, index4) => { let row = $('<tr>'); if (index4 === 0) { if (index2 === 0) { row.append(renderHeader(kind1)); } if (index3 === 0) { row.append(renderHeader(kind2)); } row.append(renderHeader(kind3, true)); } renderTd(row, item); }); }); } else { // 没有 kind3 if (kind2.isFold) { let row = $('<tr>'); if (index2 === 0) { // 如果这是渲染 kind1 的第一行,需要额外渲染左侧表头 row.append(renderHeader(kind1)); } // 如果这是渲染 kind2 的第一行,需要额外渲染左侧表头 row.append(renderHeader(kind2, true).attr('colspan', 2)); // const isFake = Boolean(kind2.children[0].isFake); // $tbody.append( // row.append( // $('<td>') // .html(`${isFake ? 0 : kind2.children.length}门课程`) // .css(tdStyle) // .attr('colspan', 6) // ) // ); renderFakeChild(row, kind2); } else kind2.children.forEach((item, index3) => { let row = $('<tr>'); if (index3 === 0) { if (index2 === 0) { row.append(renderHeader(kind1)); } row.append(renderHeader(kind2, true).attr('colspan', 2)); } renderTd(row, item); }); } }); }); } addRows(data, $tbody); $table.append($thead).append($tbody); return $('<div>') .css({ width: '100%', maxHeight: '100%', overflowY: 'auto', }) .attr('id', 'pretty-table-container') .append($table); } let $modal = null; let $overlay = null; /** 重渲染弹窗内的表格和关闭按钮 */ function rerender() { tableData = accLeafNum(tableData); console.log(tableData); // 记录当前滚动高度,并清空表格的容器 const tableContainer = $('#pretty-table-container'); const scrollTop = $('#pretty-table-container').scrollTop(); tableContainer.empty(); // 重新渲染表格,并滚动至原位置 tableContainer.append(renderTable(tableData)); tableContainer.scrollTop(scrollTop); } /** 关闭弹窗事件 */ function closeModal() { if ($overlay) { $overlay.remove(); $('body > *:not(#custom-overlay)').css('filter', 'none'); } } function goThroughLeaf(node, fn) { if (!node.children) return; if (node.isLeaf) { fn(node); } else { for (const child of node.children) { goThroughLeaf(child, fn); } } } /** 全部展开事件 */ function allUnfold() { for (const node of tableData) { goThroughLeaf(node, (node) => { node.isFold = false; }); } rerender(); } /** 全部收纳事件 */ function allFold() { for (const node of tableData) { goThroughLeaf(node, (node) => { node.isFold = true; }); } rerender(); } /** 展示表格弹窗 */ function showOverlayWithTable(tableData) { // 模糊背景 $('body > *:not(#custom-overlay)').css('filter', 'blur(4px)'); // 遮罩层 $overlay = $('<div id="custom-overlay">').css({ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0, 0, 0, 0.3)', zIndex: 9999, display: 'flex', justifyContent: 'center', alignItems: 'center', }); // 弹窗容器 $modal = $('<div>').css({ background: '#fff', padding: '40px 20px 20px', borderRadius: '8px', boxShadow: '0 0 10px rgba(0,0,0,0.3)', height: '90vh', width: '1200px', minWidth: '800px', position: 'relative', }); // 关闭按钮 const $closeBtn = $('<button>关闭</button>'); $closeBtn.css({ position: 'absolute', top: '12px', right: '12px', cursor: 'pointer', }); $closeBtn.click(closeModal); // 全部展开 const $allUnfold = $('<button>全部展开</button>'); $allUnfold.css({ position: 'absolute', top: '12px', left: '12px', cursor: 'pointer', }); $allUnfold.click(allUnfold); // 全部收纳 const $allFold = $('<button>全部收纳</button>'); $allFold.css({ position: 'absolute', top: '12px', left: '96px', cursor: 'pointer', }); $allFold.click(allFold); // 导出图片 const $exportIMG = $('<button>导出图片</button>'); $exportIMG.css({ position: 'absolute', top: '12px', left: '180px', cursor: 'pointer', }); $exportIMG.click(exportToImage); // 导出pdf const $exportExcel = $('<button>导出Excel</button>'); $exportExcel.css({ position: 'absolute', top: '12px', left: '264px', cursor: 'pointer', }); $exportExcel.click(exportToExcel); $modal .append($closeBtn) .append($allUnfold) .append($allFold) .append($exportIMG) .append($exportExcel) .append(renderTable(tableData)); $overlay.append($modal); $('body').append($overlay); } const kindOne = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']; const kindTwo = [ '(一)', '(二)', '(三)', '(四)', '(五)', '(六)', '(七)', '(八)', '(九)', '(十)', ]; function mainPrettier() { const $chartView = $('#chartView'); if ($chartView) { $('#chartView table tbody tr').each(function () { // 读取表格行 const rowData = []; $(this) .find('td') .each(function () { rowData.push($(this).text().trim()); }); // 整型数据 if (rowData.length === 8) { // 是具体的课程条目 if (tableData.length) { const last = tableData.at(-1); last.leafNum++; last.children.push({ courseSequence: rowData[0], courseCode: rowData[1], courseName: rowData[2], credit: rowData[3], finishedCredit: rowData[4], score: rowData[5], isPass: rowData[6], remark: rowData[7], }); } } else if (rowData.length === 6) { // 是课程所属门类 let kind = 0; if (kindOne.some((prefix) => rowData[0].startsWith(prefix))) { kind = 1; } else if (kindTwo.some((prefix) => rowData[0].startsWith(prefix))) { kind = 2; } else { kind = 3; } if (kind !== 0) { tableData.push({ kind: kind, leafNum: 0, name: rowData[0].replace('(所有子项均应满足要求)', ''), credit: rowData[1], finishedCredit: rowData[2], children: [], }); } } else { console.error('未知的表格行! Unknown table row !'); } }); tableData = foldItems(tableData); console.log(tableData); showOverlayWithTable(tableData); return true; } else { return false; } } /** * 根据当前时间和文件类型生成唯一的文件名 * @param {string} fileType - 文件类型,'excel' | 'image' * @returns {string} 生成的文件名,格式为:[前缀]_[YYYYMMDD_HHMMSS].[扩展名] */ function generateFilename(fileType) { // 获取当前时间 const now = new Date(); // 格式化日期时间为 YYYYMMDD_HHMMSS const formattedDateTime = [ now.getFullYear(), String(now.getMonth() + 1).padStart(2, '0'), String(now.getDate()).padStart(2, '0'), '_', String(now.getHours()).padStart(2, '0'), String(now.getMinutes()).padStart(2, '0'), String(now.getSeconds()).padStart(2, '0'), ].join(''); // 定义文件类型映射表 const fileTypeMap = { excel: { extension: 'xlsx' }, image: { extension: 'png' }, }; // 获取文件类型配置,默认为csv const { extension } = fileTypeMap[fileType] || fileTypeMap.csv; // 返回生成的文件名 return `计划完成情况_${formattedDateTime}.${extension}`; } // 导出为图片 function exportToImage() { // 创建临时容器 const tempContainer = $('<div>'); tempContainer.css({ position: 'fixed', top: '-9999px', left: '-9999px', width: 'auto', padding: '20px', backgroundColor: 'white', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', borderRadius: '8px', }); // 克隆表格并添加到临时容器 const clonedTable = renderTable(tableData, true); tempContainer.append(clonedTable); $('body').append(tempContainer); // 使用html2canvas渲染表格为图片 html2canvas(clonedTable[0], { scale: 2, useCORS: true, logging: false, }).then((canvas) => { // 转换为图片URL并下载 const imgData = canvas.toDataURL('image/png'); const test = 'test'; const $link = $('<a>').attr({ download: generateFilename('image'), href: imgData, }); $link[0].click(); // 移除临时容器 tempContainer.remove(); }); } // 导出为Excel function exportToExcel() { const table = renderTable(tableData, true); const wb = XLSX.utils.table_to_book(table[0], { sheet: 'Sheet1' }); XLSX.writeFile(wb, generateFilename('excel')); } // 下载文件 function downloadFile(content, filename, contentType) { const a = document.createElement('a'); const file = new Blob([content], { type: contentType }); a.href = URL.createObjectURL(file); a.download = filename; a.click(); URL.revokeObjectURL(a.href); } (function () { ('use strict'); $(document).ready(function () { console.log("I'm Santonin"); const myBtn = $('<div>'); myBtn.text('Prettified Table').css({ position: 'fixed', bottom: '20px', right: '20px', boxShadow: '4px 4px 12px rgba(0, 0, 0, 0.2)', borderRadius: '999px', height: '40px', padding: '0px 12px', backgroundColor: 'white', display: 'flex', justifyContent: 'center', alignItems: 'center', userSelect: 'none', cursor: 'pointer', }); myBtn.click(() => { if (tableData.length) { showOverlayWithTable(tableData); return; } const res = mainPrettier(); if (!res) { console.log('当前页面没有chartView'); } }); $('body').append(myBtn); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址