// ==UserScript== // @name 内蒙古高考招生计划添加彩色标签 // @namespace http://tampermonkey.net/ // @version 2026.7.2.1 // @description 【添加彩色标签后的页面仅供参考,本人不对其负责】脚本原理只是将某些文本变为彩色背景,不会改变文本内容。增加GM菜单:院校彩色标记、专业彩色标记(均默认关闭)。 // @author AN drew // @match https://www.nm.zsks.cn/*gkwb/*zsjh/gkjh*/jh/jhkl.html* // @match https://www.nm.zsks.cn/*gkwb/*zsjh/gkjh*/jh/jhyx.html* // @match https://www.nm.zsks.cn/*gkwb/*zsjh/gkjh*/jh/jhzy.html* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_addValueChangeListener // @grant GM_registerMenuCommand // @run-at document-end // ==/UserScript== (function() { 'use strict'; let contextMenu = null; // 自定义右键菜单元素 // ====================== 配置区 ====================== const CONFIG = { // 标签颜色 colors: { direction: '#e74c3c', // 定向 - 红色 national: '#3498db', // 国家专项计划 - 蓝色 local: '#27ae60', // 地方专项计划 - 绿色 university: '#f39c12', // 高校专项计划 - 金色 freeMedical: '#e84393', // 国家免费医学生 - 粉红 nationalTeacher: '#ad1457',// 国家公费师范生 - 紫红 localTeacher: '#722d8e', // 地方公费师范生 - 深紫色 excellentTeacher: '#2e7d32',// 优师专项 - 深绿色 military: '#215e21', // 定向培养军士生 - 军绿色 comprehensive: '#867171', // 综合评价 - 灰紫 examSchool: '#7a7bd6', // 艺考类(校考) - 浅紫蓝 examTong: '#e06d6d', // 艺考类(统考) - 浅红 planSuffix: '#8e44ad', // 专业名称中的(XXX专项计划) - 紫色 teacher: '#d35400', // 专业名称中的(师范类) - 棕色 location: '#2c3e50', // 专业名称中的(定向到XX) - 深灰蓝 officer: '#005737', // 专业名称中的(XX军官) - 松枝绿 police: '#2c3d63', // 专业名称中的(XX警官)- 警服蓝 project: '#e67e22', // 备注中的XXX项目 - 橙色 base: '#1abc59', // 备注中的XXX基地 - 浅绿色 language: '#167b67', // 外语语种 - 墨绿 subject: '#00b894', // 选科要求 - 薄荷绿 army: '#7eb526', // 陆军 navy: '#000062', // 海军 airForce: '#508ce4', // 空军 rocket: '#ea7901', // 火箭军 spaceForce: '#1d3b79', // 军事航天部队 cyberForce: '#65666a', // 网络空间部队 infoSupport: '#640772', // 信息支援部队 jointLogistics: '#296d3e', // 联勤保障部队 armedPolice: '#134a2d', // 武警部队 fireRescue: '#DE5306', // 消防救援队伍 }, // 匹配关键词 keywords: { direction: '定向', national: '国家专项计划', local: '地方专项计划', university: '高校专项计划', freeMedical: '国家免费医学生', nationalTeacher: '国家公费师范生', localTeacher: '地方公费师范生', excellentTeacher: '优师专项', military: '定向培养军士生', comprehensive: '综合评价', examSchool: '艺考类(校考)', examTong: '艺考类(统考)', planSuffix: /([^)]*专项计划)/g, // 匹配(XXX专项计划) teacher: /(师范(?:类)?)/g, // 匹配(师范类) location: /(定向到[^)]*)|定向到[^,,。]*|((?![^)]*(?:自治区|直辖|城市))[^)]*[市县旗])/g, // 匹配(定向到XX) officer: /((?:[^()]|([^()]*))*军官(?:[^()]|([^()]*))*)/g, // 匹配(XX军官) police: /([^)]*?(?:警官|入警就业|地方公安方向|移民方向|铁路公安方向|法院警务方向|检察院警务方向|狱内侦查方向|戒毒方向))/g, // 匹配(XX警官) project: /[^,,。、;:]{2,}(?`; const ICON_PENDING = ``; const ICON_FAIL = ``; const ICON_UNDO = ``; */ const ICON_PASS = ''; const ICON_PENDING = ''; const ICON_FAIL = ''; const ICON_UNDO = ' '; // ====================== 工具函数 ====================== const FEMALE_SVG = ``; const MALE_SVG = ``; /** 在男生/女生后面添加对应svg图标 */ function appendGenderIcon(text) { if (typeof text !== 'string') return text; // 如果已包含SVG,直接返回原文本 if (text.includes(' el.remove()); clone.querySelectorAll('svg').forEach(el => el.remove()); // 新增移除 SVG return clone.textContent.trim(); } /** 安全设置单元格HTML,保留原有结构 */ function setCellHTML(td, html) { if (!td) return; td.innerHTML = html; } /** 对文本中的匹配项进行替换,返回HTML字符串 */ function replaceWithTag(text, regex, tagClass, tagText) { if (!text) return text; // 如果tagText未提供,使用匹配到的完整文本 return text.replace(regex, function(match) { const displayText = tagText || match; return `${displayText}`; }); } /** 处理单个单元格:根据列索引应用不同规则 */ function processCell(td, colIndex) { if (!td) return; // 如果已经包含标签,直接返回,不再处理 if (td.querySelector('.tag-label')) { return; } const rawText = getCellText(td); if (!rawText) return; let result = rawText; // ---- 列2: 专业名称 ---- if (colIndex === 2) { let modified = rawText; let hasDirectionalOfficer = false; // 定向军士专用标志 let hasBlockTag = false; // 是否已存在块级标签(避免二次标注) // 匹配(定向军士(XX)) if (CONFIG.keywords.directionalOfficer.test(rawText)) { CONFIG.keywords.directionalOfficer.lastIndex = 0; modified = modified.replace(CONFIG.keywords.directionalOfficer, function(fullMatch, inner) { const tagClass = CONFIG.branchMap[inner.trim()] || 'unknown'; return `${fullMatch}`; }); hasDirectionalOfficer = true; hasBlockTag = true; } // 优先匹配特定的专项计划(国家、高校、地方) const specificPlanMap = { '(国家专项计划)': 'national', '(地方专项计划)': 'local', '(高校专项计划)': 'university' }; for (const [text, tag] of Object.entries(specificPlanMap)) { if (rawText.includes(text)) { modified = modified.replace(text, `${text}`); hasBlockTag = true; break; } } // 匹配 (XXX专项计划)—— planSuffix(仅当未处理过特定专项计划时) if (!hasBlockTag && CONFIG.keywords.planSuffix.test(rawText)) { CONFIG.keywords.planSuffix.lastIndex = 0; modified = replaceWithTag(modified, CONFIG.keywords.planSuffix, 'tag-planSuffix'); hasBlockTag = true; } // 匹配 (师范类)—— teacher if (CONFIG.keywords.teacher.test(rawText)) { CONFIG.keywords.teacher.lastIndex = 0; modified = replaceWithTag(modified, CONFIG.keywords.teacher, 'tag-teacher'); hasBlockTag = true; } // 匹配(定向到XX)—— location if (CONFIG.keywords.location.test(rawText)) { CONFIG.keywords.location.lastIndex = 0; modified = replaceWithTag(modified, CONFIG.keywords.location, 'tag-location'); hasBlockTag = true; } // 匹配(XX军官)—— officer if (CONFIG.keywords.officer.test(rawText)) { CONFIG.keywords.officer.lastIndex = 0; modified = replaceWithTag(modified, CONFIG.keywords.officer, 'tag-officer'); hasBlockTag = true; } // 匹配(XX警官)—— police if (CONFIG.keywords.police.test(rawText)) { CONFIG.keywords.police.lastIndex = 0; modified = replaceWithTag(modified, CONFIG.keywords.police, 'tag-police'); hasBlockTag = true; } // 匹配(纯军种括号) if (!hasDirectionalOfficer && CONFIG.keywords.branchInParen.test(rawText)) { CONFIG.keywords.branchInParen.lastIndex = 0; modified = modified.replace(CONFIG.keywords.branchInParen, function(match) { const key = match.replace(/(|)/g, ''); const tagClass = CONFIG.branchMap[key] || 'unknown'; return `${match}`; }); hasBlockTag = true; } // 匹配(数字番号+部队)整体作为块级标签,使用 military 颜色 if (CONFIG.keywords.numberTroopParen.test(rawText)) { CONFIG.keywords.numberTroopParen.lastIndex = 0; modified = modified.replace(CONFIG.keywords.numberTroopParen, function(match) { return `${match}`; }); hasBlockTag = true; } // 匹配(军种军士(性别))整体作为块级标签 const militarySergeantRegex = /((陆军|海军|空军|火箭军|军事航天部队|网络空间部队|信息支援部队|联勤保障部队|武警部队|武警)军士([男女]))/g; if (militarySergeantRegex.test(rawText)) { militarySergeantRegex.lastIndex = 0; modified = modified.replace(militarySergeantRegex, function(match, branch) { const tagClass = CONFIG.branchMap[branch] || 'unknown'; return `${match}`; }); hasBlockTag = true; } // 匹配(军种+性别)格式,如(火箭军男)、(陆军女) const branchGenderRegex = /((陆军|海军|空军|火箭军|军事航天部队|网络空间部队|信息支援部队|联勤保障部队|武警部队|武警)([男女]))/g; if (branchGenderRegex.test(rawText)) { branchGenderRegex.lastIndex = 0; modified = modified.replace(branchGenderRegex, function(match, branch, gender) { const tagClass = CONFIG.branchMap[branch] || 'unknown'; const genderSVG = gender === '男' ? MALE_SVG : FEMALE_SVG; return `(${branch}${gender}${genderSVG})`; }); hasBlockTag = true; // 关键:阻止后续 generalBranchRegex 重复匹配 } // 匹配(XX消防救援队伍,只招男生)整体作为块级标签 const fireRescueRegex = /([^)]*消防救援队伍[^)]*)/g; if (fireRescueRegex.test(rawText)) { fireRescueRegex.lastIndex = 0; modified = modified.replace(fireRescueRegex, function(match) { return `${match}`; }); hasBlockTag = true; } // 匹配(定向军种)整体作为块级标签 const directionalBranchRegex = /(定向(陆军|海军|空军|火箭军|军事航天部队|网络空间部队|信息支援部队|联勤保障部队|武警部队|武警))/g; if (directionalBranchRegex.test(rawText)) { directionalBranchRegex.lastIndex = 0; modified = modified.replace(directionalBranchRegex, function(match, branch) { const tagClass = CONFIG.branchMap[branch] || 'unknown'; return `${match}`; }); hasBlockTag = true; } // 匹配(与XX联合培养)—— 只标注军种,行内显示 const jointTrainingRegex = /(与(陆军|海军|空军|火箭军|军事航天部队|网络空间部队|信息支援部队|联勤保障部队|武警部队|武警)联合培养)/g; if (jointTrainingRegex.test(rawText)) { jointTrainingRegex.lastIndex = 0; modified = modified.replace(jointTrainingRegex, function(match, branch) { const tagClass = CONFIG.branchMap[branch] || 'unknown'; return `(与${branch}联合培养)`; }); // 行内标签不设置 hasBlockTag } // 匹配(定向培养军士,XX,男生/女生)—— 只标注军种,行内显示 const embeddedBranchRegex = /(定向培养军士,\s*(陆军|海军|空军|火箭军|军事航天部队|网络空间部队|信息支援部队|联勤保障部队|武警部队|武警)\s*,[男女]生)/g; if (embeddedBranchRegex.test(rawText)) { embeddedBranchRegex.lastIndex = 0; modified = modified.replace(embeddedBranchRegex, function(match, branch) { const tagClass = CONFIG.branchMap[branch] || 'unknown'; return match.replace(branch, `${branch}`); }); // 行内标签不设置 hasBlockTag } // 通用:匹配括号内包含军种且还有其他内容的情况(只标注军种,行内显示) // 仅在尚未有块级标签覆盖时才执行,避免二次标注 if (!hasBlockTag) { const generalBranchRegex = /(([^)]*)(陆军|海军|空军|火箭军|军事航天部队|网络空间部队|信息支援部队|联勤保障部队|武警部队|武警)([^)]*))/g; if (generalBranchRegex.test(modified)) { generalBranchRegex.lastIndex = 0; modified = modified.replace(generalBranchRegex, function(match, before, branch, after) { if (before.trim() === '' && after.trim() === '') { return match; } // 如果匹配内容包含“定向”、“军士”或“联合培养”,说明已被其他正则处理,跳过 if (match.includes('定向') || match.includes('军士') || match.includes('联合培养')) { return match; } // 判断军种是否独立:前后是分隔符或边界 const beforeChar = before.charAt(before.length - 1); const afterChar = after.charAt(0); const isSeparator = (ch) => /[,,、。;:]/.test(ch) || ch === ''; const beforeOk = isSeparator(beforeChar) || before === ''; const afterOk = isSeparator(afterChar) || after === ''; if (!(beforeOk && afterOk)) { // 前后不都是分隔符/边界,说明军种是其他词的一部分,跳过 return match; } const tagClass = CONFIG.branchMap[branch] || 'unknown'; return `(${before}${branch}${after})`; }); } } // 在男生/女生后面添加对应svg图标 modified = appendGenderIcon(modified); result = modified; } // ---- 列3: 计划性质 ---- else if (colIndex === 3) { if (rawText === CONFIG.keywords.direction) { result = `${rawText}`; } else if (rawText === CONFIG.keywords.freeMedical) { result = `${rawText}`; } else if (rawText === CONFIG.keywords.localTeacher) { result = `${rawText}`; } else if (rawText === CONFIG.keywords.nationalTeacher) { result = `${rawText}`; } else if (rawText === CONFIG.keywords.excellentTeacher) { result = `${rawText}`; } } // ---- 列4: 计划类别 ---- else if (colIndex === 4) { if (rawText === CONFIG.keywords.comprehensive) { result = `${rawText}`; } else if (rawText === CONFIG.keywords.examSchool) { result = `${rawText}`; } else if (rawText === CONFIG.keywords.examTong) { result = `${rawText}`; } else if (rawText === CONFIG.keywords.national) { result = `${rawText}`; } else if (rawText === CONFIG.keywords.local) { result = `${rawText}`; } else if (rawText === CONFIG.keywords.university) { result = `${rawText}`; } else if (rawText === CONFIG.keywords.military) { result = `${rawText}`; } } // ---- 列7: 外语语种 ---- else if (colIndex === 7) { if (rawText && rawText !== '不限') { const parts = rawText.match(/[^,,]+/g) || []; const cleanParts = parts.map(s => s.trim()).filter(s => s !== ''); if (cleanParts.length === 0) { result = rawText; } else if (cleanParts.length === 1) { result = `${cleanParts[0]}`; } else { const tagged = cleanParts.map(p => `${p}`).join(''); result = tagged; } } } // ---- 列9: 选科要求 ---- else if (colIndex === 9) { if (rawText && rawText !== '-' && rawText !== '不限' && rawText !== '不提科目要求') { // 分离括号内容(保留)和前面的科目部分 let bracketContent = ''; let cleanText = rawText; // 匹配最后一个括号对(支持中文或英文括号) const match = rawText.match(/[((][^))]*[))]$/); if (match) { bracketContent = match[0]; cleanText = rawText.substring(0, match.index).trim(); } // 如果清理后为空,则保持原样 if (!cleanText) { result = rawText; } else { const parts = cleanText.match(/[^,,]+/g) || []; const cleanParts = parts.map(s => s.trim()).filter(s => s !== ''); if (cleanParts.length === 0) { result = rawText; } else { const tagged = cleanParts.map(p => `${p}`).join(''); // 将括号内容作为纯文本追加 result = tagged + bracketContent; } } } } // ---- 列10: 专业备注 ---- else if (colIndex === 10) { let modified = rawText; // 匹配 XXX项目 if (CONFIG.keywords.project.test(rawText)) { CONFIG.keywords.project.lastIndex = 0; modified = replaceWithTag(modified, CONFIG.keywords.project, 'tag-project'); } // 匹配 XXX基地 if (CONFIG.keywords.base.test(rawText)) { CONFIG.keywords.base.lastIndex = 0; modified = replaceWithTag(modified, CONFIG.keywords.base, 'tag-base'); } // 匹配 定向到XX if (CONFIG.keywords.location.test(rawText)) { CONFIG.keywords.location.lastIndex = 0; modified = replaceWithTag(modified, CONFIG.keywords.location, 'tag-location'); } // 匹配独立军种(前后有分隔符) if (CONFIG.keywords.branchStandalone.test(rawText)) { CONFIG.keywords.branchStandalone.lastIndex = 0; modified = modified.replace(CONFIG.keywords.branchStandalone, function(match) { const tagClass = CONFIG.branchMap[match] || 'unknown'; return `${match}`; }); } // 匹配独立数字番号+部队(如 63650部队),行内标签 if (CONFIG.keywords.numberTroopStandalone.test(modified)) { CONFIG.keywords.numberTroopStandalone.lastIndex = 0; modified = modified.replace(CONFIG.keywords.numberTroopStandalone, function(match) { return `${match}`; }); } //在男生/女生后面添加对应svg图标 modified = appendGenderIcon(modified); result = modified; } // ---- 列12: 招生计划数 ---- // 招生计划数特殊处理:若为0,直接设置文字颜色为红色,不创建标签 if (colIndex === 12) { const rawText = getCellText(td); const num = parseInt(rawText.trim(), 10); if (num === 0) { td.style.color = '#ff1744'; } return; // 不再执行后续逻辑 } // 只有发生变化且与当前内容不同时才更新 if (result !== rawText) { const newHTML = result; if (td.innerHTML !== newHTML) { setCellHTML(td, newHTML); } } } /** 处理表格中的所有行 */ function processTable() { const table = document.querySelector('#table'); if (!table) return; const tbody = table.querySelector('tbody'); if (!tbody) return; const rows = tbody.querySelectorAll('tr'); if (rows.length === 0) return; // 遍历每一行,处理特定列 rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length < 13) return; // 确保列数足够 // 列索引:0专业组,1代号,2专业,3性质,4类别,5学制,6学费,7语种,8口试,9选科,10备注,11地点,12计划数 // 需要处理的列:2(专业名称), 3(计划性质), 4(计划类别),7(外语语种), 9(选科要求),10(专业备注),12(招生计划数) processCell(cells[2], 2); processCell(cells[3], 3); processCell(cells[4], 4); processCell(cells[7], 7); processCell(cells[9], 9); processCell(cells[10], 10); processCell(cells[12], 12); }); // 在所有行处理完成后,表头同步列宽(延迟确保 DOM 更新) setTimeout(syncColumnWidth, 100); } //表头同步列宽 function syncColumnWidth() { const dataTable = document.querySelector('#table'); if (!dataTable) return; const tbody = dataTable.querySelector('tbody'); if (!tbody) return; const firstRow = tbody.querySelector('tr'); if (!firstRow) return; const dataCells = firstRow.querySelectorAll('td'); if (dataCells.length === 0) return; const headerContainer = document.querySelector('.fixed-table-header'); if (!headerContainer) return; const headerRow = headerContainer.querySelector('tr'); if (!headerRow) return; const headerCells = headerRow.querySelectorAll('th'); if (headerCells.length !== dataCells.length) return; // 同步表头表格整体宽度(防止滚动条/布局差异) const headerTable = headerContainer.querySelector('table'); const dataTableRect = dataTable.getBoundingClientRect(); if (headerTable) { headerTable.style.width = dataTableRect.width + 'px'; headerTable.style.tableLayout = 'fixed'; } // 逐列设置宽度(使用 getBoundingClientRect 获取精确像素) dataCells.forEach((td, index) => { const width = td.getBoundingClientRect().width; if (width > 0) { const th = headerCells[index]; th.style.width = width + 'px'; th.style.minWidth = width + 'px'; th.style.maxWidth = width + 'px'; } }); } // ====================== 彩色标记功能 ====================== // ========== 存储 ========== // 根据当前年份生成存储键名,确保每年招生计划独立 // 只在点击审核按钮时写入数据,读取和写入均使用同一键,不会重复创建 const CURRENT_YEAR = new Date().getFullYear(); const STORAGE_KEY = 'nmg_university_plan_' + CURRENT_YEAR; // JSON配置结构: // { // "院校path": { // "yx_status": "pass" | "pending" | "fail" | null, // "majors": { // "专业代号": { "zy_status": "pass" | "pending" | "fail" | null } // } // } // } function getStatusMap() { const raw = GM_getValue(STORAGE_KEY, '{}'); try { return JSON.parse(raw); } catch (e) { return {}; } } function setStatusMap(map) { GM_setValue(STORAGE_KEY, JSON.stringify(map)); } function getCurrentPath() { const url = new URL(window.location.href); return url.searchParams.get('path') || ''; } // ---------- 院校状态操作 ---------- function getInstitutionStatus(path) { const map = getStatusMap(); const entry = map[path]; if (!entry) return null; return entry.yx_status || null; // 若不存在或为 null 返回 null } function setInstitutionStatus(path, status) { const map = getStatusMap(); if (!map[path]) { map[path] = { majors: {} }; } if (status === null || status === undefined) { delete map[path].yx_status; // 如果该院校没有专业数据,删除整个条目(可选) if (!map[path].majors || Object.keys(map[path].majors).length === 0) { delete map[path]; } } else { map[path].yx_status = status; } setStatusMap(map); } // ---------- 专业状态操作 ---------- function getMajorStatus(path, majorCode) { const map = getStatusMap(); const entry = map[path]; if (!entry) return null; if (entry.majors && entry.majors[majorCode]) { return entry.majors[majorCode].zy_status || null; } return null; } function setMajorStatus(path, majorCode, status) { const map = getStatusMap(); if (!map[path]) { map[path] = { majors: {} }; } if (!map[path].majors) { map[path].majors = {}; } if (status === null || status === undefined) { if (map[path].majors[majorCode]) { delete map[path].majors[majorCode]; } // 如果 majors 为空,可删除 majors 属性 if (Object.keys(map[path].majors).length === 0) { delete map[path].majors; } // 如果既没有 yx_status 也没有 majors,删除整个条目 if (!map[path].yx_status && !map[path].majors) { delete map[path]; } } else { map[path].majors[majorCode] = { zy_status: status }; } setStatusMap(map); } // ========== 标题监听(通用函数,合并自辅助脚本) ========== function watchTitleFromH3(selector) { const targetNode = document.querySelector(selector); if (!targetNode) return; const updateTitle = () => { const text = targetNode.textContent.trim(); if (!text) return; const newTitle = '内蒙古教育考试院 ' + text; if (document.title !== newTitle) { document.title = newTitle; } }; updateTitle(); const observer = new MutationObserver(updateTitle); observer.observe(targetNode, { characterData: true, childList: true, subtree: true }); } // ========== Toast 提示 ========== function showToast(message, bgColor = '#333') { const toast = document.createElement('div'); toast.textContent = message; toast.style.cssText = ` position: fixed; bottom: 120px; right: 40px; z-index: 10001; padding: 10px 20px; border-radius: 6px; font-family: 'Microsoft YaHei', sans-serif; font-size: 14px; color: white; background: ${bgColor}; box-shadow: 0 4px 10px rgba(0,0,0,0.2); opacity: 0; transform: translateY(10px); transition: opacity 0.3s, transform 0.3s; `; document.body.appendChild(toast); requestAnimationFrame(() => { toast.style.opacity = '1'; toast.style.transform = 'translateY(0)'; }); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateY(-20px)'; toast.addEventListener('transitionend', () => toast.remove()); }, 1500); } // ========== jhyx 列表页着色(先重置再着色)========== function applyStatusColors() { const statusMap = getStatusMap(); const rows = document.querySelectorAll('#table tbody tr'); // 清除所有行背景色 rows.forEach(row => { row.style.backgroundColor = ''; }); // 如果没有任何存储数据,直接返回 if (Object.keys(statusMap).length === 0) return; const url = new URL(window.location.href); const path = url.searchParams.get('path') || ''; const pathname = window.location.pathname; const planMatch = pathname.match(/gkjh_\d+_(\d+)/); if (!planMatch) return; const planNumber = planMatch[1]; const prefix = planNumber + path; const colorMap = { pass: '#d4edda', pending: '#fff3cd', fail: '#f8d7da' }; rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length < 2) return; const code = cells[1].textContent.trim(); const fullPath = prefix + code; // 仅读取院校状态,不再回退到专业状态 const status = getInstitutionStatus(fullPath); if (status) { row.style.backgroundColor = colorMap[status] || ''; } }); } function watchDOMChanges() { const observer = new MutationObserver(() => applyStatusColors()); observer.observe(document.body, { childList: true, subtree: true }); } // ========== 浮动审核面板(jhzy 页面)========== function createFloatingPanel() { const path = getCurrentPath(); if (!path) return; const oldPanel = document.getElementById('review-float-panel'); if (oldPanel) oldPanel.remove(); const currentStatus = getInstitutionStatus(path); // 可能为 null const btnColors = { pass: '#28a745', pending: '#ffc107', fail: '#dc3545' }; const statusTexts = { pass: '通过', pending: '待定', fail: '不通过' }; const panel = document.createElement('div'); panel.id = 'review-float-panel'; panel.style.cssText = ` position: fixed; bottom: 30px; right: 30px; z-index: 10000; width: 240px; height: 60px; background: #ffffff; border: 2px solid #ddd; border-radius: 10px; display: flex; align-items: center; justify-content: center; overflow: hidden; font-family: 'Microsoft YaHei', sans-serif; `; const animLayer = document.createElement('div'); animLayer.style.cssText = ` position: absolute; top: 0; left: 0; right: 0; bottom: 0; transform-origin: center; background: transparent; transform: scaleX(0); z-index: 2; border-radius: 10px; pointer-events: none; `; panel.appendChild(animLayer); const contentDiv = document.createElement('div'); contentDiv.style.cssText = ` position: relative; z-index: 3; display: flex; gap: 8px; `; panel.appendChild(contentDiv); const createUndoButton = () => { const undoBtn = document.createElement('button'); undoBtn.textContent = '撤销'; undoBtn.style.cssText = ` padding: 8px 20px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold; color: #1565c0; background: #ffffff; border: 1px solid #1565c0; transition: transform 0.15s, box-shadow 0.15s; `; undoBtn.addEventListener('mouseenter', () => { undoBtn.style.transform = 'scale(1.05)'; undoBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)'; }); undoBtn.addEventListener('mouseleave', () => { undoBtn.style.transform = 'scale(1)'; undoBtn.style.boxShadow = 'none'; }); undoBtn.addEventListener('click', () => { setInstitutionStatus(path, null); // 清除院校状态 createFloatingPanel(); // 重建面板 }); return undoBtn; }; const setStatusWithAnimation = (status) => { setInstitutionStatus(path, status); contentDiv.innerHTML = ''; animLayer.style.background = btnColors[status]; animLayer.style.transition = 'transform 0.4s ease'; animLayer.style.transform = 'scaleX(0)'; animLayer.offsetHeight; animLayer.style.transform = 'scaleX(1)'; const onTransitionEnd = () => { animLayer.removeEventListener('transitionend', onTransitionEnd); animLayer.style.transition = ''; animLayer.style.transform = ''; animLayer.style.background = 'transparent'; panel.style.backgroundColor = btnColors[status]; contentDiv.appendChild(createUndoButton()); }; animLayer.addEventListener('transitionend', onTransitionEnd); showToast(`院校状态已设为 ${statusTexts[status]}`, btnColors[status]); }; if (currentStatus) { panel.style.backgroundColor = btnColors[currentStatus]; contentDiv.appendChild(createUndoButton()); } else { const createBtn = (text, color, status) => { const btn = document.createElement('button'); btn.textContent = text; btn.style.cssText = ` padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold; color: white; background-color: ${color}; transition: transform 0.15s, box-shadow 0.15s; `; btn.addEventListener('mouseenter', () => { btn.style.transform = 'scale(1.05)'; btn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)'; }); btn.addEventListener('mouseleave', () => { btn.style.transform = 'scale(1)'; btn.style.boxShadow = 'none'; }); btn.addEventListener('click', () => setStatusWithAnimation(status)); return btn; }; contentDiv.appendChild(createBtn('通过', btnColors.pass, 'pass')); contentDiv.appendChild(createBtn('待定', btnColors.pending, 'pending')); contentDiv.appendChild(createBtn('不通过', btnColors.fail, 'fail')); } document.body.appendChild(panel); } // ========== 专业按钮相关(jhzy 页面) ========== // 更新单行背景色(同时更新按钮组,但这里只负责行背景) function updateRowStatus(row, path, majorCode) { const status = getMajorStatus(path, majorCode); // 直接取专业状态,不回退院校 const colorMap = { pass: '#d4edda', pending: '#fff3cd', fail: '#f8d7da' }; row.style.backgroundColor = status ? colorMap[status] : ''; } // 更新所有行状态(用于批量刷新) function updateAllRowStatuses() { const path = getCurrentPath(); const rows = document.querySelectorAll('#table tbody tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length < 2) return; const majorCode = cells[1].textContent.trim(); if (majorCode) { updateRowStatus(row, path, majorCode); } }); } // 显示专业菜单(由双击触发) function showMajorMenu(row, majorCode, clientX, clientY) { const path = getCurrentPath(); const currentStatus = getMajorStatus(path, majorCode); // 移除已有菜单 if (contextMenu) { contextMenu.remove(); contextMenu = null; } const menu = document.createElement('div'); menu.style.cssText = ` position: fixed; left: ${clientX}px; top: ${clientY}px; background: white; border: 1px solid #ccc; border-radius: 6px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); padding: 4px 0; z-index: 10000; min-width: 120px; font-family: 'Microsoft YaHei', sans-serif; user-select: none; `; /* const items = [ { label: '✅ 通过', status: 'pass' }, { label: '⏸️ 待定', status: 'pending' }, { label: '❌ 不通过', status: 'fail' } ]; if (currentStatus) { items.push({ label: '↩️ 撤销', status: null }); } */ const items = [ { label: ICON_PASS + ' 通过', status: 'pass' }, { label: ICON_PENDING + ' 待定', status: 'pending' }, { label: ICON_FAIL + ' 不通过', status: 'fail' } ]; if (currentStatus) { items.push({ label: ICON_UNDO + ' 撤销', status: null }); } items.forEach(({ label, status }) => { const item = document.createElement('div'); item.innerHTML = label; item.style.cssText = ` padding: 6px 20px; cursor: pointer; font-size: 14px; color: #333; transition: background 0.1s; user-select: none; display: flex; align-items: center; gap: 6px; `; item.addEventListener('mouseenter', function() { this.style.background = '#f0f0f0'; }); item.addEventListener('mouseleave', function() { this.style.background = ''; }); item.addEventListener('click', function(e) { e.stopPropagation(); setMajorStatus(path, majorCode, status); const colorMap = { pass: '#d4edda', pending: '#fff3cd', fail: '#f8d7da' }; if (status) { console.log('设置颜色:', status, colorMap[status], row); row.style.backgroundColor = colorMap[status]; // 强制内联样式,增加 !important row.style.setProperty('background-color', colorMap[status], 'important'); } else { updateRowStatus(row, path, majorCode); } const panel = document.getElementById('review-float-panel'); if (panel) createFloatingPanel(); menu.remove(); contextMenu = null; }); menu.appendChild(item); }); document.body.appendChild(menu); contextMenu = menu; // 点击其他区域关闭菜单 const closeMenu = function(e) { if (menu && !menu.contains(e.target)) { menu.remove(); contextMenu = null; document.removeEventListener('click', closeMenu); } }; setTimeout(() => { document.addEventListener('click', closeMenu); }, 10); } // ========== 实时监听(jhyx 页面)========== if (window.location.href.includes('/jh/jhyx.html')) { GM_addValueChangeListener(STORAGE_KEY, () => { applyStatusColors(); }); } // ====================== GM菜单注册 ====================== function registerMenu() { // 院校彩色标记 const instState = GM_getValue('enableInstitutionMark', false); const instMenu = '院校彩色标记(' + (instState ? '已开启' : '已关闭') + ')'; GM_registerMenuCommand(instMenu, function() { const current = GM_getValue('enableInstitutionMark', false); const newVal = !current; GM_setValue('enableInstitutionMark', newVal); showToast('院校彩色标记已' + (newVal ? '开启' : '关闭'), newVal ? '#28a745' : '#dc3545'); location.reload(); }); // 专业彩色标记 const majorState = GM_getValue('enableMajorMark', false); const majorMenu = '专业彩色标记(' + (majorState ? '已开启' : '已关闭') + ')'; GM_registerMenuCommand(majorMenu, function() { const current = GM_getValue('enableMajorMark', false); const newVal = !current; GM_setValue('enableMajorMark', newVal); showToast('专业彩色标记已' + (newVal ? '开启' : '关闭'), newVal ? '#28a745' : '#dc3545'); location.reload(); }); } // ====================== 主逻辑 ====================== /** 初始化:设置MutationObserver监听表格变化 */ function init() { // ---------- 标题增强(所有页面通用) ---------- const url = window.location.href; const isJhzy = url.includes('/jh/jhzy.html'); const isJhyx = url.includes('/jh/jhyx.html'); const isJhkl = url.includes('/jh/jhkl.html'); // 根据当前页面只监听对应的标题元素 if (isJhkl) watchTitleFromH3('h3.jhkltitle'); else if (isJhyx) watchTitleFromH3('h3.jhyxtitle'); else if (isJhzy) watchTitleFromH3('h3.jhzytitle'); // ---------- 院校彩色标记 ---------- const enableInstitutionMark = GM_getValue('enableInstitutionMark', false); if (enableInstitutionMark) { if (isJhyx) { applyStatusColors(); watchDOMChanges(); // 存储变化监听(外部已有) } else if (isJhzy) { createFloatingPanel(); } } // ---------- 专业彩色标记(仅jhzy) ---------- const enableMajorMark = GM_getValue('enableMajorMark', false); if (isJhzy && enableMajorMark) { // 使用点击事件模拟双击(两次点击间隔小于 300ms 且在同一行) const table = document.querySelector('#table'); if (table) { let lastClickTime = 0; let lastClickRow = null; let clickTimer = null; table.addEventListener('click', function(e) { const row = e.target.closest('tr'); if (!row) return; const cells = row.querySelectorAll('td'); if (cells.length < 2) return; const majorCode = cells[1].textContent.trim(); if (!majorCode) return; const now = Date.now(); // 如果是同一行且时间间隔小于 300ms,视为双击 if (row === lastClickRow && now - lastClickTime < 300) { if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; } showMajorMenu(row, majorCode, e.clientX, e.clientY); lastClickTime = 0; lastClickRow = null; } else { // 记录本次点击,并设置定时器清除记录 lastClickTime = now; lastClickRow = row; if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; } clickTimer = setTimeout(() => { lastClickTime = 0; lastClickRow = null; clickTimer = null; }, 300); } }); } // 初始化所有行的背景色(读取存储) setTimeout(() => { updateAllRowStatuses(); }, 100); } // ---------- 3. 彩色标签处理(仅jhzy页面) ---------- if (isJhzy) { // ====================== 注入样式 ====================== // 自动生成所有标签样式 const styleRules = Object.keys(CONFIG.colors).map(key => { return `.tag-${key} { background-color: ${CONFIG.colors[key]}; }`; }).join('\n'); GM_addStyle(` /* 通用标签样式 */ .tag-label { display: inline-block; padding: 1px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; line-height: 1.6; letter-spacing: 0.3px; white-space: nowrap; color: #fff; margin: 0 2px; box-shadow: 0 1px 3px rgba(0,0,0,0.12); } ${styleRules} `).id = 'tag-style-main'; GM_addStyle(` /* 让单元格内容更整齐 */ #table tbody td { vertical-align: middle; } /* 备注列允许换行,但标签不换行 */ #table tbody td:nth-child(11) .tag-label { white-space: nowrap; } /* 专业名称列(第3列)整体加粗 */ #table tbody td:nth-child(3) { font-weight: bold; text-align: left !important; } /* 该列中的标签独占一行,并保持间距 */ #table tbody td:nth-child(3) .tag-label { display: block; width: fit-content; max-width: 100%; margin: 2px 0; } /* 专业名称列中,特定标签保持行内 */ #table tbody td:nth-child(3) .tag-label.inline-tag { display: inline-block !important; width: auto !important; max-width: none !important; margin: 0 2px !important; } /* 计划类别列中,需要独占一行并居中的标签 */ #table tbody td:nth-child(5) .block-center { display: block; text-align: center; margin: 2px auto; width: fit-content; } `).id = 'tag-style-layout'; // 先尝试立即处理一次(如果表格已存在) const table = document.querySelector('#table'); if (table && table.querySelector('tbody tr')) { processTable(); } // 使用MutationObserver监听表格内容变化 const observer = new MutationObserver(function(mutations) { // 检查是否有新增的行或内容变化 let shouldProcess = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { shouldProcess = true; break; } if (mutation.type === 'characterData') { shouldProcess = true; break; } } if (shouldProcess) { // 延迟执行,等待DOM完全更新 clearTimeout(window._tagTimer); window._tagTimer = setTimeout(() => { processTable(); if (enableMajorMark) { updateAllRowStatuses(); } }, 150); } }); // 观察目标:整个#table及其子树 const targetNode = document.querySelector('#table') || document.querySelector('#tablediv'); if (targetNode) { observer.observe(targetNode, { childList: true, subtree: true, characterData: true, }); } else { // 如果#table还没出现,观察整个文档 observer.observe(document.body, { childList: true, subtree: true, }); } // 额外:当搜索框输入时,延迟处理(因为搜索会重新渲染表格) const searchInput = document.querySelector('.fixed-table-toolbar .search input'); if (searchInput) { searchInput.addEventListener('input', function() { clearTimeout(window._tagTimer); window._tagTimer = setTimeout(() => { processTable(); }, 300); }); } // 也监听页面上的"上一页"链接点击(页面切换) document.addEventListener('click', function(e) { const target = e.target.closest('a'); if (target && target.id === 'syhref') { clearTimeout(window._tagTimer); window._tagTimer = setTimeout(() => { // 页面切换后,等待新表格加载 const checkInterval = setInterval(() => { const tbody = document.querySelector('#table tbody'); if (tbody && tbody.querySelector('tr')) { clearInterval(checkInterval); processTable(); } }, 200); // 最多等待5秒 setTimeout(() => clearInterval(checkInterval), 5000); }, 300); } }); // 保存observer到全局,便于调试 window.__tagObserver = observer; // 额外:如果页面已经完全加载但表格还没出现,使用setInterval兜底检查 let fallbackCount = 0; const fallbackTimer = setInterval(() => { const table = document.querySelector('#table'); if (table && table.querySelector('tbody tr')) { processTable(); clearInterval(fallbackTimer); } fallbackCount++; if (fallbackCount > 30) { // 30次后停止(约15秒) clearInterval(fallbackTimer); } }, 500); console.log('[内蒙古高考招生计划添加彩色标签] 脚本已启动 ✅'); } } // ====================== 启动 ====================== // 注册GM菜单(放在最前面,确保菜单出现) registerMenu(); // 等待页面加载完成 if (document.readyState === 'complete' || document.readyState === 'interactive') { init(); } else { document.addEventListener('DOMContentLoaded', init); } })();