求职助手

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

// ==UserScript==
// @name         求职助手
// @namespace    job_seeking_helper
// @author       Gloduck
// @license MIT
// @version      1.1.2
// @description  为相关的求职平台(BOSS直聘、拉勾、智联招聘、猎聘)添加一些实用的小功能,如自定义薪资范围、过滤黑名单公司等。
// @match        https://www.zhipin.com/*
// @match        https://m.zhipin.com/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function () {
    'use strict';

    // 策略枚举
    const JobPlatform = {
        BOSS_ZHIPIN: Symbol('Boss'), LAGOU: Symbol('Lagou'), UNKNOWN: Symbol('未知网站')
    };

    const JobPageType = {
        SEARCH: Symbol('Search'),
        RECOMMEND: Symbol('Recommend'),
        MOBILE_SEARCH: Symbol('MobileSearch'),
        MOBILE_RECOMMEND: Symbol('MobileRecommend')
    }

    const JobFilterType = {
        BLACKLIST: Symbol('Blacklist'), VIEWED: Symbol('Viewed'), MISMATCH_CONDITION: Symbol('MismatchCondition'),
    }

    let curPageHash = null;
    let lock = false;
    // 默认设置
    const defaults = {
        blacklist: [],
        minSalary: 0,
        maxSalary: Infinity,
        salaryFilterType: 'include',
        filterInactiveJob: false,
        maxDailyHours: 8,
        maxMonthlyDays: 22,
        maxWeeklyCount: 4,
        viewedAction: 'mark',
        blacklistAction: 'hide',
        conditionAction: 'hide'
    };

    // 加载用户设置
    const settings = {
        blacklist: GM_getValue('blacklist', defaults.blacklist),
        minSalary: GM_getValue('minSalary', defaults.minSalary),
        maxSalary: GM_getValue('maxSalary', defaults.maxSalary),
        salaryFilterType: GM_getValue('salaryFilterType', defaults.salaryFilterType),
        filterInactiveJob: GM_getValue('filterInactiveJob', defaults.filterInactiveJob),
        maxDailyHours: GM_getValue('maxDailyHours', defaults.maxDailyHours),
        maxMonthlyDays: GM_getValue('maxMonthlyDays', defaults.maxMonthlyDays),
        maxWeeklyCount: GM_getValue('maxWeeklyCount', defaults.maxWeeklyCount),
        viewedAction: GM_getValue('viewedAction', defaults.viewedAction),
        blacklistAction: GM_getValue('blacklistAction', defaults.blacklistAction),
        conditionAction: GM_getValue('conditionAction', defaults.conditionAction)
    };

    // 注册(不可用)设置菜单
    GM_registerMenuCommand('职位过滤设置', showSettings);

    function showSettings() {
        const dialog = document.createElement('div');
        dialog.style = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #ffffff;
            padding: 25px;
            border-radius: 12px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.15);
            z-index: 9999;
            min-width: 380px;
            font-family: 'Segoe UI', sans-serif;
        `;

        dialog.innerHTML = `
            <h2 style="color: #2c3e50; margin: 0 0 20px; font-size: 1.4em;">职位过滤设置</h2>
            
            <!-- 基础设置 -->
            <div style="margin-bottom: 20px;">
                <label style="display: block; margin-bottom: 8px; font-weight: 500;">黑名单关键词(逗号分隔)</label>
                <input type="text" id="blacklist" 
                    style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"
                    value="${settings.blacklist.join(',')}">
            </div>

            <!-- 薪资设置 -->
            
            <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
                <h3 style="color: #2c3e50; margin: 0 0 15px; font-size: 1.1em;">薪资设置</h3>
                <div style="margin-bottom: 12px;">
                    <label style="display: block; margin-bottom: 6px;">薪资范围(元/月):</label>
                    <div style="display: flex; gap: 10px;">
                        <input type="number" id="minSalary" placeholder="最低" 
                            style="flex: 1; padding: 8px;" value="${settings.minSalary}">
                        <span style="align-self: center;">-</span>
                        <input type="number" id="maxSalary" placeholder="最高" 
                            style="flex: 1; padding: 8px;" value="${settings.maxSalary}">
                    </div>
                </div>
                <div style="margin-bottom: 12px;">
                    <label style="display: block; margin-bottom: 6px;">薪资过滤方式:</label>
                    ${createRadioGroup('salaryFilterType', ['include', 'overlap'], settings.salaryFilterType, {
            include: '包含范围',
            overlap: '存在交集'
        })}
                </div>

            </div>
            
            <!-- 其他设置 -->
            <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
                <h3 style="color: #2c3e50; margin: 0 0 15px; font-size: 1.1em;">其他设置</h3>
                <div style="margin-bottom: 12px;">

                    <label style="display: flex; align-items: center; gap: 8px; color: #34495e;">
                        <input type="checkbox" id="filterInactiveJob" ${settings.filterInactiveJob ? 'checked' : ''}>
                        过滤不活跃的职位
                    </label>
                </div>

            </div>
            
            <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
                <h3 style="color: #2c3e50; margin: 0 0 15px; font-size: 1.1em;">工作时间限制</h3>
                
                <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px;">
                    <div>
                        <label style="display: block; margin-bottom: 6px;">日工作小时</label>
                        <input type="number" id="maxDailyHours" 
                            min="1" max="24" step="0.5"
                            style="width: 100%; padding: 8px;"
                            value="${settings.maxDailyHours}">
                    </div>
                    
                    <div>
                        <label style="display: block; margin-bottom: 6px;">月工作天数</label>
                        <input type="number" id="maxMonthlyDays" 
                            min="1" max="31" step="1"
                            style="width: 100%; padding: 8px;"
                            value="${settings.maxMonthlyDays}">
                    </div>
                    
                    <div>
                        <label style="display: block; margin-bottom: 6px;">月工作周数</label>
                        <input type="number" id="maxWeeklyCount" 
                            min="1" max="6" step="0.5"
                            style="width: 100%; padding: 8px;"
                            value="${settings.maxWeeklyCount}">
                    </div>
                </div>
            </div>

            <!-- 整合后的处理方式设置 -->
            <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
                <h3 style="color: #2c3e50; margin: 0 0 15px; font-size: 1.1em;">处理方式设置</h3>
                
                <div style="margin-bottom: 12px;">
                    <label style="display: block; margin-bottom: 6px;">黑名单职位:</label>
                    ${createRadioGroup('blacklistAction', ['delete', 'hide', 'mark', 'noop'], settings.blacklistAction, {
            delete: '删除',
            hide: '屏蔽',
            mark: '标识',
            noop: '无操作'
        })}
                </div>

                <div style="margin-bottom: 12px;">
                    <label style="display: block; margin-bottom: 6px;">已查看职位:</label>
                    ${createRadioGroup('viewedAction', ['delete', 'hide', 'mark', 'noop'], settings.viewedAction, {
            delete: '删除',
            hide: '屏蔽',
            mark: '标识',
            noop: '无操作'
        })}
                </div>

                <div>
                    <label style="display: block; margin-bottom: 6px;">不符合条件职位:</label>
                    ${createRadioGroup('conditionAction', ['delete', 'hide', 'mark', 'noop'], settings.conditionAction, {
            delete: '删除',
            hide: '屏蔽',
            mark: '标识',
            noop: '无操作'
        })}
                </div>
            </div>
            
            <div style="margin-top: 25px; padding-top: 20px; border-top: 1px solid #eee;">
                <button id="clearCacheBtn" 
                    style="
                        background: #95a5a6;
                        color: white;
                        border: none;
                        padding: 8px 16px;
                        border-radius: 4px;
                        cursor: pointer;
                        transition: opacity 0.2s;
                    ">
                    清理已查看职位
                </button>
                <span style="color: #7f8c8d; font-size: 0.9em; margin-left: 10px;">
                    将重置当前平台所有已经查看的职位
                </span>
            </div>

            <!-- 操作按钮 -->
            <div style="display: flex; gap: 10px; justify-content: flex-end; border-top: 1px solid #eee; padding-top: 20px;">
                <button id="saveBtn" class="dialog-btn primary">保存</button>
                <button id="cancelBtn" class="dialog-btn secondary">取消</button>
            </div>
        `;

        // 添加样式
        const style = document.createElement('style');
        style.textContent = `
            .dialog-btn {
                padding: 8px 20px;
                border: none;
                border-radius: 6px;
                cursor: pointer;
                transition: all 0.2s;
                font-size: 14px;
            }
            .primary {
                background: #3498db;
                color: white;
            }
            .secondary {
                background: #f0f0f0;
                color: #666;
            }
            .radio-group {
                display: flex;
                gap: 15px;
                margin-top: 5px;
            }
            .radio-item label {
                display: flex;
                align-items: center;
                gap: 6px;
                cursor: pointer;
            }
        `;
        dialog.appendChild(style);
        document.body.appendChild(dialog);

        // 事件绑定
        dialog.querySelector('#saveBtn').addEventListener('click', saveSettings);
        dialog.querySelector('#cancelBtn').addEventListener('click', () => dialog.remove());
        dialog.querySelector('#clearCacheBtn').addEventListener('click', () => {
            if (confirm('确定要清理已查看职位吗?\n这将重置当前平台所有已经查看的职位!')) {
                clearViewedJob(choosePlatForm());
                alert('清理完成!');
                location.reload();
            }
        });
    }

    // 生成带自定义标签的单选组
    function createRadioGroup(name, values, selected, labels) {
        return `
            <div class="radio-group">
                ${values.map(value => `
                    <div class="radio-item">
                        <label>
                            <input type="radio" name="${name}" value="${value}" ${value === selected ? 'checked' : ''}>
                            ${labels[value] || value}
                        </label>
                    </div>
                `).join('')}
            </div>
        `;
    }


    function saveSettings() {
        settings.blacklist = document.querySelector('#blacklist').value
            .split(',')
            .map(s => s.trim().toLowerCase())
            .filter(Boolean);

        settings.minSalary = parseInt(document.querySelector('#minSalary').value) || 0;
        settings.maxSalary = parseInt(document.querySelector('#maxSalary').value) || Infinity;
        settings.salaryFilterType = document.querySelector('input[name="salaryFilterType"]:checked').value;
        settings.filterInactiveJob = document.querySelector('#filterInactiveJob:checked').value;
        settings.viewedAction = document.querySelector('input[name="viewedAction"]:checked').value;
        settings.blacklistAction = document.querySelector('input[name="blacklistAction"]:checked').value;
        settings.conditionAction = document.querySelector('input[name="conditionAction"]:checked').value;
        settings.maxDailyHours = parseFloat(document.querySelector('#maxDailyHours').value) || 0;
        settings.maxMonthlyDays = parseInt(document.querySelector('#maxMonthlyDays').value) || 0;
        settings.maxWeeklyCount = parseFloat(document.querySelector('#maxWeeklyCount').value) || 0;

        // 保存设置
        GM_setValue('blacklist', settings.blacklist);
        GM_setValue('minSalary', settings.minSalary);
        GM_setValue('maxSalary', settings.maxSalary);
        GM_setValue('salaryFilterType', settings.salaryFilterType);
        GM_setValue('filterInactiveJob', settings.filterInactiveJob);
        GM_setValue('viewedAction', settings.viewedAction);
        GM_setValue('blacklistAction', settings.blacklistAction);
        GM_setValue('conditionAction', settings.conditionAction);
        GM_setValue('maxDailyHours', settings.maxDailyHours);
        GM_setValue('maxMonthlyDays', settings.maxMonthlyDays);
        GM_setValue('maxWeeklyCount', settings.maxWeeklyCount);

        document.querySelector('div').remove();
        alert('设置已保存!');
        location.reload();
    }

    /**
     *
     * @return {symbol}
     */
    function choosePlatForm() {
        const href = window.location.href;
        if (href.includes('zhipin.com')) {
            return JobPlatform.BOSS_ZHIPIN;
        } else {
            return JobPlatform.UNKNOWN;
        }
    }

    /**
     *
     * @returns {PlatFormStrategy}
     */
    function getStrategy() {
        switch (choosePlatForm()) {
            case JobPlatform.BOSS_ZHIPIN:
                return new BossStrategy();
            default:
                throw new Error('Unsupported platform')
        }
    }


    /**
     *
     * @param {String} salaryRange
     */
    function parseSalaryToMonthly(salaryRange) {
        const regex = /(\d+(\.\d+)?)\s*-\s*(\d+(\.\d+)?)/;
        const match = salaryRange.match(regex);

        if (!match) {
            throw new Error('Invalid salary range format');
        }

        // 提取最小值和最大值
        let minSalary = parseFloat(match[1]);
        let maxSalary = parseFloat(match[3]);
        if (salaryRange.includes('K') || salaryRange.includes('k')) {
            minSalary = minSalary * 1000;
            maxSalary = maxSalary * 1000;
        }
        if (salaryRange.includes('周')) {
            minSalary = minSalary * settings.maxWeeklyCount;
            maxSalary = maxSalary * settings.maxWeeklyCount;
        } else if (salaryRange.includes('天')) {
            minSalary = minSalary * settings.maxMonthlyDays;
            maxSalary = maxSalary * settings.maxMonthlyDays;
        } else if (salaryRange.includes('时')) {
            minSalary = minSalary * settings.maxMonthlyDays * settings.maxDailyHours;
            maxSalary = maxSalary * settings.maxMonthlyDays * settings.maxDailyHours;
        }
        return {
            min: minSalary, max: maxSalary,
        }
    }

    /**
     *
     * @param {Symbol} jobPlatform
     */
    function clearViewedJob(jobPlatform) {
        let jobViewKey = getJobViewKey(jobPlatform);
        // GM_setValue(jobViewKey, []);
        CacheManager.delete(jobViewKey);
    }

    /**
     *
     * @param {Symbol} jobPlatform
     * @param {String} uniqueKey
     */
    function setJobViewed(jobPlatform, uniqueKey) {
        let jobViewKey = getJobViewKey(jobPlatform);
        const jobViewedSet = getJobViewedSet(jobPlatform);
        if (jobViewedSet.has(uniqueKey)) {
            return;
        }
        jobViewedSet.add(uniqueKey);
        // GM_setValue(jobViewKey, [...jobViewedSet]);
        CacheManager.set(jobViewKey, [...jobViewedSet]);
    }

    /**
     *
     * @param {Symbol} jobPlatform
     * @return {Set<String>}
     */
    function getJobViewedSet(jobPlatform) {
        let jobViewKey = getJobViewKey(jobPlatform);
        // return new Set(GM_getValue(jobViewKey, []));
        const cache = CacheManager.get(jobViewKey);
        return new Set(cache != null ? cache : []);
    }

    /**
     *
     * @param {Symbol} jobPlatform
     * @return {string}
     */
    function getJobViewKey(jobPlatform) {
        return jobPlatform.description + "ViewHistory";
    }


    class PlatFormStrategy {
        /**
         * @returns {JobPageType}
         */
        fetchJobPageType() {
            throw new Error('Method not implemented');
        }

        /**
         * @param {JobPageType} jobPageType
         * @returns {NodeListOf<Element>}
         */
        fetchJobElements(jobPageType) {
            throw new Error('Method not implemented');

        }

        /**
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @returns {String|null}
         */
        fetchJobUniqueKey(jobElement, jobPageType) {
            throw new Error('Method not implemented');
        }

        /**
         * @param {Element} jobElement - 职位信息 DOM 元素
         * @param {JobPageType} jobPageType - 职位页面类型
         * @returns {Promise<{min: number, max: number}>}
         */
        async parseSalary(jobElement, jobPageType) {
            throw new Error('Method not implemented'); // 自动转换为 rejected Promise
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @returns {Promise<String>}
         */
        async parseJobName(jobElement, jobPageType) {
            throw new Error('Method not implemented');
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @returns {Promise<String>}
         */
        async parseCompanyName(jobElement, jobPageType) {
            throw new Error('Method not implemented');
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @returns {Promise<Boolean>}
         */
        async jobIsActive(jobElement, jobPageType) {
            throw new Error('Method not implemented');
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @param {JobFilterType[]} jobFilterTypes
         * @returns {void}
         */
        markCurJobElement(jobElement, jobPageType, jobFilterTypes) {
            throw new Error('Method not implemented');
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @param {JobFilterType[]} jobFilterTypes
         * @returns {void}
         */
        blockCurJobElement(jobElement, jobPageType, jobFilterTypes) {
            throw new Error('Method not implemented');
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @returns {void}
         */
        removeCurJobElement(jobElement, jobPageType) {
            throw new Error('Method not implemented');
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @param {Function} eventCallback
         * @returns {void}
         */
        addViewedCallback(jobElement, jobPageType, eventCallback) {
            throw new Error('Method not implemented');
        }

        /**
         *
         * @param {Element} element
         */
        addDeleteLine(element) {
            const delElement = document.createElement('del');

            while (element.firstChild) {
                delElement.appendChild(element.firstChild);
            }

            element.appendChild(delElement);
        }

        /**
         * @param {JobFilterType} jobFilterType
         * @returns {String}
         */
        convertFilterTypeToMessage(jobFilterType) {
            if (jobFilterType === JobFilterType.BLACKLIST) {
                return '黑名单';
            } else if (jobFilterType === JobFilterType.VIEWED) {
                return '已查看';
            } else if (jobFilterType === JobFilterType.MISMATCH_CONDITION) {
                return '条件不符';
            } else {
                return '未知';
            }
        }

    }

    class BossStrategy extends PlatFormStrategy {
        fetchJobPageType() {
            if (document.querySelector('.search-job-result') != null) {
                return JobPageType.SEARCH;
            } else if (document.querySelector('.rec-job-list') != null) {
                return JobPageType.RECOMMEND;
            } else if (document.querySelector('.job-recommend .job-list') != null) {
                return JobPageType.MOBILE_RECOMMEND;
            } else if (document.querySelector('#main .job-list') != null) {
                return JobPageType.MOBILE_SEARCH;
            }
            return null;
        }

        fetchJobElements(jobPageType) {
            if (jobPageType === JobPageType.SEARCH) {
                return document.querySelectorAll('ul.job-list-box > li.job-card-wrapper');
            } else if (jobPageType === JobPageType.RECOMMEND) {
                return document.querySelectorAll('ul.rec-job-list > div > li.job-card-box');
            } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
                return document.querySelectorAll('.job-list > ul > li');
            } else {
                throw new Error('Not a job element')
            }
        }

        fetchJobUniqueKey(jobElement, jobPageType) {
            if (jobPageType === JobPageType.SEARCH) {
                const element = jobElement.querySelector('.job-card-left');
                if (element == null) {
                    return null;
                }
                const url = element.href;
                if (url == null) {
                    return null;
                }
                return url.split('/job_detail/')[1].split('.html')[0];
            } else if (jobPageType === JobPageType.RECOMMEND) {
                const element = jobElement.querySelector('.job-name');
                if (element == null) {
                    return null;
                }
                const url = element.href;
                if (url == null) {
                    return null;
                }
                return url.split('/job_detail/')[1].split('.html')[0];
            } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
                const element = jobElement.querySelector('a');
                if (element == null) {
                    return null;
                }
                if (element.classList.contains('delete')) {
                    return null;
                }
                const url = element.href;
                if (url == null) {
                    return null;
                }
                return url.split('/job_detail/')[1].split('.html')[0];
            } else {
                throw new Error('Not a job element')
            }
        }

        async parseSalary(jobElement, jobPageType) {
            if (jobPageType === JobPageType.SEARCH) {
                const salary = jobElement.querySelector('.salary').textContent;
                return parseSalaryToMonthly(salary);
            } else if (jobPageType === JobPageType.RECOMMEND) {
                const salary = this.convertSalaryField(jobElement.querySelector('.job-salary').textContent);
                return parseSalaryToMonthly(salary);
            } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
                const salary = jobElement.querySelector('.salary').textContent;
                return parseSalaryToMonthly(salary);
            } else {
                throw new Error('Not a job element')
            }
        }

        async parseCompanyName(jobElement, jobPageType) {
            if (jobPageType === JobPageType.SEARCH) {
                return jobElement.querySelector('.company-name > a').textContent
            } else if (jobPageType === JobPageType.RECOMMEND) {
                return jobElement.querySelector('.boss-name').textContent
            } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
                return jobElement.querySelector('.company').textContent
            } else {
                throw new Error('Not a job element')
            }
        }

        async parseJobName(jobElement, jobPageType) {
            if (jobPageType === JobPageType.SEARCH) {
                return jobElement.querySelector('.job-name').textContent
            } else if (jobPageType === JobPageType.RECOMMEND) {
                return jobElement.querySelector('.job-name').textContent
            } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
                return jobElement.querySelector('.title-text').textContent
            } else {
                throw new Error('Not a job element')
            }
        }

        async jobIsActive(jobElement, jobPageType) {
            const onlineTag = jobElement.querySelector('.boss-online-tag');
            if (onlineTag != null) {
                return true;
            }
            const onlineIcon = jobElement.querySelector('.boss-online-icon');
            if (onlineIcon != null) {
                return true;
            }

            const details = await this.fetchJobDetails(jobElement, jobPageType);
            return details.active;
        }

        addViewedCallback(jobElement, jobPageType, eventCallback) {
            jobElement.addEventListener('click', eventCallback, true);
        }

        markCurJobElement(jobElement, jobPageType, jobFilterTypes) {
            if (jobPageType === JobPageType.SEARCH || jobPageType === JobPageType.RECOMMEND) {
                const titleElement = jobElement.querySelector('.job-title');
                let markSpan = titleElement.querySelector('.mark');
                if (markSpan === null) {
                    markSpan = document.createElement('span');
                    markSpan.classList.add('mark');
                    markSpan.style.color = 'red';
                    markSpan.style.float = 'left';
                    titleElement.insertBefore(markSpan, titleElement.firstChild);
                }
                markSpan.textContent = '(' + jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|') + ')';
                this.changeJobElementColor(jobElement, jobPageType);

            } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
                const titleElement = jobElement.querySelector('.title');
                let markSpan = titleElement.querySelector('.mark');
                if (markSpan === null) {
                    markSpan = document.createElement('span');
                    markSpan.classList.add('mark');
                    markSpan.style.color = 'red';
                    markSpan.style.float = 'left';
                    titleElement.insertBefore(markSpan, titleElement.firstChild);
                }
                markSpan.textContent = '(' + jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|') + ')';
                this.changeJobElementColor(jobElement, jobPageType);
            } else {
                throw new Error('Not a job element')
            }
        }

        blockCurJobElement(jobElement, jobPageType, jobFilterTypes) {
            const message = jobFilterTypes.map(jobFilterType => this.convertFilterTypeToMessage(jobFilterType)).join('|');
            if (jobPageType === JobPageType.SEARCH) {
                const cardBody = jobElement.querySelector('.job-card-body');
                cardBody.innerHTML = `
                    <div class="job-card-left"></div>
                    <div class="tip" style="color: dimgray; font-weight: bold; font-size: large; padding-top: 20px">已屏蔽</div>
                    <div class="job-card-right"></div>
                    `;
                const cardFooter = jobElement.querySelector('.job-card-footer');
                cardFooter.innerHTML = `
                    <div class="info-desc">${message}</div>
                    `;
                this.changeJobElementColor(jobElement, jobPageType);
            } else if (jobPageType === JobPageType.RECOMMEND) {
                const cardBody = jobElement.querySelector('.job-info');
                cardBody.innerHTML = `
                    <div class="tip" style="color: dimgray; font-weight: bold; font-size: large; padding-top: 20px">已屏蔽</div>
                    `;
                const cardFooter = jobElement.querySelector('.job-card-footer');
                cardFooter.innerHTML = `
                    <div class="info-desc">${message}</div>
                    `;
                this.changeJobElementColor(jobElement, jobPageType);
            } else if (jobPageType === JobPageType.MOBILE_SEARCH || jobPageType === JobPageType.MOBILE_RECOMMEND) {
                const cardBody = jobElement.querySelector('a');
                cardBody.classList.add('delete');
                cardBody.innerHTML = `
                    <div class="tip" style="color: dimgray; font-weight: bold; font-size: large; padding-top: 20px">已屏蔽</div>
                    <span>${message}</span>
                    `;
                this.changeJobElementColor(jobElement, jobPageType);
            } else {
                throw new Error('Not a job element')
            }
        }

        removeCurJobElement(jobElement, jobPageType) {
            jobElement.parentElement.removeChild(jobElement);
            // jobElement.style.display = 'none';
        }

        /**
         *
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         */
        changeJobElementColor(jobElement, jobPageType) {
            if (jobPageType === JobPageType.SEARCH || jobPageType === JobPageType.RECOMMEND || jobPageType === JobPageType.MOBILE_RECOMMEND || jobPageType === JobPageType.MOBILE_SEARCH) {
                jobElement.style.backgroundColor = '#e1e1e1';
            } else {
                throw new Error('Not a job element')
            }
        }

        /**
         *
         * @param {String} salary
         */
        convertSalaryField(salary) {
            // 恶心的BOSS添加了特殊字符,需要转换
            let res = '';
            for (let i = 0; i < salary.length; i++) {
                let charCode = salary.charCodeAt(i);
                if (charCode >= 57393 && charCode <= 57402) {
                    charCode = charCode - 57393 + 48;
                }
                res += String.fromCharCode(charCode);
            }
            return res;
        }

        /**
         * @param {Element} jobElement
         * @param {JobPageType} jobPageType
         * @returns {Promise<{active: boolean}>}
         */
        async fetchJobDetails(jobElement, jobPageType) {
            let active = false;

            if(jobPageType === JobPageType.SEARCH){
                const url = `https://www.zhipin.com/wapi/zpgeek/job/card.json?${jobElement.querySelector(".job-card-left").href.split("?")[1]}`;
                const response = await fetch(url);
                const json = await response.json();
                if(json.code === 0){
                    const card = json.zpData.jobCard;
                    const activeTimeDesc = card.activeTimeDesc;
                    if(card.online || (activeTimeDesc && (activeTimeDesc.includes('刚刚') || activeTimeDesc.includes('日') || activeTimeDesc.includes('本周')))){
                        active = true;
                    } else {
                        console.log("活跃状态:" + activeTimeDesc);
                    }
                }
            }

            return {
                "active": active,
            }
        }



    }

    // setTimeout(handleScheduleJob, 1000);
    setInterval(handleScheduleJob, 1000);

    async function handleScheduleJob() {
        if (lock) {
            return;
        }
        const strategy = getStrategy();
        if (strategy == null) {
            return;
        }
        let jobPageType = strategy.fetchJobPageType();
        if (jobPageType == null) {
            return;
        }
        lock = true;
        try {
            await handleElements(strategy, jobPageType);
        } catch (e) {
            console.error('处理元素失败:', e);
        } finally {
            lock = false;
        }
    }

    /**
     *
     * @param {PlatFormStrategy}strategy
     * @param {JobPageType} jobPageType
     * @returns {Promise<void>}
     */
    async function handleElements(strategy, jobPageType) {
        if (getPageHash(strategy, jobPageType) === curPageHash) {
            return;
        }
        const elements = strategy.fetchJobElements(jobPageType);


        let validElements = fetchValidElements(strategy, jobPageType, elements);

        const platForm = choosePlatForm();
        const viewedJobIds = getJobViewedSet(platForm);

        // 添加点击事件
        validElements.forEach(value => {
            initEventHandler(platForm, strategy, jobPageType, value.element);
        })

        const filterPromises = validElements.map(value => {
            return filterElementTask(strategy, value.element, jobPageType, viewedJobIds);
        });
        await Promise.all(filterPromises);

        curPageHash = getPageHash(strategy, jobPageType);
    }

    /**
     *
     * @param {PlatFormStrategy} strategy
     * @param {Element} jobElement
     * @param {JobPageType} jobPageType
     * @param {Set<String>} viewedJobIds
     * @returns {Promise<void>}
     */
    async function filterElementTask(strategy, jobElement, jobPageType, viewedJobIds) {
        const jobInfo = await fetchJobInfo(strategy, jobElement, jobPageType).then(value => {
            console.log(`Id:${value.id},公司:${value.companyName},岗位:${value.jobName},月薪: ${value.salary.min} - ${value.salary.max}`);
            return value;
        });
        const filterTypes = getJobFilterType(jobInfo, viewedJobIds);
        handleFilterElement(strategy, jobElement, jobPageType, filterTypes);
    }

    /**
     *
     * @param {PlatFormStrategy} strategy
     * @param {Element} job
     * @param {JobPageType} pageType
     * @returns {Promise<{ id:string, jobName: string, companyName: string, salary: {min: number, max: number}, isActive: boolean}>}
     */
    async function fetchJobInfo(strategy, job, pageType) {
        try {
            const [id, jobName, companyName, salary, isActive] = await Promise.all([
                strategy.fetchJobUniqueKey(job, pageType),
                strategy.parseJobName(job, pageType),
                strategy.parseCompanyName(job, pageType),
                strategy.parseSalary(job, pageType),
                strategy.jobIsActive(job, pageType)
            ]);

            return {id, jobName, companyName, salary, isActive};
        } catch (error) {
            throw error;
        }
    }

    /**
     * @param {{ id:string, jobName: string, companyName: string, salary: {min: number, max: number}, isActive: boolean}} jobInfo
     * @param {Set<String>} viewedJobs
     * @returns {JobFilterType[]}
     */
    function getJobFilterType(jobInfo, viewedJobs) {
        const filterTypes = new Set();
        const companyName = jobInfo.companyName.toLowerCase();
        for (let i = 0; i < settings.blacklist.length; i++) {
            if (companyName.includes(settings.blacklist[i])) {
                filterTypes.add(JobFilterType.BLACKLIST);
                break
            }
        }
        if (viewedJobs.has(jobInfo.id)) {
            filterTypes.add(JobFilterType.VIEWED);
        }

        const companySalary = jobInfo.salary;
        if (settings.salaryFilterType === 'include') {
            if (companySalary.min < settings.minSalary || companySalary.max > settings.maxSalary) {
                filterTypes.add(JobFilterType.MISMATCH_CONDITION);
            }
        } else if (settings.salaryFilterType === 'overlap') {
            if (!(companySalary.max >= settings.minSalary && settings.maxSalary >= companySalary.min)) {
                filterTypes.add(JobFilterType.MISMATCH_CONDITION);
            }
        }

        if (settings.filterInactiveJob && !jobInfo.isActive) {
            filterTypes.add(JobFilterType.MISMATCH_CONDITION);
        }
        return [...filterTypes];
    }

    /**
     *
     * @param {PlatFormStrategy} strategy
     * @param {Element} jobElement
     * @param {JobPageType} jobPageType
     * @param {JobFilterType[]} jobFilterTypes
     */
    function handleFilterElement(strategy, jobElement, jobPageType, jobFilterTypes) {
        if (jobFilterTypes.length === 0) {
            return;
        }
        // 过滤掉NoOp的过滤类型,不然后面会拼接出提示
        let actionFilterTypes = [];
        let filter = jobFilterTypes.map(filterType => {
            let action = null;
            if (filterType === JobFilterType.BLACKLIST) {
                action = settings.blacklistAction;
            } else if (filterType === JobFilterType.VIEWED) {
                action = settings.viewedAction;
            } else if (filterType === JobFilterType.MISMATCH_CONDITION) {
                action = settings.conditionAction;
            }
            if(action !== 'noop'){
                actionFilterTypes.push(filterType);
            }
            return action;
        }).filter(action => action != null && typeof action === 'string');
        if (filter.includes('delete')) {
            strategy.removeCurJobElement(jobElement, jobPageType);
        } else if (filter.includes('hide')) {
            strategy.blockCurJobElement(jobElement, jobPageType, actionFilterTypes);
        } else if (filter.includes('mark')) {
            strategy.markCurJobElement(jobElement, jobPageType, actionFilterTypes);
        }
    }

    /**
     *
     * @param {PlatFormStrategy} strategy
     * @param {JobPageType} jobPageType
     * @param {NodeListOf<Element>} elements
     * @returns {[{id: string, element: Element}]}
     */
    function fetchValidElements(strategy, jobPageType, elements) {
        const validElements = [];
        for (let i = 0; i < elements.length; i++) {
            const id = strategy.fetchJobUniqueKey(elements[i], jobPageType);
            if (id === null) {
                continue;
            }
            validElements.push({'id': id, element: elements[i]});
        }
        return validElements;
    }

    /**
     *
     * @param {PlatFormStrategy} strategy
     * @param {JobPageType} jobPageType
     * @returns {number}
     */
    function getPageHash(strategy, jobPageType) {
        const elements = strategy.fetchJobElements(jobPageType)
        const keys = fetchValidElements(strategy, jobPageType, elements).map(t => t.id).sort().join();
        return hashCode(keys);
    }

    /**
     *@param {Symbol} jobPlatform
     * @param {PlatFormStrategy} strategy
     * @param {JobPageType} jobPageType
     * @param {Element} element
     */
    function initEventHandler(jobPlatform, strategy, jobPageType, element) {
        const callBack = () => {
            const id = strategy.fetchJobUniqueKey(element, jobPageType);
            setJobViewed(jobPlatform, id);
            // 重置PageHash,来刷新
            curPageHash = null;
        };
        strategy.addViewedCallback(element, jobPageType, callBack);
    }

    /**
     *
     * @param {String} str
     * @returns {number}
     */
    function hashCode(str) {
        let hash = 0;
        if (str.length === 0) return hash;
        for (let i = 0; i < str.length; i++) {
            hash = (hash << 5) - hash + str.charCodeAt(i);
            hash |= 0;
        }
        return hash;
    }

    const CacheManager = {
        /**
         * 设置缓存(ttl 为可选参数,单位毫秒)
         * @param {String} key
         * @param value
         * @param {Number} ttl
         */
        set: function (key, value, ttl) {
            const data = {value: value};
            if (typeof ttl === 'number') {
                data.expires = Date.now() + ttl;
            }
            localStorage.setItem(key, JSON.stringify(data));
        },

        /**
         * 获取缓存(自动处理过期)
         * @param {String} key
         * @returns {*|null}
         */
        get: function (key) {
            const item = localStorage.getItem(key);
            if (!item) return null;

            try {
                const data = JSON.parse(item);
                // 检查过期时间(如果存在)
                if (data.expires && Date.now() > data.expires) {
                    this.delete(key);
                    return null;
                }
                return data.value;
            } catch (e) {
                console.error('缓存解析失败:', e);
                this.delete(key);
                return null;
            }
        },

        /**
         * 删除指定缓存
         * @param {String} key
         */
        delete: function (key) {
            localStorage.removeItem(key);
        },

        /**
         * 清除所有带过期时间的缓存
         */
        cleanExpired: function () {
            for (let i = localStorage.length - 1; i >= 0; i--) {
                const key = localStorage.key(i);
                this.get(key); // 自动触发过期检查
            }
        }
    };

})();

QingJ © 2025

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