// ==UserScript== // @name 期刊评级查询 // @version 1.3 // @description 网页期刊查询小工具,支持划词和快捷键调用了 // @author ZhaoXi // @match *://*/* // @include * // @grant GM.xmlHttpRequest // @grant GM_getValue // @connect wechat6d.fenqubiao.com // ==/UserScript== /* ==UserConfig== group: configA: title: 快捷键配置(选择是否开启快捷键) description: 通过快捷键唤起查询 type: select default: '0 关闭快捷键(默认)' values: ['0 关闭快捷键(默认)','1 仅使用快捷键','2 同时使用按钮和快捷键'] configB: title: 选择快捷键(需要先开启快捷键) description: 需要先开启快捷键 type: select default: 'Alt + J' values: ['Alt + J','Alt + L','Ctrl + Q','其他快捷键请按格式添加'] ==/UserConfig== */ /* 代码规范: Python移植方便 保证线性结构 分开网络请求和DOM操作 */ console.log('》》》 期刊评级程序已启用!😋 开始愉快的学习吧(doge)') // 封装原函数,实现请求顺序执行 function gmFetch(url, options = {}) { return new Promise((resolve, reject) => { // 解构并排除 onload/onerror,防止覆盖 const { onload, onerror, ...otherOptions } = options; GM.xmlHttpRequest({ url, method: options.method || 'GET', headers: options.headers || {}, data: options.body, onload: (res) => { resolve(res); }, onerror: (err) => { reject(err); }, ...otherOptions }); }); } (function () { 'use strict'; // 全局属性 let Year = '2025' let Hidden_time = 2000 // 按钮自动隐藏时间,默认2000ms // 获取配置信息 let ShortcutEnabled = Number(GM_getValue('group.configA').split(' ')[0]) let ShortcutKeys = null if (ShortcutEnabled > 0) { let shortcutStr = GM_getValue('group.configB') if (shortcutStr.includes('+')) { ShortcutKeys = parseShortcut(shortcutStr) console.log(ShortcutKeys) } else { ShortcutEnabled = 0 } } function parseShortcut(shortcutStr) { const parts = shortcutStr.split('+').map(part => part.trim()); let altKey = false; let ctrlKey = false; let key = ''; for (const part of parts) { if (part.toLowerCase() === 'alt') { altKey = true; } else if (part.toLowerCase() === 'ctrl') { ctrlKey = true; } else { // 假设最后一个非修饰键的部分是实际按键 key = part.toUpperCase(); // 转换为大写便于比较 } } return { altKey, ctrlKey, key }; } // API实现 const Api = { home_url: "https://wechat6d.fenqubiao.com", headers: { "Host": "wechat6d.fenqubiao.com", "Content-Type": "application/json", }, msg_codes: ['成功', '请求出错!请重试', '未检索到该期刊:', '未知错误'], msg_box: function (code, variable = null) { let msg = Api.msg_codes[code] return { code, msg, variable } }, async search(keyword) { const suffix = "/api/journal/search" let params = { "keyword": keyword, "year": Year, "top": "15", "code": "chenfuyou" } const url_params = new URLSearchParams(params) const url = `${Api.home_url}${suffix}?${url_params}` console.log('query_url=', url) const response = await gmFetch(url, { method: 'GET', headers: Api.headers }) if (response.status < 200 || response.status >= 300) { return Api.msg_box(1) } try { // 这里直接粗暴try判断是否拿到期刊id if (!response.responseText) return Api.msg_box(2, { keyword }) const journal_id = JSON.parse(response.responseText)[0]['id'] return Api.msg_box(0, journal_id) // get_detail(journal_id) } catch (e) { return Api.msg_box(2, keyword) } }, async get_detail(id1) { const suffix = '/api/journal/get' let params = { "year": Year, "id": id1, "code": "chenfuyou" } const url_params = new URLSearchParams(params) const url = `${Api.home_url}${suffix}?${url_params}` const response = await gmFetch(url, { method: 'GET', headers: Api.headers, }) if (response.status < 200 || response.status >= 300) { return Api.msg_box(1) } try { const result = JSON.parse(response.responseText); if (!result) { return Api.msg_box(3, id1) } else { return Api.msg_box(0, result) } } catch (e) { return Api.msg_box(2) } }, format_output(result) { const title = result['title'] const ISSN = result['issn'] const zky = result['zky'][0] const jcr = result['jcr'] console.log("期刊名称:" + title.toLowerCase()) console.log("ISSN:" + ISSN) console.log("大类及分区:" + zky['name'] + zky['class'] + '区') console.log("小类及分区:") for (let i in jcr) { console.log(`${jcr[i]['name'].toLowerCase()} ${jcr[i]['class']}区`) } return 0 }, async main(keyword) { let m_box = await Api.search(keyword) console.log('搜索结果', m_box) if (m_box.code !== 0) { return m_box } const id1 = m_box.variable m_box = await Api.get_detail(id1) console.log('查询结果', m_box) if (m_box.code === 0) { Api.format_output(m_box.variable) } return m_box } } // DOM操作 const UI = { floatButton: null, isQueryActive: false, // 标记是否正在查询或已出结果,避免按钮无法移除 autoHideTimer: null, // 自动隐藏计时器 createFloatButton(rect) { if (this.floatButton) return; // 防重复 this.floatButton = document.createElement('button'); this.floatButton.id = 'journal-float-button' this.floatButton.textContent = '查评级'; // 在创建按钮的style中添加: this.floatButton.style.cssText = ` position: absolute; width: 60px; /* 固定宽度 */ height: 30px; /* 固定高度 */ line-height: 30px; /* 文字垂直居中 */ text-align: center; /* 文字水平居中 */ background: #0078d4; color: white; border: none; padding: 0 10px; /* 去掉上下内边距,仅保留左右 */ font-size: 12px; border-radius: 4px; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.2); z-index: 9999; transform: translateY(-8px); /* 强制继承基础样式,避免页面干扰 */ font-family: sans-serif; margin: 0; `; this.floatButton.addEventListener('click', async () => { // 清除自动隐藏计时器 clearTimeout(this.autoHideTimer); const text = window.getSelection().toString().trim(); if (text) { this.isQueryActive = true console.log('查询中:', text) let m_box = await Api.main(text) this.showResultPanel(m_box) } this.removeFloatButton(); }); document.body.appendChild(this.floatButton); this.positionButton(rect); this.resetAutoHideTimer(); }, positionButton(rect) { this.floatButton.style.top = (rect.top + window.scrollY + 33) + 'px'; this.floatButton.style.left = (rect.left + window.scrollX + rect.width / 2 - 30) + 'px'; }, removeFloatButton() { // 清除计时器 if (this.autoHideTimer) { clearTimeout(this.autoHideTimer); this.autoHideTimer = null; } if (this.floatButton) { this.floatButton.remove(); this.floatButton = null; } }, resetAutoHideTimer() { // 清除现有计时器 if (this.autoHideTimer) { clearTimeout(this.autoHideTimer); } // 设置新计时器 this.autoHideTimer = setTimeout(() => { if (this.floatButton && !this.isQueryActive) { this.removeFloatButton(); } }, Hidden_time); }, showResultPanel(m_box) { // 如果已有面板,先移除 const existing = document.getElementById('journal-result-panel'); if (existing) existing.remove(); const panel = document.createElement('div'); panel.id = 'journal-result-panel'; panel.style.cssText = ` position: absolute; top: 40px; left: 0; background: white; color: #333; font-family: Consolas, monospace, sans-serif; font-size: 13px; line-height: 1.6; border: 1px solid #ccc; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); padding: 12px; width: auto; min-width: 150px; max-height: 400px; overflow-y: auto; z-index: 99999; cursor: default; `; // 消息盒子code!=0 时改为显示报错信息 if (m_box.code === 0) { let result = m_box.variable const title = result['title']; const ISSN = result['issn']; const zky = result['zky'][0]; const jcr = result['jcr']; // 构建内容 // 大类分区颜色映射(1区红色,2区橙色,3-4区灰色) const getClassColor = (classNum) => { if (classNum === 1) { return 'color: #f2c01d; font-weight: bold;'; // 金色加粗(1区) } else if (classNum === 2) { return 'color: #944aec;'; // 紫色(2区) } else { return 'color: #7c430b;'; // 铜色(3,4区) } }; panel.innerHTML = `
期刊名称:${title.toLowerCase()}
ISSN:${ISSN}
大类及分区: ${zky['name']} ${zky['class']}区
小类及分区:
${jcr.map(item => `${item['name'].toLowerCase()} ${item['class']}区` ).join('
')}
`; } else { let msg = m_box.msg let variable = m_box.variable panel.innerHTML = `
${msg} ${variable}
` } // 添加到页面 document.body.appendChild(panel); // 定位:放在选区正下方 let default_height = 150 let default_width = 250 const rect = window.getSelection().getRangeAt(0).getBoundingClientRect() let top = rect.bottom + window.scrollY + 5; let left = rect.left + window.scrollX - 15; // 防止面板超出右侧屏幕 if (left + default_width > window.innerWidth + window.scrollX) { left = window.innerWidth + window.scrollX - default_width - 10; } if (top + default_height > window.innerHeight + window.scrollY) { top = rect.top + window.scrollY - default_height - 5 } panel.style.top = top + 'px'; panel.style.left = left + 'px'; // === 修改:同时支持鼠标和触摸点击关闭 === setTimeout(() => { function closePanel(e) { if (!panel.contains(e.target)) { panel.remove(); document.removeEventListener('click', closePanel); document.removeEventListener('touchstart', closePanel); UI.isQueryActive = false; } } document.addEventListener('click', closePanel); document.addEventListener('touchstart', closePanel); }, 100); }, } // 为1表示不使用按钮 if (ShortcutEnabled !== 1) { console.log("---按钮查询已开启!") // 划词事件监听器 (兼容 PC 和 移动端) function handleSelectionEnd(event) { // 对于 touchend 事件,阻止默认的长按弹窗(可选) // if (event.type === 'touchend') { event.preventDefault(); } setTimeout(() => { if (UI.isQueryActive) return; const selection = window.getSelection(); const text = selection.toString().trim(); if (!text || selection.rangeCount === 0) return; // 清除旧按钮 UI.removeFloatButton(); const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); // 过滤大面积选择(防误触) if (rect.height < 60 && rect.width > 0) { UI.createFloatButton(rect); } }, 50); // 延迟确保 selection 稳定 } // 同时监听鼠标释放和触摸结束事件 document.addEventListener('mouseup', handleSelectionEnd); document.addEventListener('touchend', handleSelectionEnd, { passive: true }); // 点击页面其他地方,关闭按钮 document.addEventListener('click', (e) => { if (UI.floatButton && e.target !== UI.floatButton) { UI.removeFloatButton(); } }) // 鼠标悬停在按钮上时重置计时器,避免误隐藏 document.addEventListener('mouseover', (e) => { if (e.target === UI.floatButton) { UI.resetAutoHideTimer(); } }) } if (ShortcutEnabled != 0) { console.log('---快捷键查询已开启!') function isShortcutMatch(event, shortcutConfig) { const eventKey = event.key.toUpperCase(); return ( event.altKey === shortcutConfig.altKey && event.ctrlKey === shortcutConfig.ctrlKey && eventKey === shortcutConfig.key && !event.repeat // 防止重复触发 ); } async function onShortcutPressed() { const text = window.getSelection().toString().trim(); // 限制最大查询长度为50 if (text && text.length < 50) { console.log('查询中:', text) let m_box = await Api.main(text) UI.showResultPanel(m_box) } } document.addEventListener('keydown', async function handleKeyDown(event) { if (isShortcutMatch(event, ShortcutKeys)) { event.preventDefault(); // 阻止默认行为(可选) await onShortcutPressed(); } }) } })();