求职助手

为相关的求职平台(BOSS直聘、拉勾、智联招聘、猎聘)添加一些实用的小功能,如自定义薪资范围、过滤黑名单公司等。

  1. // ==UserScript==
  2. // @name 求职助手
  3. // @namespace job_seeking_helper
  4. // @author Gloduck
  5. // @license MIT
  6. // @version 1.1.3
  7. // @description 为相关的求职平台(BOSS直聘、拉勾、智联招聘、猎聘)添加一些实用的小功能,如自定义薪资范围、过滤黑名单公司等。
  8. // @match https://www.zhipin.com/*
  9. // @match https://m.zhipin.com/*
  10. // @match https://c.liepin.com/*
  11. // @match https://mc.liepin.com/*
  12. // @match https://m.liepin.com/*
  13. // @match https://www.liepin.com/*
  14. // @grant GM_registerMenuCommand
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // ==/UserScript==
  18.  
  19. (function () {
  20. 'use strict';
  21.  
  22. // 策略枚举
  23. const JobPlatform = {
  24. BOSS_ZHIPIN: Symbol('Boss'), LAGOU: Symbol('Lagou'), LIEPIN: Symbol('Liepin'), UNKNOWN: Symbol('未知网站')
  25. };
  26.  
  27. const JobPageType = {
  28. SEARCH: Symbol('Search'),
  29. RECOMMEND: Symbol('Recommend'),
  30. MOBILE_SEARCH: Symbol('MobileSearch'),
  31. MOBILE_RECOMMEND: Symbol('MobileRecommend')
  32. }
  33.  
  34. const JobFilterType = {
  35. BLACKLIST: Symbol('Blacklist'), VIEWED: Symbol('Viewed'), MISMATCH_CONDITION: Symbol('MismatchCondition'),
  36. }
  37.  
  38. let curPageHash = null;
  39. let lock = false;
  40. // 默认设置
  41. const defaults = {
  42. blacklist: [],
  43. minSalary: 0,
  44. maxSalary: Infinity,
  45. salaryFilterType: 'include',
  46. filterInactiveJob: false,
  47. maxDailyHours: 8,
  48. maxMonthlyDays: 22,
  49. maxWeeklyCount: 4,
  50. viewedAction: 'mark',
  51. blacklistAction: 'hide',
  52. conditionAction: 'hide'
  53. };
  54.  
  55. // 加载用户设置
  56. const settings = {
  57. blacklist: GM_getValue('blacklist', defaults.blacklist),
  58. minSalary: GM_getValue('minSalary', defaults.minSalary),
  59. maxSalary: GM_getValue('maxSalary', defaults.maxSalary),
  60. salaryFilterType: GM_getValue('salaryFilterType', defaults.salaryFilterType),
  61. filterInactiveJob: GM_getValue('filterInactiveJob', defaults.filterInactiveJob),
  62. maxDailyHours: GM_getValue('maxDailyHours', defaults.maxDailyHours),
  63. maxMonthlyDays: GM_getValue('maxMonthlyDays', defaults.maxMonthlyDays),
  64. maxWeeklyCount: GM_getValue('maxWeeklyCount', defaults.maxWeeklyCount),
  65. viewedAction: GM_getValue('viewedAction', defaults.viewedAction),
  66. blacklistAction: GM_getValue('blacklistAction', defaults.blacklistAction),
  67. conditionAction: GM_getValue('conditionAction', defaults.conditionAction)
  68. };
  69.  
  70. // 注册(不可用)设置菜单
  71. GM_registerMenuCommand('职位过滤设置', showSettings);
  72.  
  73. function showSettings() {
  74. const dialog = document.createElement('div');
  75. dialog.style = `
  76. position: fixed;
  77. top: 50%;
  78. left: 50%;
  79. transform: translate(-50%, -50%);
  80. background: #ffffff;
  81. padding: 25px;
  82. border-radius: 12px;
  83. box-shadow: 0 8px 24px rgba(0,0,0,0.15);
  84. z-index: 9999;
  85. min-width: 380px;
  86. font-family: 'Segoe UI', sans-serif;
  87. `;
  88.  
  89. dialog.innerHTML = `
  90. <h2 style="color: #2c3e50; margin: 0 0 20px; font-size: 1.4em;">职位过滤设置</h2>
  91. <!-- 基础设置 -->
  92. <div style="margin-bottom: 20px;">
  93. <label style="display: block; margin-bottom: 8px; font-weight: 500;">黑名单关键词(逗号分隔)</label>
  94. <input type="text" id="blacklist"
  95. style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
  96. value="${settings.blacklist.join(',')}">
  97. </div>
  98.  
  99. <!-- 薪资设置 -->
  100. <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
  101. <h3 style="color: #2c3e50; margin: 0 0 15px; font-size: 1.1em;">薪资设置</h3>
  102. <div style="margin-bottom: 12px;">
  103. <label style="display: block; margin-bottom: 6px;">薪资范围(元/月):</label>
  104. <div style="display: flex; gap: 10px;">
  105. <input type="number" id="minSalary" placeholder="最低"
  106. style="flex: 1; padding: 8px;" value="${settings.minSalary}">
  107. <span style="align-self: center;">-</span>
  108. <input type="number" id="maxSalary" placeholder="最高"
  109. style="flex: 1; padding: 8px;" value="${settings.maxSalary}">
  110. </div>
  111. </div>
  112. <div style="margin-bottom: 12px;">
  113. <label style="display: block; margin-bottom: 6px;">薪资过滤方式:</label>
  114. ${createRadioGroup('salaryFilterType', ['include', 'overlap'], settings.salaryFilterType, {
  115. include: '包含范围',
  116. overlap: '存在交集'
  117. })}
  118. </div>
  119.  
  120. </div>
  121. <!-- 其他设置 -->
  122. <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
  123. <h3 style="color: #2c3e50; margin: 0 0 15px; font-size: 1.1em;">其他设置</h3>
  124. <div style="margin-bottom: 12px;">
  125.  
  126. <label style="display: flex; align-items: center; gap: 8px; color: #34495e;">
  127. <input type="checkbox" id="filterInactiveJob" ${settings.filterInactiveJob ? 'checked' : ''}>
  128. 过滤不活跃的职位
  129. </label>
  130. </div>
  131.  
  132. </div>
  133. <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
  134. <h3 style="color: #2c3e50; margin: 0 0 15px; font-size: 1.1em;">工作时间限制</h3>
  135. <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px;">
  136. <div>
  137. <label style="display: block; margin-bottom: 6px;">日工作小时</label>
  138. <input type="number" id="maxDailyHours"
  139. min="1" max="24" step="0.5"
  140. style="width: 100%; padding: 8px;"
  141. value="${settings.maxDailyHours}">
  142. </div>
  143. <div>
  144. <label style="display: block; margin-bottom: 6px;">月工作天数</label>
  145. <input type="number" id="maxMonthlyDays"
  146. min="1" max="31" step="1"
  147. style="width: 100%; padding: 8px;"
  148. value="${settings.maxMonthlyDays}">
  149. </div>
  150. <div>
  151. <label style="display: block; margin-bottom: 6px;">月工作周数</label>
  152. <input type="number" id="maxWeeklyCount"
  153. min="1" max="6" step="0.5"
  154. style="width: 100%; padding: 8px;"
  155. value="${settings.maxWeeklyCount}">
  156. </div>
  157. </div>
  158. </div>
  159.  
  160. <!-- 整合后的处理方式设置 -->
  161. <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
  162. <h3 style="color: #2c3e50; margin: 0 0 15px; font-size: 1.1em;">处理方式设置</h3>
  163. <div style="margin-bottom: 12px;">
  164. <label style="display: block; margin-bottom: 6px;">黑名单职位:</label>
  165. ${createRadioGroup('blacklistAction', ['delete', 'hide', 'mark', 'noop'], settings.blacklistAction, {
  166. delete: '删除',
  167. hide: '屏蔽',
  168. mark: '标识',
  169. noop: '无操作'
  170. })}
  171. </div>
  172.  
  173. <div style="margin-bottom: 12px;">
  174. <label style="display: block; margin-bottom: 6px;">已查看职位:</label>
  175. ${createRadioGroup('viewedAction', ['delete', 'hide', 'mark', 'noop'], settings.viewedAction, {
  176. delete: '删除',
  177. hide: '屏蔽',
  178. mark: '标识',
  179. noop: '无操作'
  180. })}
  181. </div>
  182.  
  183. <div>
  184. <label style="display: block; margin-bottom: 6px;">不符合条件职位:</label>
  185. ${createRadioGroup('conditionAction', ['delete', 'hide', 'mark', 'noop'], settings.conditionAction, {
  186. delete: '删除',
  187. hide: '屏蔽',
  188. mark: '标识',
  189. noop: '无操作'
  190. })}
  191. </div>
  192. </div>
  193. <div style="margin-top: 25px; padding-top: 20px; border-top: 1px solid #eee;">
  194. <button id="clearCacheBtn"
  195. style="
  196. background: #95a5a6;
  197. color: white;
  198. border: none;
  199. padding: 8px 16px;
  200. border-radius: 4px;
  201. cursor: pointer;
  202. transition: opacity 0.2s;
  203. ">
  204. 清理已查看职位
  205. </button>
  206. <span style="color: #7f8c8d; font-size: 0.9em; margin-left: 10px;">
  207. 将重置当前平台所有已经查看的职位
  208. </span>
  209. </div>
  210.  
  211. <!-- 操作按钮 -->
  212. <div style="display: flex; gap: 10px; justify-content: flex-end; border-top: 1px solid #eee; padding-top: 20px;">
  213. <button id="saveBtn" class="dialog-btn primary">保存</button>
  214. <button id="cancelBtn" class="dialog-btn secondary">取消</button>
  215. </div>
  216. `;
  217.  
  218. // 添加样式
  219. const style = document.createElement('style');
  220. style.textContent = `
  221. .dialog-btn {
  222. padding: 8px 20px;
  223. border: none;
  224. border-radius: 6px;
  225. cursor: pointer;
  226. transition: all 0.2s;
  227. font-size: 14px;
  228. }
  229. .primary {
  230. background: #3498db;
  231. color: white;
  232. }
  233. .secondary {
  234. background: #f0f0f0;
  235. color: #666;
  236. }
  237. .radio-group {
  238. display: flex;
  239. gap: 15px;
  240. margin-top: 5px;
  241. }
  242. .radio-item label {
  243. display: flex;
  244. align-items: center;
  245. gap: 6px;
  246. cursor: pointer;
  247. }
  248. `;
  249. dialog.appendChild(style);
  250. document.body.appendChild(dialog);
  251.  
  252. // 事件绑定
  253. dialog.querySelector('#saveBtn').addEventListener('click', saveSettings);
  254. dialog.querySelector('#cancelBtn').addEventListener('click', () => dialog.remove());
  255. dialog.querySelector('#clearCacheBtn').addEventListener('click', () => {
  256. if (confirm('确定要清理已查看职位吗?\n这将重置当前平台所有已经查看的职位!')) {
  257. clearViewedJob(choosePlatForm());
  258. alert('清理完成!');
  259. location.reload();
  260. }
  261. });
  262. }
  263.  
  264. // 生成带自定义标签的单选组
  265. function createRadioGroup(name, values, selected, labels) {
  266. return `
  267. <div class="radio-group">
  268. ${values.map(value => `
  269. <div class="radio-item">
  270. <label>
  271. <input type="radio" name="${name}" value="${value}" ${value === selected ? 'checked' : ''}>
  272. ${labels[value] || value}
  273. </label>
  274. </div>
  275. `).join('')}
  276. </div>
  277. `;
  278. }
  279.  
  280.  
  281. function saveSettings() {
  282. settings.blacklist = document.querySelector('#blacklist').value
  283. .split(',')
  284. .map(s => s.trim().toLowerCase())
  285. .filter(Boolean);
  286.  
  287. settings.minSalary = parseInt(document.querySelector('#minSalary').value) || 0;
  288. settings.maxSalary = parseInt(document.querySelector('#maxSalary').value) || Infinity;
  289. settings.salaryFilterType = document.querySelector('input[name="salaryFilterType"]:checked').value;
  290. settings.filterInactiveJob = document.querySelector('#filterInactiveJob:checked').value || false;
  291. settings.viewedAction = document.querySelector('input[name="viewedAction"]:checked').value;
  292. settings.blacklistAction = document.querySelector('input[name="blacklistAction"]:checked').value;
  293. settings.conditionAction = document.querySelector('input[name="conditionAction"]:checked').value;
  294. settings.maxDailyHours = parseFloat(document.querySelector('#maxDailyHours').value) || 0;
  295. settings.maxMonthlyDays = parseInt(document.querySelector('#maxMonthlyDays').value) || 0;
  296. settings.maxWeeklyCount = parseFloat(document.querySelector('#maxWeeklyCount').value) || 0;
  297.  
  298. // 保存设置
  299. GM_setValue('blacklist', settings.blacklist);
  300. GM_setValue('minSalary', settings.minSalary);
  301. GM_setValue('maxSalary', settings.maxSalary);
  302. GM_setValue('salaryFilterType', settings.salaryFilterType);
  303. GM_setValue('filterInactiveJob', settings.filterInactiveJob);
  304. GM_setValue('viewedAction', settings.viewedAction);
  305. GM_setValue('blacklistAction', settings.blacklistAction);
  306. GM_setValue('conditionAction', settings.conditionAction);
  307. GM_setValue('maxDailyHours', settings.maxDailyHours);
  308. GM_setValue('maxMonthlyDays', settings.maxMonthlyDays);
  309. GM_setValue('maxWeeklyCount', settings.maxWeeklyCount);
  310.  
  311. document.querySelector('div').remove();
  312. alert('设置已保存!');
  313. location.reload();
  314. }
  315.  
  316. /**
  317. *
  318. * @return {symbol}
  319. */
  320. function choosePlatForm() {
  321. const href = window.location.href;
  322. if (href.includes('zhipin.com')) {
  323. return JobPlatform.BOSS_ZHIPIN;
  324. } else if (href.includes('liepin.com')) {
  325. return JobPlatform.LIEPIN;
  326. } else {
  327. return JobPlatform.UNKNOWN;
  328. }
  329. }
  330.  
  331. /**
  332. *
  333. * @returns {PlatFormStrategy}
  334. */
  335. function getStrategy() {
  336. switch (choosePlatForm()) {
  337. case JobPlatform.BOSS_ZHIPIN:
  338. return new BossStrategy();
  339. case JobPlatform.LIEPIN:
  340. return new LiepinStrategy();
  341. default:
  342. throw new Error('Unsupported platform')
  343. }
  344. }
  345.  
  346.  
  347. /**
  348. *
  349. * @param {String} salaryRange
  350. */
  351. function parseSalaryToMonthly(salaryRange) {
  352. // 更新正则表达式以匹配单个数值或范围
  353. const regex = /(\d+(?:\.\d+)?)(?:\s*-\s*(\d+(?:\.\d+)?))?/;
  354. const match = salaryRange.match(regex);
  355.  
  356. if (!match) {
  357. if (salaryRange.includes('面议')) {
  358. return { min: 0, max: Infinity };
  359. }
  360. throw new Error(`Invalid salary range format( ${salaryRange} )`);
  361. }
  362.  
  363. // 提取并处理最小值和最大值
  364. let minSalary = parseFloat(match[1]);
  365. let maxSalary = match[2] ? parseFloat(match[2]) : minSalary; // 处理单个数值情况
  366.  
  367. // 处理单位转换
  368. if (/k/i.test(salaryRange)) { // 检查是否存在k/K
  369. minSalary *= 1000;
  370. maxSalary *= 1000;
  371. }
  372.  
  373. // 根据时间单位转换月薪
  374. if (salaryRange.includes('周')) {
  375. minSalary *= settings.maxWeeklyCount;
  376. maxSalary *= settings.maxWeeklyCount;
  377. } else if (salaryRange.includes('天')) {
  378. minSalary *= settings.maxMonthlyDays;
  379. maxSalary *= settings.maxMonthlyDays;
  380. } else if (salaryRange.includes('时')) {
  381. minSalary *= settings.maxMonthlyDays * settings.maxDailyHours;
  382. maxSalary *= settings.maxMonthlyDays * settings.maxDailyHours;
  383. }
  384.  
  385. return { min: minSalary, max: maxSalary };
  386. }
  387.  
  388.  
  389. /**
  390. *
  391. * @param {Symbol} jobPlatform
  392. */
  393. function clearViewedJob(jobPlatform) {
  394. let jobViewKey = getJobViewKey(jobPlatform);
  395. // GM_setValue(jobViewKey, []);
  396. CacheManager.delete(jobViewKey);
  397. }
  398.  
  399. /**
  400. *
  401. * @param {Symbol} jobPlatform
  402. * @param {String} uniqueKey
  403. */
  404. function setJobViewed(jobPlatform, uniqueKey) {
  405. if(uniqueKey == null){
  406. return;
  407. }
  408. let jobViewKey = getJobViewKey(jobPlatform);
  409. const jobViewedSet = getJobViewedSet(jobPlatform);
  410. if (jobViewedSet.has(uniqueKey)) {
  411. return;
  412. }
  413. jobViewedSet.add(uniqueKey);
  414. // GM_setValue(jobViewKey, [...jobViewedSet]);
  415. CacheManager.set(jobViewKey, [...jobViewedSet]);
  416. }
  417.  
  418. /**
  419. *
  420. * @param {Symbol} jobPlatform
  421. * @return {Set<String>}
  422. */
  423. function getJobViewedSet(jobPlatform) {
  424. let jobViewKey = getJobViewKey(jobPlatform);
  425. // return new Set(GM_getValue(jobViewKey, []));
  426. const cache = CacheManager.get(jobViewKey);
  427. return new Set(cache != null ? cache : []);
  428. }
  429.  
  430. /**
  431. *
  432. * @param {Symbol} jobPlatform
  433. * @return {string}
  434. */
  435. function getJobViewKey(jobPlatform) {
  436. return jobPlatform.description + "ViewHistory";
  437. }
  438.  
  439.  
  440. class PlatFormStrategy {
  441. /**
  442. * @returns {JobPageType}
  443. */
  444. fetchJobPageType() {
  445. throw new Error('Method not implemented');
  446. }
  447.  
  448. /**
  449. * @param {JobPageType} jobPageType
  450. * @returns {NodeListOf<Element>}
  451. */
  452. fetchJobElements(jobPageType) {
  453. throw new Error('Method not implemented');
  454.  
  455. }
  456.  
  457. /**
  458. * @param {Element} jobElement
  459. * @param {JobPageType} jobPageType
  460. * @returns {String|null}
  461. */
  462. fetchJobUniqueKey(jobElement, jobPageType) {
  463. throw new Error('Method not implemented');
  464. }
  465.  
  466. /**
  467. * @param {Element} jobElement - 职位信息 DOM 元素
  468. * @param {JobPageType} jobPageType - 职位页面类型
  469. * @returns {Promise<{min: number, max: number}>}
  470. */
  471. async parseSalary(jobElement, jobPageType) {
  472. throw new Error('Method not implemented'); // 自动转换为 rejected Promise
  473. }
  474.  
  475. /**
  476. *
  477. * @param {Element} jobElement
  478. * @param {JobPageType} jobPageType
  479. * @returns {Promise<String>}
  480. */
  481. async parseJobName(jobElement, jobPageType) {
  482. throw new Error('Method not implemented');
  483. }
  484.  
  485. /**
  486. *
  487. * @param {Element} jobElement
  488. * @param {JobPageType} jobPageType
  489. * @returns {Promise<String>}
  490. */
  491. async parseCompanyName(jobElement, jobPageType) {
  492. throw new Error('Method not implemented');
  493. }
  494.  
  495. /**
  496. *
  497. * @param {Element} jobElement
  498. * @param {JobPageType} jobPageType
  499. * @returns {Promise<Boolean>}
  500. */
  501. async jobIsActive(jobElement, jobPageType) {
  502. throw new Error('Method not implemented');
  503. }
  504.  
  505. /**
  506. *
  507. * @param {Element} jobElement
  508. * @param {JobPageType} jobPageType
  509. * @param {JobFilterType[]} jobFilterTypes
  510. * @returns {void}
  511. */
  512. markCurJobElement(jobElement, jobPageType, jobFilterTypes) {
  513. throw new Error('Method not implemented');
  514. }
  515.  
  516. /**
  517. *
  518. * @param {Element} jobElement
  519. * @param {JobPageType} jobPageType
  520. * @param {JobFilterType[]} jobFilterTypes
  521. * @returns {void}
  522. */
  523. blockCurJobElement(jobElement, jobPageType, jobFilterTypes) {
  524. throw new Error('Method not implemented');
  525. }
  526.  
  527. /**
  528. *
  529. * @param {Element} jobElement
  530. * @param {JobPageType} jobPageType
  531. * @returns {void}
  532. */
  533. removeCurJobElement(jobElement, jobPageType) {
  534. throw new Error('Method not implemented');
  535. }
  536.  
  537. /**
  538. *
  539. * @param {Element} jobElement
  540. * @param {JobPageType} jobPageType
  541. * @param {Function} eventCallback
  542. * @returns {void}
  543. */
  544. addViewedCallback(jobElement, jobPageType, eventCallback) {
  545. throw new Error('Method not implemented');
  546. }
  547.  
  548. /**
  549. *
  550. * @param {Element} element
  551. */
  552. addDeleteLine(element) {
  553. const delElement = document.createElement('del');
  554.  
  555. while (element.firstChild) {
  556. delElement.appendChild(element.firstChild);
  557. }
  558.  
  559. element.appendChild(delElement);
  560. }
  561.  
  562. /**
  563. * @param {JobFilterType} jobFilterType
  564. * @returns {String}
  565. */
  566. convertFilterTypeToMessage(jobFilterType) {
  567. if (jobFilterType === JobFilterType.BLACKLIST) {
  568. return '黑名单';
  569. } else if (jobFilterType === JobFilterType.VIEWED) {
  570. return '已查看';
  571. } else if (jobFilterType === JobFilterType.MISMATCH_CONDITION) {
  572. return '条件不符';
  573. } else {
  574. return '未知';
  575. }
  576. }
  577.  
  578. }
  579.  
  580. class LiepinStrategy extends PlatFormStrategy {
  581. fetchJobPageType() {
  582. if (document.querySelector("#home-main-box-container") != null) {
  583. return JobPageType.RECOMMEND;
  584. } else if (document.querySelector('#lp-search-job-box') != null) {
  585. return JobPageType.SEARCH;
  586. } else if (document.querySelector('.main-content .job-list-box') != null || document.querySelector('.home-container .recommend-job-list') != null) {
  587. return JobPageType.MOBILE_RECOMMEND;
  588. } else if (document.querySelector('.so-job-job-list') != null) {
  589. return JobPageType.MOBILE_SEARCH;
  590. }
  591. }
  592.  
  593. fetchJobElements(jobPageType) {
  594. if (jobPageType === JobPageType.SEARCH) {
  595. return document.querySelectorAll('div.job-list-box > div');
  596. } else if (jobPageType === JobPageType.RECOMMEND) {
  597. return document.querySelectorAll('ul.pull-up-content > li.pull-up-li');
  598. } else if (jobPageType === JobPageType.MOBILE_SEARCH) {
  599. return document.querySelectorAll('.so-job-job-list > div');
  600. } else if (jobPageType === JobPageType.MOBILE_RECOMMEND) {
  601. if (document.querySelector('.recommend-job-list') != null) {
  602. // 未登录(不可用)时的推荐页
  603. return document.querySelectorAll('.recommend-job-list > a');
  604. } else {
  605. // 登录(不可用)后的推荐页
  606. return document.querySelectorAll('ul.pull-up-content > li.pull-up-li');
  607. }
  608. } else {
  609. throw new Error('Not a job element')
  610. }
  611. }
  612.  
  613. fetchJobUniqueKey(jobElement, jobPageType) {
  614. if (jobElement.classList.contains('filter-blocked')) {
  615. return null;
  616. }
  617. if (jobPageType === JobPageType.SEARCH || jobPageType === JobPageType.RECOMMEND) {
  618. const element = jobElement.querySelector('.job-detail-box > a');
  619. if (element == null) {
  620. return null;
  621. }
  622. const url = element.href;
  623. if (url === null) {
  624. return null;
  625. }
  626. return this.fetchUniqueKeyFromUrl(url);
  627. } else if (jobPageType === JobPageType.MOBILE_RECOMMEND || jobPageType === JobPageType.MOBILE_SEARCH) {
  628. const element = this.fetchMobileJobElementContainer(jobElement)
  629. if (element == null) {
  630. return null;
  631. }
  632. if (element.hasAttribute('href')) {
  633. return this.fetchUniqueKeyFromUrl(element.href);
  634. }
  635. const dataTags = element.getAttribute('data-tlg-ext');
  636. if (dataTags == null) {
  637. return null;
  638. }
  639. const dataTagsJson = JSON.parse(decodeURIComponent(dataTags))
  640. if (dataTagsJson.jobId != null) {
  641. return dataTagsJson.jobId.toString();
  642. } else if (dataTagsJson.job_id != null) {
  643. return dataTagsJson.job_id.toString();
  644. } else {
  645. throw new Error('Not a job element')
  646. }
  647. } else {
  648. throw new Error('Not a job element')
  649. }
  650. }
  651.  
  652.  
  653. async parseSalary(jobElement, jobPageType) {
  654. if (jobPageType === JobPageType.SEARCH || jobPageType === JobPageType.RECOMMEND) {
  655. const salary = jobElement.querySelector('.job-detail-box > a > div > span:last-child').textContent;
  656. return parseSalaryToMonthly(salary);
  657. } else if (jobPageType === JobPageType.MOBILE_RECOMMEND || jobPageType === JobPageType.MOBILE_SEARCH) {
  658. const salary = jobElement.querySelector('h3 small').textContent;
  659. return parseSalaryToMonthly(salary);
  660. } else {
  661. throw new Error('Not a job element')
  662. }
  663. }
  664.  
  665. async parseCompanyName(jobElement, jobPageType) {
  666. if (jobPageType === JobPageType.SEARCH || jobPageType === JobPageType.RECOMMEND) {
  667. return jobElement.querySelector('.job-detail-box > div > div > span').textContent;
  668. } else if (jobPageType === JobPageType.MOBILE_SEARCH) {
  669. return jobElement.querySelector('.job-card-company').textContent;
  670. } else if (jobPageType === JobPageType.MOBILE_RECOMMEND) {
  671. return jobElement.querySelector('.job-card-company > span:first-child').textContent;
  672. } else {
  673. throw new Error('Not a job element')
  674. }
  675. }
  676.  
  677. async parseJobName(jobElement, jobPageType) {
  678. if (jobPageType === JobPageType.SEARCH || jobPageType === JobPageType.RECOMMEND) {
  679. return jobElement.querySelector('.job-detail-box > a > div > div > div').textContent;
  680. } else if (jobPageType === JobPageType.MOBILE_SEARCH) {
  681. return jobElement.querySelector('h3 > span:first-child').textContent;
  682. } else if (jobPageType === JobPageType.MOBILE_RECOMMEND) {
  683. return jobElement.querySelector('.job-title > span').textContent;
  684. } else {
  685. throw new Error('Not a job element')
  686. }
  687. }
  688.  
  689. async jobIsActive(jobElement, jobPageType) {
  690. if (jobPageType === JobPageType.SEARCH) {
  691. /*
  692. // 请求频繁容易被封,暂时先不支持
  693. const requestUrl = `https://${location.host}/a/${this.fetchJobUniqueKey(jobElement, jobPageType)}.shtml`;
  694. const response = await fetch(requestUrl);
  695. const documentText = await response.text();
  696. const parser = new DOMParser();
  697. const parseDocument = parser.parseFromString(documentText, 'text/html');
  698. const onlineElement = parseDocument.querySelector('div.name-box span.online');
  699. if(onlineElement == null){
  700. return false;
  701. }
  702. console.log("活跃状态:" + onlineElement.textContent);
  703. return onlineElement.textContent.includes('在线');
  704. */
  705. return true;
  706. } else if (jobPageType === JobPageType.RECOMMEND) {
  707. const onlineMessage = jobElement.querySelector('.recruiter-info-box').textContent;
  708. if (onlineMessage == null) {
  709. return false;
  710. }
  711. return onlineMessage.includes('在线');
  712. } else if (jobPageType === JobPageType.MOBILE_SEARCH) {
  713. return true;
  714. } else if (jobPageType === JobPageType.MOBILE_RECOMMEND) {
  715. return true;
  716. } else {
  717. throw new Error('Not a job element')
  718. }
  719. }
  720.  
  721. addViewedCallback(jobElement, jobPageType, eventCallback) {
  722. jobElement.addEventListener('click', eventCallback, true);
  723. }
  724.  
  725.  
  726. markCurJobElement(jobElement, jobPageType, jobFilterTypes) {
  727. let container;
  728. if (jobPageType === JobPageType.SEARCH || jobPageType === JobPageType.RECOMMEND) {
  729. container = jobElement.querySelector('.job-detail-box a > div');
  730. this.changePcJobElementColor(jobElement, jobPageType);
  731. } else if (jobPageType === JobPageType.MOBILE_RECOMMEND || jobPageType === JobPageType.MOBILE_SEARCH) {
  732. container = this.fetchMobileJobElementContainer(jobElement);
  733. this.changeMobileJobElementColor(jobElement, jobPageType);
  734. } else {
  735. throw new Error('Not a job element')
  736. }
  737. if (container == null) {
  738. return;
  739. }
  740.  
  741. let markSpan = container.querySelector('.mark');
  742. if (markSpan === null) {
  743. markSpan = document.createElement('span');
  744. markSpan.classList.add('mark');
  745. markSpan.style.color = 'red';
  746. markSpan.style.float = 'left';
  747. container.insertBefore(markSpan, container.firstChild);
  748. }
  749. markSpan.textContent = '(' + jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|') + ')';
  750. }
  751.  
  752.  
  753. blockCurJobElement(jobElement, jobPageType, jobFilterTypes) {
  754. jobElement.classList.add('filter-blocked');
  755. const message = jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|');
  756. if (jobPageType === JobPageType.SEARCH || jobPageType === JobPageType.RECOMMEND) {
  757. const cardBody = jobElement.querySelector('.job-detail-box > a');
  758. cardBody.innerHTML = `
  759. <div class="tip" style="color: dimgray; font-weight: bold; font-size: large; padding-top: 20px">已屏蔽</div>
  760. `;
  761. const cardFooter = jobElement.querySelector('div[data-nick="job-detail-company-info"] > div');
  762. cardFooter.innerHTML = `
  763. <span>${message}</span>
  764. `;
  765. const avatar = jobElement.querySelector('div.job-card-right-box');
  766. avatar.innerHTML = ``;
  767. this.changePcJobElementColor(jobElement, jobPageType);
  768. } else if (jobPageType === JobPageType.MOBILE_RECOMMEND || jobPageType === JobPageType.MOBILE_SEARCH) {
  769. const cardBody = this.fetchMobileJobElementContainer(jobElement);
  770. cardBody.innerHTML = `
  771. <div class="tip" style="color: dimgray; font-weight: bold; font-size: large; padding-top: 20px">已屏蔽</div>
  772. <div><span>${message}</span></div>
  773. `;
  774. this.changeMobileJobElementColor(jobElement, jobPageType);
  775. } else {
  776. throw new Error('Not a job element')
  777. }
  778. }
  779.  
  780. removeCurJobElement(jobElement, jobPageType) {
  781. if (jobPageType === JobPageType.RECOMMEND) {
  782. // 如果直接删除元素页面切换tab的时候会报错,这里隐藏
  783. // jobElement.style.display = 'none';
  784. jobElement.innerHTML = ``;
  785. } else {
  786. jobElement.parentElement.removeChild(jobElement);
  787. }
  788. }
  789.  
  790. /**
  791. *
  792. * @param {Element} jobElement
  793. * @param {JobPageType} jobPageType
  794. */
  795. changePcJobElementColor(jobElement, jobPageType) {
  796. const container = jobElement.querySelector('.job-card-pc-container');
  797. if (container == null) {
  798. return;
  799. }
  800. container.style.backgroundColor = '#e1e1e1';
  801. }
  802.  
  803.  
  804. /**
  805. *
  806. * @param {Element} jobElement
  807. * @param {JobPageType} jobPageType
  808. */
  809. changeMobileJobElementColor(jobElement, jobPageType) {
  810. jobElement.style.backgroundColor = '#e1e1e1';
  811. }
  812.  
  813. /**
  814. *
  815. * @param {Element} jobElement
  816. * @returns {Element}
  817. */
  818. fetchMobileJobElementContainer(jobElement) {
  819. if (jobElement.classList.contains('job-card')) {
  820. return jobElement;
  821. }
  822. return jobElement.querySelector('.job-card');
  823. }
  824.  
  825. /**
  826. *
  827. * @param {String} url
  828. * @returns {String}
  829. */
  830. fetchUniqueKeyFromUrl(url) {
  831. const regex = /\/(\d+)\.shtml/;
  832. const match = url.match(regex);
  833. if (match && match[1]) {
  834. if (match[1].length === 10) {
  835. return match[1].slice(2);
  836. } else {
  837. return match[1];
  838. }
  839. }
  840. return null;
  841. }
  842.  
  843. }
  844.  
  845. class BossStrategy extends PlatFormStrategy {
  846. fetchJobPageType() {
  847. if (document.querySelector('.search-job-result') != null) {
  848. return JobPageType.SEARCH;
  849. } else if (document.querySelector('.rec-job-list') != null) {
  850. return JobPageType.RECOMMEND;
  851. } else if (document.querySelector('.job-recommend .job-list') != null) {
  852. return JobPageType.MOBILE_RECOMMEND;
  853. } else if (document.querySelector('#main .job-list') != null) {
  854. return JobPageType.MOBILE_SEARCH;
  855. }
  856. return null;
  857. }
  858.  
  859. fetchJobElements(jobPageType) {
  860. if (jobPageType === JobPageType.SEARCH) {
  861. return document.querySelectorAll('ul.job-list-box > li.job-card-wrapper');
  862. } else if (jobPageType === JobPageType.RECOMMEND) {
  863. return document.querySelectorAll('ul.rec-job-list > div > li.job-card-box');
  864. } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
  865. return document.querySelectorAll('.job-list > ul > li');
  866. } else {
  867. throw new Error('Not a job element')
  868. }
  869. }
  870.  
  871. fetchJobUniqueKey(jobElement, jobPageType) {
  872. if (jobElement.classList.contains('filter-blocked')) {
  873. return null;
  874. }
  875. if (jobPageType === JobPageType.SEARCH) {
  876. const element = jobElement.querySelector('.job-card-left');
  877. if (element == null) {
  878. return null;
  879. }
  880. const url = element.href;
  881. if (url == null) {
  882. return null;
  883. }
  884. return url.split('/job_detail/')[1].split('.html')[0];
  885. } else if (jobPageType === JobPageType.RECOMMEND) {
  886. const element = jobElement.querySelector('.job-name');
  887. if (element == null) {
  888. return null;
  889. }
  890. const url = element.href;
  891. if (url == null) {
  892. return null;
  893. }
  894. return url.split('/job_detail/')[1].split('.html')[0];
  895. } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
  896. const element = jobElement.querySelector('a');
  897. if (element == null) {
  898. return null;
  899. }
  900.  
  901. const url = element.href;
  902. if (url == null) {
  903. return null;
  904. }
  905. return url.split('/job_detail/')[1].split('.html')[0];
  906. } else {
  907. throw new Error('Not a job element')
  908. }
  909. }
  910.  
  911. async parseSalary(jobElement, jobPageType) {
  912. if (jobPageType === JobPageType.SEARCH) {
  913. const salary = jobElement.querySelector('.salary').textContent;
  914. return parseSalaryToMonthly(salary);
  915. } else if (jobPageType === JobPageType.RECOMMEND) {
  916. const salary = this.convertSalaryField(jobElement.querySelector('.job-salary').textContent);
  917. return parseSalaryToMonthly(salary);
  918. } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
  919. const salary = jobElement.querySelector('.salary').textContent;
  920. return parseSalaryToMonthly(salary);
  921. } else {
  922. throw new Error('Not a job element')
  923. }
  924. }
  925.  
  926. async parseCompanyName(jobElement, jobPageType) {
  927. if (jobPageType === JobPageType.SEARCH) {
  928. return jobElement.querySelector('.company-name > a').textContent
  929. } else if (jobPageType === JobPageType.RECOMMEND) {
  930. return jobElement.querySelector('.boss-name').textContent
  931. } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
  932. return jobElement.querySelector('.company').textContent
  933. } else {
  934. throw new Error('Not a job element')
  935. }
  936. }
  937.  
  938. async parseJobName(jobElement, jobPageType) {
  939. if (jobPageType === JobPageType.SEARCH) {
  940. return jobElement.querySelector('.job-name').textContent
  941. } else if (jobPageType === JobPageType.RECOMMEND) {
  942. return jobElement.querySelector('.job-name').textContent
  943. } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
  944. return jobElement.querySelector('.title-text').textContent
  945. } else {
  946. throw new Error('Not a job element')
  947. }
  948. }
  949.  
  950. async jobIsActive(jobElement, jobPageType) {
  951. const onlineTag = jobElement.querySelector('.boss-online-tag');
  952. if (onlineTag != null) {
  953. return true;
  954. }
  955. const onlineIcon = jobElement.querySelector('.boss-online-icon');
  956. if (onlineIcon != null) {
  957. return true;
  958. }
  959.  
  960. const details = await this.fetchJobDetails(jobElement, jobPageType);
  961. return details.active;
  962. }
  963.  
  964. addViewedCallback(jobElement, jobPageType, eventCallback) {
  965. jobElement.addEventListener('click', eventCallback, true);
  966. }
  967.  
  968. markCurJobElement(jobElement, jobPageType, jobFilterTypes) {
  969. if (jobPageType === JobPageType.SEARCH || jobPageType === JobPageType.RECOMMEND) {
  970. const titleElement = jobElement.querySelector('.job-title');
  971. let markSpan = titleElement.querySelector('.mark');
  972. if (markSpan === null) {
  973. markSpan = document.createElement('span');
  974. markSpan.classList.add('mark');
  975. markSpan.style.color = 'red';
  976. markSpan.style.float = 'left';
  977. titleElement.insertBefore(markSpan, titleElement.firstChild);
  978. }
  979. markSpan.textContent = '(' + jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|') + ')';
  980. this.changeJobElementColor(jobElement, jobPageType);
  981.  
  982. } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
  983. const titleElement = jobElement.querySelector('.title');
  984. let markSpan = titleElement.querySelector('.mark');
  985. if (markSpan === null) {
  986. markSpan = document.createElement('span');
  987. markSpan.classList.add('mark');
  988. markSpan.style.color = 'red';
  989. markSpan.style.float = 'left';
  990. titleElement.insertBefore(markSpan, titleElement.firstChild);
  991. }
  992. markSpan.textContent = '(' + jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|') + ')';
  993. this.changeJobElementColor(jobElement, jobPageType);
  994. } else {
  995. throw new Error('Not a job element')
  996. }
  997. }
  998.  
  999. blockCurJobElement(jobElement, jobPageType, jobFilterTypes) {
  1000. jobElement.classList.add('filter-blocked');
  1001. const message = jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|');
  1002. if (jobPageType === JobPageType.SEARCH) {
  1003. const cardBody = jobElement.querySelector('.job-card-body');
  1004. cardBody.innerHTML = `
  1005. <div class="job-card-left"></div>
  1006. <div class="tip" style="color: dimgray; font-weight: bold; font-size: large; padding-top: 20px">已屏蔽</div>
  1007. <div class="job-card-right"></div>
  1008. `;
  1009. const cardFooter = jobElement.querySelector('.job-card-footer');
  1010. cardFooter.innerHTML = `
  1011. <div class="info-desc">${message}</div>
  1012. `;
  1013. this.changeJobElementColor(jobElement, jobPageType);
  1014. } else if (jobPageType === JobPageType.RECOMMEND) {
  1015. const cardBody = jobElement.querySelector('.job-info');
  1016. cardBody.innerHTML = `
  1017. <div class="tip" style="color: dimgray; font-weight: bold; font-size: large; padding-top: 20px">已屏蔽</div>
  1018. `;
  1019. const cardFooter = jobElement.querySelector('.job-card-footer');
  1020. cardFooter.innerHTML = `
  1021. <div class="info-desc">${message}</div>
  1022. `;
  1023. this.changeJobElementColor(jobElement, jobPageType);
  1024. } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
  1025. const cardBody = jobElement.querySelector('a');
  1026. cardBody.innerHTML = `
  1027. <div class="tip" style="color: dimgray; font-weight: bold; font-size: large; padding-top: 20px">已屏蔽</div>
  1028. <span>${message}</span>
  1029. `;
  1030. this.changeJobElementColor(jobElement, jobPageType);
  1031. } else {
  1032. throw new Error('Not a job element')
  1033. }
  1034. }
  1035.  
  1036. removeCurJobElement(jobElement, jobPageType) {
  1037. jobElement.parentElement.removeChild(jobElement);
  1038. // jobElement.style.display = 'none';
  1039. }
  1040.  
  1041. /**
  1042. *
  1043. * @param {Element} jobElement
  1044. * @param {JobPageType} jobPageType
  1045. */
  1046. changeJobElementColor(jobElement, jobPageType) {
  1047. if (jobPageType === JobPageType.SEARCH || jobPageType === JobPageType.RECOMMEND || jobPageType === JobPageType.MOBILE_RECOMMEND || jobPageType === JobPageType.MOBILE_SEARCH) {
  1048. jobElement.style.backgroundColor = '#e1e1e1';
  1049. } else {
  1050. throw new Error('Not a job element')
  1051. }
  1052. }
  1053.  
  1054. /**
  1055. *
  1056. * @param {String} salary
  1057. */
  1058. convertSalaryField(salary) {
  1059. // 恶心的BOSS添加了特殊字符,需要转换
  1060. let res = '';
  1061. for (let i = 0; i < salary.length; i++) {
  1062. let charCode = salary.charCodeAt(i);
  1063. if (charCode >= 57393 && charCode <= 57402) {
  1064. charCode = charCode - 57393 + 48;
  1065. }
  1066. res += String.fromCharCode(charCode);
  1067. }
  1068. return res;
  1069. }
  1070.  
  1071. /**
  1072. * @param {Element} jobElement
  1073. * @param {JobPageType} jobPageType
  1074. * @returns {Promise<{active: boolean}>}
  1075. */
  1076. async fetchJobDetails(jobElement, jobPageType) {
  1077. let active = false;
  1078.  
  1079. if (jobPageType === JobPageType.SEARCH) {
  1080. const url = `https://www.zhipin.com/wapi/zpgeek/job/card.json?${jobElement.querySelector(".job-card-left").href.split("?")[1]}`;
  1081. const response = await fetch(url);
  1082. const json = await response.json();
  1083. if (json.code === 0) {
  1084. const card = json.zpData.jobCard;
  1085. const activeTimeDesc = card.activeTimeDesc;
  1086. if (card.online || (activeTimeDesc && (activeTimeDesc.includes('刚刚') || activeTimeDesc.includes('日') || activeTimeDesc.includes('本周')))) {
  1087. active = true;
  1088. } else {
  1089. console.log("活跃状态:" + activeTimeDesc);
  1090. }
  1091. }
  1092. } else {
  1093. // 防止限流,其他页面展示不支持
  1094. active = true;
  1095. }
  1096.  
  1097. return {
  1098. "active": active,
  1099. }
  1100. }
  1101.  
  1102.  
  1103. }
  1104.  
  1105. // setTimeout(handleScheduleJob, 1000);
  1106. setInterval(handleScheduleJob, 1000);
  1107.  
  1108. async function handleScheduleJob() {
  1109. if (lock) {
  1110. return;
  1111. }
  1112. const strategy = getStrategy();
  1113. if (strategy == null) {
  1114. return;
  1115. }
  1116. let jobPageType = strategy.fetchJobPageType();
  1117. if (jobPageType == null) {
  1118. return;
  1119. }
  1120. lock = true;
  1121. try {
  1122. await handleElements(strategy, jobPageType);
  1123. } catch (e) {
  1124. console.error('处理元素失败:', e);
  1125. } finally {
  1126. lock = false;
  1127. }
  1128. }
  1129.  
  1130. /**
  1131. *
  1132. * @param {PlatFormStrategy}strategy
  1133. * @param {JobPageType} jobPageType
  1134. * @returns {Promise<void>}
  1135. */
  1136. async function handleElements(strategy, jobPageType) {
  1137. if (getPageHash(strategy, jobPageType) === curPageHash) {
  1138. return;
  1139. }
  1140. const elements = strategy.fetchJobElements(jobPageType);
  1141.  
  1142.  
  1143. let validElements = fetchValidElements(strategy, jobPageType, elements);
  1144.  
  1145. const platForm = choosePlatForm();
  1146. const viewedJobIds = getJobViewedSet(platForm);
  1147.  
  1148. // 添加点击事件
  1149. validElements.forEach(value => {
  1150. initEventHandler(platForm, strategy, jobPageType, value.element);
  1151. })
  1152.  
  1153. const filterPromises = validElements.map(value => {
  1154. return filterElementTask(strategy, value.element, jobPageType, viewedJobIds);
  1155. });
  1156. await Promise.all(filterPromises);
  1157.  
  1158. curPageHash = getPageHash(strategy, jobPageType);
  1159. }
  1160.  
  1161. /**
  1162. *
  1163. * @param {PlatFormStrategy} strategy
  1164. * @param {Element} jobElement
  1165. * @param {JobPageType} jobPageType
  1166. * @param {Set<String>} viewedJobIds
  1167. * @returns {Promise<void>}
  1168. */
  1169. async function filterElementTask(strategy, jobElement, jobPageType, viewedJobIds) {
  1170. const jobInfo = await fetchJobInfo(strategy, jobElement, jobPageType).then(value => {
  1171. console.log(`Id${value.id},公司:${value.companyName},岗位:${value.jobName},月薪: ${value.salary.min} - ${value.salary.max}`);
  1172. return value;
  1173. });
  1174. const filterTypes = getJobFilterType(jobInfo, viewedJobIds);
  1175. handleFilterElement(strategy, jobElement, jobPageType, filterTypes);
  1176. }
  1177.  
  1178. /**
  1179. *
  1180. * @param {PlatFormStrategy} strategy
  1181. * @param {Element} job
  1182. * @param {JobPageType} pageType
  1183. * @returns {Promise<{ id:string, jobName: string, companyName: string, salary: {min: number, max: number}, isActive: boolean}>}
  1184. */
  1185. async function fetchJobInfo(strategy, job, pageType) {
  1186. try {
  1187. const [id, jobName, companyName, salary, isActive] = await Promise.all([
  1188. strategy.fetchJobUniqueKey(job, pageType),
  1189. strategy.parseJobName(job, pageType),
  1190. strategy.parseCompanyName(job, pageType),
  1191. strategy.parseSalary(job, pageType),
  1192. strategy.jobIsActive(job, pageType)
  1193. ]);
  1194.  
  1195. return { id, jobName, companyName, salary, isActive };
  1196. } catch (error) {
  1197. throw error;
  1198. }
  1199. }
  1200.  
  1201. /**
  1202. * @param {{ id:string, jobName: string, companyName: string, salary: {min: number, max: number}, isActive: boolean}} jobInfo
  1203. * @param {Set<String>} viewedJobs
  1204. * @returns {JobFilterType[]}
  1205. */
  1206. function getJobFilterType(jobInfo, viewedJobs) {
  1207. const filterTypes = new Set();
  1208. const companyName = jobInfo.companyName.toLowerCase();
  1209. for (let i = 0; i < settings.blacklist.length; i++) {
  1210. if (companyName.includes(settings.blacklist[i])) {
  1211. filterTypes.add(JobFilterType.BLACKLIST);
  1212. break
  1213. }
  1214. }
  1215. if (viewedJobs.has(jobInfo.id)) {
  1216. filterTypes.add(JobFilterType.VIEWED);
  1217. }
  1218.  
  1219. const companySalary = jobInfo.salary;
  1220. if (settings.salaryFilterType === 'include') {
  1221. if (companySalary.min < settings.minSalary || companySalary.max > settings.maxSalary) {
  1222. filterTypes.add(JobFilterType.MISMATCH_CONDITION);
  1223. }
  1224. } else if (settings.salaryFilterType === 'overlap') {
  1225. if (!(companySalary.max >= settings.minSalary && settings.maxSalary >= companySalary.min)) {
  1226. filterTypes.add(JobFilterType.MISMATCH_CONDITION);
  1227. }
  1228. }
  1229.  
  1230. if (settings.filterInactiveJob && !jobInfo.isActive) {
  1231. filterTypes.add(JobFilterType.MISMATCH_CONDITION);
  1232. }
  1233. return [...filterTypes];
  1234. }
  1235.  
  1236. /**
  1237. *
  1238. * @param {PlatFormStrategy} strategy
  1239. * @param {Element} jobElement
  1240. * @param {JobPageType} jobPageType
  1241. * @param {JobFilterType[]} jobFilterTypes
  1242. */
  1243. function handleFilterElement(strategy, jobElement, jobPageType, jobFilterTypes) {
  1244. if (jobFilterTypes.length === 0) {
  1245. return;
  1246. }
  1247. // 过滤掉NoOp的过滤类型,不然后面会拼接出提示
  1248. let actionFilterTypes = [];
  1249. let filter = jobFilterTypes.map(filterType => {
  1250. let action = null;
  1251. if (filterType === JobFilterType.BLACKLIST) {
  1252. action = settings.blacklistAction;
  1253. } else if (filterType === JobFilterType.VIEWED) {
  1254. action = settings.viewedAction;
  1255. } else if (filterType === JobFilterType.MISMATCH_CONDITION) {
  1256. action = settings.conditionAction;
  1257. }
  1258. if (action !== 'noop') {
  1259. actionFilterTypes.push(filterType);
  1260. }
  1261. return action;
  1262. }).filter(action => action != null && typeof action === 'string');
  1263. if (filter.includes('delete')) {
  1264. strategy.removeCurJobElement(jobElement, jobPageType);
  1265. } else if (filter.includes('hide')) {
  1266. strategy.blockCurJobElement(jobElement, jobPageType, actionFilterTypes);
  1267. } else if (filter.includes('mark')) {
  1268. strategy.markCurJobElement(jobElement, jobPageType, actionFilterTypes);
  1269. }
  1270. }
  1271.  
  1272. /**
  1273. *
  1274. * @param {PlatFormStrategy} strategy
  1275. * @param {JobPageType} jobPageType
  1276. * @param {NodeListOf<Element>} elements
  1277. * @returns {[{id: string, element: Element}]}
  1278. */
  1279. function fetchValidElements(strategy, jobPageType, elements) {
  1280. const validElements = [];
  1281. for (let i = 0; i < elements.length; i++) {
  1282. const id = strategy.fetchJobUniqueKey(elements[i], jobPageType);
  1283. if (id === null) {
  1284. continue;
  1285. }
  1286. validElements.push({ 'id': id, element: elements[i] });
  1287. }
  1288. return validElements;
  1289. }
  1290.  
  1291. /**
  1292. *
  1293. * @param {PlatFormStrategy} strategy
  1294. * @param {JobPageType} jobPageType
  1295. * @returns {number}
  1296. */
  1297. function getPageHash(strategy, jobPageType) {
  1298. const elements = strategy.fetchJobElements(jobPageType)
  1299. const keys = fetchValidElements(strategy, jobPageType, elements).map(t => t.id).sort().join();
  1300. return hashCode(keys);
  1301. }
  1302.  
  1303. /**
  1304. *@param {Symbol} jobPlatform
  1305. * @param {PlatFormStrategy} strategy
  1306. * @param {JobPageType} jobPageType
  1307. * @param {Element} element
  1308. */
  1309. function initEventHandler(jobPlatform, strategy, jobPageType, element) {
  1310. const callBack = () => {
  1311. const id = strategy.fetchJobUniqueKey(element, jobPageType);
  1312. setJobViewed(jobPlatform, id);
  1313. // 重置PageHash,来刷新
  1314. curPageHash = null;
  1315. };
  1316. strategy.addViewedCallback(element, jobPageType, callBack);
  1317. }
  1318.  
  1319. /**
  1320. *
  1321. * @param {String} str
  1322. * @returns {number}
  1323. */
  1324. function hashCode(str) {
  1325. let hash = 0;
  1326. if (str.length === 0) return hash;
  1327. for (let i = 0; i < str.length; i++) {
  1328. hash = (hash << 5) - hash + str.charCodeAt(i);
  1329. hash |= 0;
  1330. }
  1331. return hash;
  1332. }
  1333.  
  1334. const CacheManager = {
  1335. /**
  1336. * 设置缓存(ttl 为可选参数,单位毫秒)
  1337. * @param {String} key
  1338. * @param value
  1339. * @param {Number} ttl
  1340. */
  1341. set: function (key, value, ttl) {
  1342. const data = { value: value };
  1343. if (typeof ttl === 'number') {
  1344. data.expires = Date.now() + ttl;
  1345. }
  1346. localStorage.setItem(key, JSON.stringify(data));
  1347. },
  1348.  
  1349. /**
  1350. * 获取缓存(自动处理过期)
  1351. * @param {String} key
  1352. * @returns {*|null}
  1353. */
  1354. get: function (key) {
  1355. const item = localStorage.getItem(key);
  1356. if (!item) return null;
  1357.  
  1358. try {
  1359. const data = JSON.parse(item);
  1360. // 检查过期时间(如果存在)
  1361. if (data.expires && Date.now() > data.expires) {
  1362. this.delete(key);
  1363. return null;
  1364. }
  1365. return data.value;
  1366. } catch (e) {
  1367. console.error('缓存解析失败:', e);
  1368. this.delete(key);
  1369. return null;
  1370. }
  1371. },
  1372.  
  1373. /**
  1374. * 删除指定缓存
  1375. * @param {String} key
  1376. */
  1377. delete: function (key) {
  1378. localStorage.removeItem(key);
  1379. },
  1380.  
  1381. /**
  1382. * 清除所有带过期时间的缓存
  1383. */
  1384. cleanExpired: function () {
  1385. for (let i = localStorage.length - 1; i >= 0; i--) {
  1386. const key = localStorage.key(i);
  1387. this.get(key); // 自动触发过期检查
  1388. }
  1389. }
  1390. };
  1391.  
  1392. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址