// ==UserScript== // @name showBossActiveTime // @namespace http://www.chensong.cc/ // @version 0.4.3 // @description to show hr lastest login time,help you deliver your resume efficiently. // @author chensong // @match https://www.zhipin.com/web/geek/job // @match https://www.zhipin.com/web/geek/job* // @match https://www.zhipin.com/web/geek/recommend // @match https://www.zhipin.com/web/geek/recommend* // @exclude https://www.zhipin.com/web/geek/map/jobs* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant GM_xmlhttpRequest // @license MIT //失业中,欢迎来扰:) 857763541@qq.com // @downloadURL https://update.greasyfork.icu/scripts/463885/showBossActiveTime.user.js // @updateURL https://update.greasyfork.icu/scripts/463885/showBossActiveTime.meta.js // ==/UserScript== (function () { 'use strict'; class ShowBossActiveTime { static MAXLIMIT = 5; static TIME = 2000; constructor(options) { this.queryQueue = []; //记录需要查询的队列 this.currentDomList = []; //当前列表的dom this.secondQueryList = []; //需要二次查询的队列 this.maxLimit = ShowBossActiveTime.MAXLIMIT; this.timer = null; this.frame = null; let bossActiveStatusList; try { bossActiveStatusList = JSON.parse( localStorage.getItem('bossActiveStatusList') ) || [ '半年前活跃', '近半年活跃', '4月前活跃', '2月内活跃', '2周内活跃' ]; } catch (error) { // 转换之前客户端存在的数据格式 bossActiveStatusList = localStorage .getItem('bossActiveStatusList') .split(','); } this.statusOptions = bossActiveStatusList.filter((option)=> option && option!== ''); this.removeStatusList = []; this.options = Object.assign( { listElement: '.job-card-wrapper', onlineElement: '.boss-online-tag', chatElement: '.start-chat-btn', hunterElement: '.job-tag-icon', linkElement: '.job-card-left', paginationElement: '.options-pages', hideChated: false }, options ) this.list = []; //数据列表 //添加过滤条件,因为要保存选择数据,所以这个不能切换时清空 this.addStatusFilter(); this.addStyleSheet(); this.ready(); // 监听请求数据事件 this.observeLoadingData(); this.init(); } // 获取节点列表 getList() { Array.from(document.querySelectorAll(this.options.listElement)).forEach( (node) => { const status = node.querySelector(this.options.onlineElement); // 保存所有list的dom this.list.push(node); let hunter = node.querySelector(this.options.hunterElement); hunter && (hunter = hunter.alt === '猎头') // 不在线且不是猎头 if (!status && !hunter) { // 需要查询的dom this.queryQueue.push(node); }else{ //如果是猎头 if (hunter) { const chat = node.querySelector(this.options.chatElement).textContent; this.setText(node, '猎头,没有活跃状态', chat); return; } // 先给在线的数据设置状态 const chat = node.querySelector(this.options.chatElement).textContent; const online = node.querySelector(this.options.onlineElement); if (online) { this.setText(node, '在线', chat); } } } ); } getListStatus() { // 每两秒下一个请求 // 保存请求的条数 let requestNum = this.queryQueue.length; let requestedNum = 0; this.timer = setInterval(() => { if (this.queryQueue.length === 0) { clearInterval(this.timer); this.timer = null; this.maxLimit = ShowBossActiveTime.MAXLIMIT; return; } if (this.maxLimit > 0) { this.maxLimit--; let node = this.queryQueue.shift(); let link = node.querySelector(this.options.linkElement).href; let chat = node.querySelector(this.options.chatElement).textContent; this.getStatusByXHR(link, node, chat).then(({ text, type }) => { // 设置文字 if(text===''){ text ='没有找到hr登录状态' } this.setText(node, text, chat); this.toggleDom(node); console.log(type, text, link); // 增加一个请求 this.maxLimit++; requestedNum++; if (requestedNum === requestNum) { this.alertBox('查询完毕'); } }); } }, ShowBossActiveTime.TIME); } async getStatusByXHR(url, node, chat) { return new Promise((resolve) => { // 第一次获取 GM_xmlhttpRequest({ method: 'GET', url, headers: { Cookie: document.cookie }, onload: (response) => { if (/security-check.html/.test(response.finalUrl)) { // 出现重定向后,使用返回的callbackUrl生成目标链接,二次获取 this.getHtmlByFormatUrl(response, 1).then((res) => { resolve(res); }); } else { const doc = this.parseHtml(response); const text = this.getStatusText(doc); resolve({ text, type: '第一次就获取到的数据' }); } } }); }); } getHtmlByFormatUrl(response, repeatCount) { return new Promise((resolve) => { // 二次获取,说明触发302,需要延时 setTimeout(() => { getFinalUrl(response.finalUrl, (url) => { GM_xmlhttpRequest({ method: 'GET', url, onload: (res) => { if (!/security-check.html/.test(res.finalUrl)) { const doc = this.parseHtml(res); const text = this.getStatusText(doc); resolve({ text, type: '二次请求的数据' }); } else { // 二次请求不成功,继续使用GM_xmlhttpRequest成功率低,转为其它形式,先fetch,fetch不行,用iframe return fetch(url, { redirect: 'error' }) .then((res) => { return res.text(); }) .then(async (data) => { const doc = document.createElement('div'); doc.insertAdjacentHTML('afterbegin', data); const text = this.getStatusText(doc); resolve({ text, type: 'fetch的数据' }); }) .catch(async (error) => { /*请求被302临时重定向了,无法获取到数据,需要用iframe来获取了*/ this.getStatusByIframe(url).then((text) => { resolve({ text, type: 'iframe的数据' }); }); }); } } }); }); }, 1000 * repeatCount); }); } parseHtml(res) { const html = res.responseText; const parser = new DOMParser(); return parser.parseFromString(html, 'text/html'); } ready() { var frame = document.createElement('iframe'); frame.style.height = 0; frame.style.width = 0; frame.style.margin = 0; frame.style.padding = 0; frame.style.border = '0 none'; frame.name = 'zhipinMyFrame'; frame.src = 'about:blank'; (document.body || document.documentElement).appendChild(frame); this.frame = frame; } checkIsSecurityCheckPage(tempIframe) { return tempIframe.contentWindow.securityPageName === 'securityCheck'; } async getStatusByIframe(url) { let iframe = document.createElement('iframe'); let match = url.match(/job_detail\/([^\/]+)\.html/); iframe.src = url; iframe.id = 'tempIframe' + match[1]; iframe.style.cssText = 'width:0;height:0;'; document.body.appendChild(iframe); return await new Promise((resolve) => { // 定时检测iframe中的某个节点是否已经渲染 let timer = setInterval(() => { // 获取iframe中的document对象 let iframeDoc = iframe.contentDocument || iframe.contentWindow.document; let hasFooter = iframeDoc.querySelector('#footer'); let status = this.getStatusText(iframeDoc); let verify = iframeDoc.querySelector('page-verify-slider') || document.querySelector('body').textContent.match(/您的账号可能存在异常访问行为,完成验证后即可正常使用/g) if(verify){ this.alertBox('更新过于频繁,触发认证操作了'); this.clear(); } if (hasFooter && iframeDoc.title !== '请稍候' && status) { resolve(status); iframe.remove(); clearInterval(timer); } }, 200); }); } observeLoadingData() { const container = document.querySelector('.search-job-result'); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { const addNode = mutation.addedNodes; const removedNode = mutation.removedNodes; if ( addNode.length && addNode[0].className === 'job-loading-wrapper' ) { console.log('触发了请求列表数据'); } if ( removedNode.length && removedNode[0].className === 'job-loading-wrapper' ) { console.log('页面加载完成,开始执行查询状态'); this.clear(); this.init(); } } }); }); const config = { attributes: false, childList: true, subtree: false }; observer.observe(container, config); // 监听是否是搜索列表页,不是就移除dom const listContainer = document.querySelector('#wrap'); const listObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { const wrapper = document.querySelector('.job-list-wrapper'); const removeNode = document.querySelector( '#removeFilterDataContainer' ); if (!wrapper && removeNode) { document.body.removeChild(removeNode); listObserver.disconnect(); // 清除查询 this.clear(); } } }); }); listObserver.observe(listContainer, config); } alertBox(msg) { let div = document.createElement('div'); div.id = 'alertBox'; div.innerHTML = msg; document.body.appendChild(div); setTimeout(function () { document.body.removeChild(div); }, 2000); } getStatusText(doc) { const timeNode = doc.querySelector('.boss-active-time'); if (timeNode) { return timeNode.textContent; } else { let status = null; // 没有获取到状态,但页面是已经加载到的了 if (doc.querySelector('.job-boss-info')) { status = '获取到数据了,但不知道是什么数据'; } const isHunter = ['.certification-tags', '.boss-info-attr'].filter( (name) => { const node = doc.querySelector(name); return /猎头|人力|人才|经纪/.test(node?.textContent); } ); isHunter && (status = '猎头,没有活跃状态'); return status; } } toggleDom(node) { const status = node.querySelector('.status')?.textContent; const chat = node.querySelector(this.options.chatElement).textContent; // 先显示全部 node.style.display = 'block'; // 首先判断是否隐藏已沟通 if (this.options.hideChated && chat === '继续沟通') { node.style.display = 'none'; } // 状态数据已经获取了 if (status && chat) { if (this.removeStatusList.includes(status)) { node.style.display = 'none'; } if (this.options.hideChated && chat === '继续沟通') { node.style.display = 'none'; } } } toggleDoms() { this.list.forEach((node) => { this.toggleDom(node); }); } addStatusFilter() { const container = document.createElement('div'); container.id = 'removeFilterDataContainer'; const html = `
`; const title = document.createElement('div'); title.className = 'title'; title.innerHTML = html; const tips = document.createElement('div'); tips.innerHTML = '过滤掉勾选的数据'; tips.className = 'tips'; container.appendChild(title); container.appendChild(tips); container .querySelector('#boss-active-time-arrow') .addEventListener('click', function () { container.classList.contains('hide') ? container.classList.remove('hide') : container.classList.add('hide'); }); this.statusOptions.forEach((option) => { const label = document.createElement('label'); const el = document.createElement('input'); el.type = 'checkbox'; el.name = option; el.value = option; el.className = 'status-checkbox'; label.appendChild(el); label.appendChild(document.createTextNode(option)); container.appendChild(label); }); container.addEventListener('change', () => { const selectedValues = Array.from( container.querySelectorAll('.status-checkbox:checked') ).map((el) => el.value); this.removeStatusList = selectedValues; const hideNode = document.querySelector('input[name="hideChated"]'); this.options.hideChated = hideNode?.checked; this.toggleDoms(); }); document.body.appendChild(container); } // 清空查询列表,清除缓存的dom clear() { this.queryQueue.length = 0; this.list.length = 0; this.currentDomList.length = 0; this.maxLimit = ShowBossActiveTime.MAXLIMIT; clearInterval(this.timer); this.timer = null; } ready() { let frame = document.createElement('iframe'); frame.style.height = 0; frame.style.width = 0; frame.style.margin = 0; frame.style.padding = 0; frame.style.border = '0 none'; frame.name = 'zhipinMyFrame'; frame.src = 'about:blank'; document.body.appendChild(frame); } addStyleSheet() { const style = ` .show-active-status{display:flex;padding:5px 10px;background:#e1f5e3;color:green;width:80%;border-radius:4px;margin-top:10px;margin-bottom:5px;} .show-active-status .status{} .show-active-status .chat{} #alertBox{position: fixed; top: 20%; left: 50%; transform: translate(-50%, -50%); background-color: rgb(0 190 189); border-radius: 5px; color: #fff; z-index: 9999; padding: 20px 40px; font-size: 20px; box-shadow: 0px 0px 10px rgba(0,0,0,.2);} #removeFilterDataContainer{ position: fixed;right: 70px;top: 70px;z-index: 20000;background: #00bebd; color: #fff;display: flex;flex-direction: column;padding-bottom:10px } #removeFilterDataContainer.hide{height:28px;overflow:hidden} #removeFilterDataContainer .title {display:flex;justify-content: space-around;} #removeFilterDataContainer .title label{align-items:center;padding:0 15px;} #removeFilterDataContainer.hide #boss-active-time-arrow svg{transform: rotate(180deg);} #removeFilterDataContainer #boss-active-time-arrow {cursor: pointer;font-size: 24px;background: #009796;padding:2px 10px;line-height:1;} #removeFilterDataContainer .tips{font-size:16px;margin:5px 20px;} #removeFilterDataContainer label{display:flex;padding:0 20px;} #removeFilterDataContainer label input{margin-right:5px;} `; const styleEle = document.createElement('style'); styleEle.id = 'show-boss-active-time-css'; styleEle.innerHTML = style; document.head?.appendChild(styleEle); } // 设置文本内容 setText(node, text, status) { const html = `${text}
${status}