// ==UserScript==
// @name BOSS直聘 · 职位正文自由复制
// @namespace local.boss-job-copy
// @version 1.0.0
// @description 恢复职位描述的选择、复制和右键菜单,并提供一键复制正文按钮。
// @match https://www.zhipin.com/*
// @run-at document-start
// @grant GM_setClipboard
// @noframes
// ==/UserScript==
(() => {
'use strict';
// 已核对的职位描述节点。整个站点匹配用于兼容站内跳转,
// 只有找到可见正文时才显示按钮。
const BODY_SELECTOR = '.job-detail-section .job-sec-text';
const PREFIX = 'tm-boss-job-copy';
const style = document.createElement('style');
style.id = `${PREFIX}-style`;
style.textContent = `
.job-detail-section, .job-detail-section * {
-webkit-user-select: text !important;
user-select: text !important;
}
#${PREFIX} {
position: fixed !important;
right: 24px !important;
bottom: 24px !important;
z-index: 2147483647 !important;
max-width: min(320px, calc(100vw - 48px));
font: 14px/1.5 system-ui, sans-serif;
text-align: right;
}
#${PREFIX}[hidden] { display: none !important; }
#${PREFIX} button {
appearance: none;
border: 0;
border-radius: 8px;
padding: 10px 16px;
color: #fff;
background: #087f83;
box-shadow: 0 3px 12px #0003;
font: inherit;
cursor: pointer;
}
#${PREFIX} button:hover { background: #06666a; }
#${PREFIX} button:focus-visible { outline: 3px solid #50c9cc; outline-offset: 3px; }
#${PREFIX} button:disabled { cursor: wait; opacity: .7; }
#${PREFIX} [role="status"]:not(:empty) {
margin-top: 6px;
padding: 6px 10px;
border-radius: 6px;
background: #fff;
color: #263238;
box-shadow: 0 2px 10px #0002;
}
`;
const panel = document.createElement('div');
panel.id = PREFIX;
panel.hidden = true;
const button = document.createElement('button');
button.type = 'button';
button.textContent = '复制职位描述';
const status = document.createElement('div');
status.setAttribute('role', 'status');
status.setAttribute('aria-live', 'polite');
panel.append(button, status);
function elementOf(node) {
return node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
}
function isEditable(node) {
const element = elementOf(node);
return !!element && (element.isContentEditable || !!element.closest('input, textarea, select'));
}
function bodyOf(node) {
return elementOf(node)?.closest(BODY_SELECTOR) || null;
}
// 保留浏览器默认行为,只阻止正文事件继续传给网页处理器。
// 不拦截点击、输入、剪切等其他操作。
function protectTextEvent(event) {
if (isEditable(event.target) || isEditable(document.activeElement)) return;
if (event.type === 'copy') {
const selection = window.getSelection();
if (!selection || selection.isCollapsed || isEditable(selection.anchorNode)) return;
const body = bodyOf(selection.anchorNode);
if (!body || body !== bodyOf(selection.focusNode)) return;
} else if (!bodyOf(event.target)) {
return;
}
event.stopImmediatePropagation();
}
for (const type of ['copy', 'selectstart', 'contextmenu']) {
window.addEventListener(type, protectTextEvent, true);
}
function visibleBodies() {
return [...document.querySelectorAll(BODY_SELECTOR)].filter(element => {
if (!element.isConnected || !element.getClientRects().length) return false;
const computed = getComputedStyle(element);
if (computed.visibility === 'hidden' || computed.visibility === 'collapse') return false;
for (let parent = element; parent; parent = parent.parentElement) {
if (getComputedStyle(parent).opacity === '0') return false;
}
return true;
});
}
function cleanText(text) {
return text
// 只规范化康熙部首兼容字,例如"⽑、⼒";保留全角标点。
.replace(/[\u2F00-\u2FDF]/g, character => character.normalize('NFKC'))
.replace(/\u00a0/g, ' ')
.replace(/[\u200b\ufeff]/g, '')
.replace(/[ \t]+\n/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
let statusTimer;
function showStatus(message) {
clearTimeout(statusTimer);
status.textContent = message;
statusTimer = setTimeout(() => { status.textContent = ''; }, 6000);
}
button.addEventListener('click', () => {
// 点击时重新查询,避免切换职位后复制旧正文。
const texts = visibleBodies().map(element => cleanText(element.innerText)).filter(Boolean);
const text = [...new Set(texts)].join('\n\n');
if (!text) {
showStatus('未找到可见的职位描述,请打开职位详情。');
return;
}
const partial = /登录查看完整内容/.test(text);
button.disabled = true;
let settled = false;
const finish = message => {
if (settled) return;
settled = true;
clearTimeout(watchdog);
button.disabled = false;
showStatus(message);
};
const watchdog = setTimeout(() => {
finish('复制未确认,请重试或直接选择正文复制。');
}, 5000);
try {
GM_setClipboard(text, 'text', () => {
finish(partial
? '已复制当前内容;登录后可查看并复制完整描述。'
: '职位描述已复制,可直接粘贴。');
});
} catch (error) {
finish('复制失败,请检查脚本是否已在油猴中启用。');
console.error('[BOSS 职位复制]', error);
}
});
function update() {
if (!document.documentElement) return;
if (!style.isConnected) (document.head || document.documentElement).append(style);
if (!document.body) return;
if (!panel.isConnected) document.body.append(panel);
const hidden = visibleBodies().length === 0;
if (panel.hidden !== hidden) panel.hidden = hidden;
}
let scheduled = false;
const observer = new MutationObserver(records => {
// 忽略自己的状态提示,避免观察器反复触发自身更新。
if (records.every(record => panel.contains(record.target) || style.contains(record.target))) return;
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
update();
});
});
observer.observe(document, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'style', 'hidden']
});
window.addEventListener('pageshow', update);
update();
})();
【油猴脚本】Boss直聘 岗位职责复制
麦麦麦当劳大王2026-11-05 23:55
相关推荐
starzy199021 分钟前
深入理解Scala Map:从实现原理到工程实践小lu飞1 小时前
2026小红书小工具怎么制作:纯前端 ZIP 打包、沙箱限制与笔记挂载流程只睡四小时1 小时前
Three.js Sub-tick:漏判 60.7%→0smallcelebration1 小时前
151 从0开始搭建后端项目wangruofeng1 小时前
手机和电脑传文件,除了微信还有 LocalSend码农小河662 小时前
基于SSM+JSP的高校专业信息管理系统【附源码+数据库】excel2 小时前
基于 GPT 的自动发布文章接口方案:从一句话或一张图片生成并发布内容东鹏特饮4 小时前
系统运维(传统/通用运维)面试题其实防守也摸鱼4 小时前
个人学习 Pwn 思路:从基础到漏洞利用的进阶路径