// ==UserScript== // @name 软考报名助手(全国计算机软件资格考试) // @namespace https://bm.ruankao.org.cn/ // @version 1.0.0 // @description 软考报名平台增强:自动标记各省报名状态(进行中/未开始/已结束)、我的省份置顶+实时倒计时、状态筛选、一键展开全部省份列表 // @author 汉口铭哥 // @match https://bm.ruankao.org.cn/sign/welcome* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; /* ================= 配置 ================= */ var STATUS = { OPEN: { key: 'open', text: '报名中', color: '#fff', bg: '#67c23a', rowBg: '#f0f9eb', border: '#b3e19d' }, FUTURE:{ key: 'future', text: '未开始', color: '#909399', bg: '#f4f4f5', rowBg: '#fafafa', border: '#dcdfe6' }, CLOSED:{ key: 'closed', text: '已结束', color: '#f56c6c', bg: '#fef0f0', rowBg: '#fdf6f6', border: '#fbc4c4' } }; /* ================= 工具 ================= */ function parseTime(str) { // "2026-08-14 09:30" -> Date (本地时间) var m = str.trim().match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})$/); if (!m) return null; return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], 0); } function fmtDiff(ms) { if (ms <= 0) return '已截止'; var s = Math.floor(ms / 1000); var d = Math.floor(s / 86400); s -= d * 86400; var h = Math.floor(s / 3600); s -= h * 3600; var mi = Math.floor(s / 60); s -= mi * 60; var out = ''; if (d > 0) out += d + '天'; if (d > 0 || h > 0) out += h + '小时'; if (d === 0) out += mi + '分' + s + '秒'; return out; } function addStyle(css) { var s = document.createElement('style'); s.textContent = css; document.head.appendChild(s); } /* ================= 主逻辑 ================= */ var myProvince = GM_getValue('rk_my_province', ''); var rows = []; // {el, name, signStart, signEnd, payEnd, status} var timerId = null; function collectRows() { rows = []; var els = document.querySelectorAll('.listCont.selectorg'); for (var i = 0; i < els.length; i++) { var el = els[i]; var name = (el.querySelector('.col1') || {}).textContent || ''; name = name.trim(); var tws = el.querySelectorAll('.timeW'); var signRange = tws.length > 0 ? tws[0].textContent : ''; var payRange = tws.length > 1 ? tws[1].textContent : ''; var parts = signRange.split('~'); var signStart = parseTime(parts[0] || ''); var signEnd = parseTime(parts[1] || ''); var payParts = payRange.split('~'); var payEnd = parseTime(payParts[1] || ''); var now = new Date(); var status; if (signStart && now < signStart) status = STATUS.FUTURE; else if (signEnd && now > signEnd) status = STATUS.CLOSED; else status = STATUS.OPEN; rows.push({ el: el, name: name, signStart: signStart, signEnd: signEnd, payEnd: payEnd, status: status, signRange: signRange.trim(), payRange: payRange.trim() }); } } function decorateRows() { // 行内样式加星标 + 状态徽章 rows.forEach(function (r) { var el = r.el; el.setAttribute('data-rk-status', r.status.key); el.style.background = r.status.rowBg; el.style.borderLeft = '4px solid ' + r.status.border; el.style.position = 'relative'; // 状态徽章(插在省份名后面) if (!el.querySelector('.rk-badge')) { var badge = document.createElement('span'); badge.className = 'rk-badge'; badge.textContent = r.status.text; badge.style.cssText = 'margin-left:8px;padding:1px 8px;font-size:12px;border-radius:10px;' + 'color:' + r.status.color + ';background:' + r.status.bg + ';border:1px solid ' + r.status.border + ';'; var col1 = el.querySelector('.col1'); if (col1) col1.appendChild(badge); } // 收藏星标 if (!el.querySelector('.rk-star')) { var star = document.createElement('span'); star.className = 'rk-star'; star.textContent = (r.name === myProvince) ? '★' : '☆'; star.title = '设为我的省份(置顶+倒计时)'; star.style.cssText = 'position:absolute;right:10px;top:50%;transform:translateY(-50%);' + 'cursor:pointer;font-size:20px;color:#e6a23c;z-index:5;user-select:none;'; star.addEventListener('click', function (e) { e.stopPropagation(); // 不触发原站的选省跳转 if (myProvince === r.name) { GM_deleteValue('rk_my_province'); myProvince = ''; } else { GM_setValue('rk_my_province', r.name); myProvince = r.name; } location.reload(); }); el.appendChild(star); } }); } function pinMyProvince() { if (!myProvince) return; var target = null; rows.forEach(function (r) { if (r.name === myProvince) target = r; }); if (!target) return; target.el.style.background = '#fdf6ec'; target.el.style.borderLeft = '4px solid #e6a23c'; target.el.classList.add('rk-mine'); // 置顶:移到列表最前(jQuery 直接绑定在元素上,移动 DOM 不丢事件) var container = target.el.parentNode; var first = container.querySelector('.listCont.selectorg'); if (first && first !== target.el) { container.insertBefore(target.el, first); } // 平滑滚到可见处 setTimeout(function () { try { target.el.scrollIntoView({ block: 'center', behavior: 'smooth' }); } catch (e) {} }, 300); } /* ================= 筛选条 ================= */ var currentFilter = 'all'; function buildFilterBar() { if (document.querySelector('.rk-filterbar')) return; var header = document.querySelector('.listHeader'); if (!header) return; var bar = document.createElement('div'); bar.className = 'rk-filterbar'; bar.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px 15px;' + 'background:#fff;border-bottom:1px solid #eee;flex-wrap:wrap;'; var mkBtn = function (key, label) { var b = document.createElement('button'); b.textContent = label; b.dataset.filter = key; b.style.cssText = 'padding:4px 14px;font-size:13px;border:1px solid #dcdfe6;border-radius:14px;' + 'background:#fff;color:#606266;cursor:pointer;'; b.addEventListener('click', function () { currentFilter = key; applyFilter(); bar.querySelectorAll('button').forEach(function (x) { var on = x.dataset.filter === key; x.style.background = on ? '#409eff' : '#fff'; x.style.color = on ? '#fff' : '#606266'; x.style.borderColor = on ? '#409eff' : '#dcdfe6'; }); }); return b; }; var counts = { all: rows.length, open: 0, future: 0, closed: 0 }; rows.forEach(function (r) { counts[r.status.key]++; }); bar.appendChild(mkBtn('all', '全部 (' + counts.all + ')')); bar.appendChild(mkBtn('open', '报名中 (' + counts.open + ')')); bar.appendChild(mkBtn('future', '未开始 (' + counts.future + ')')); bar.appendChild(mkBtn('closed', '已结束 (' + counts.closed + ')')); // 展开全部按钮 var expand = document.createElement('button'); expand.textContent = '⤢ 展开全部省份'; expand.style.cssText = 'padding:4px 14px;font-size:13px;border:1px solid #67c23a;border-radius:14px;' + 'background:#f0f9eb;color:#67c23a;cursor:pointer;margin-left:auto;'; var expanded = false; expand.addEventListener('click', function () { expanded = !expanded; var cs = document.querySelector('.contentShow'); if (!cs) return; if (expanded) { cs.style.height = 'auto'; cs.style.maxHeight = 'none'; cs.style.overflow = 'visible'; expand.textContent = '⤡ 收起列表'; var down = document.querySelector('.lists_ > .down'); if (down) down.style.display = 'none'; } else { cs.style.height = ''; cs.style.maxHeight = ''; cs.style.overflow = ''; expand.textContent = '⤢ 展开全部省份'; location.reload(); } }); bar.appendChild(expand); header.parentNode.insertBefore(bar, header.nextSibling); // 默认选中"全部" var firstBtn = bar.querySelector('button'); if (firstBtn) firstBtn.click(); } function applyFilter() { rows.forEach(function (r) { r.el.style.display = (currentFilter === 'all' || r.status.key === currentFilter) ? '' : 'none'; }); var empty = document.querySelector('.rk-empty'); var visible = rows.some(function (r) { return r.el.style.display !== 'none'; }); if (!visible && !empty) { empty = document.createElement('div'); empty.className = 'rk-empty'; empty.textContent = '当前筛选下没有省份'; empty.style.cssText = 'padding:30px;text-align:center;color:#909399;'; var box = document.querySelector('.contentShowIn'); if (box) box.appendChild(empty); } else if (visible && empty) { empty.remove(); } } /* ================= 倒计时浮窗 ================= */ function buildCountdownPanel() { if (!myProvince) return; var target = null; rows.forEach(function (r) { if (r.name === myProvince) target = r; }); if (!target) return; var panel = document.createElement('div'); panel.className = 'rk-panel'; panel.innerHTML = '