diff --git a/plugins/micro-tools/README.md b/plugins/micro-tools/README.md index 75d08ddd9..75691ad39 100644 --- a/plugins/micro-tools/README.md +++ b/plugins/micro-tools/README.md @@ -6,14 +6,15 @@ ``` micro-tools/ -├── plugin.json # 插件入口 + feature 触发配置 -├── preload.js # CommonJS:读 xlsx,暴露 window.journalApi -├── index.html # 前端界面(多功能 SPA) -├── index.css # 样式 -├── index.js # 前端逻辑 -├── 2026年度JCR期刊名单(完整版).xlsx # 期刊数据源(22,643 条,与插件一起发布) -├── vendor/xlsx.full.min.js # SheetJS 单文件发行版 -├── logo.png # 插件图标 +├── plugin.json # 插件入口 + feature 触发配置 +├── preload.js # CommonJS:读两个数据集,合并后暴露 window.journalApi +├── index.html # 前端界面(多功能 SPA) +├── index.css # 样式 +├── index.js # 前端逻辑 +├── journals.json.gz # JCR 2026 期刊数据(22,643 条,gzip -9) +├── ccf.json.gz # CCF 第七版目录(677 条,gzip -9) +├── pack.sh # 打包为 .zpx(ASAR + brotli) +├── logo.png / logo.svg # 插件图标 └── README.md ``` @@ -21,44 +22,63 @@ micro-tools/ | 功能 | 状态 | 说明 | | --- | --- | --- | -| **期刊查询** | ✅ 可用 | 基于 JCR 2026 名单查询影响因子(JIF)、分区、JCI、被引等 | -| **数据库查询** | 🔧 体验版 | NCBI Genome / Gene / Nucleotide、PubMed、UniProt、PDB、Ensembl、Google Scholar 一键检索 | +| **期刊 · 会议查询** | ✅ 可用 | 同一入口检索 JCR 2026 期刊与 CCF 第七版目录 | +| **数据库查询** | 🔧 体验版 | NCBI / PubMed / UniProt / PDB / Ensembl 等一键检索 | | 文献速读 | 🚧 即将推出 | PDF 摘要提炼 | | 单位换算 | 🚧 即将推出 | 科研常用单位 | -### 期刊查询 +### 期刊 · 会议查询 -- 按 **期刊名 / JCR 缩写 / ISSN** 模糊检索(220ms 防抖),结果按 **JIF 高→低排序**。 -- 卡片展示:**2025 JIF 徽标、JCR 分区 Q1–Q4 配色、JCI、总被引、金色 OA%、各学科分区详情、出版商、学科、ISSN**。 -- 「复制信息」经 `ztools.copyText()` + `showNotification()`。 +一个入口同时检索两套体系,卡片上**有就显示、没有就不显示**: + +| 数据 | 来源 | 卡片显示 | +| --- | --- | --- | +| 影响因子 / 分区 / JCI / 被引 | JCR 2026(22,643 条) | `IF 20.4`、`Q1`、四项统计 | +| CCF 等级 / 领域 / 变动 | CCF 第七版(677 条) | `CCF A`、`人工智能`、`晋级` | + +- **三种情况都覆盖**:既有 IF 又有 CCF 等级(如 TPAMI:IF 20.4 + CCF A);只有 CCF 等级(会议,如 CVPR;以及已被 JCR 除名的期刊,如 MTA);只有 IF(非计算机领域期刊,如 Nature)。 +- 检索字段:期刊/会议全称、JCR 缩写、**CCF 缩写**、ISSN、CCF 中文领域(可直接搜「人工智能」)。 +- 顶部筛选条:**全部 / 期刊 / 会议**。 +- 动作按钮:复制信息(纯文本多行)、JCR 官网、官方主页/DBLP。 +- 相关性排序:精确 > 前缀 > 包含,同分按影响因子降序。 ### 数据库查询 -- 输入检索词 → 选择数据库卡片 → 在系统浏览器中打开对应检索结果页(通过 `ztools.shellOpenExternal` 或回退到 `window.open`)。 -- 支持数据库:NCBI Genome / NCBI Gene / NCBI Nucleotide / PubMed / UniProt / PDB / Ensembl / Google Scholar。 +输入检索词 → 选择数据库 → 系统浏览器打开检索结果(`ztools.shellOpenExternal`)。 +支持:NCBI Genome / Gene / Nucleotide、PubMed、UniProt、PDB、Ensembl、Google Scholar。 -## 为什么是“纯 Preload”格式 +## 数据合并机制 -- `preload.js` 用 Node `fs` 读同目录 xlsx,经 SheetJS 解析后暴露给前端;前端只渲染 UI。 -- 不依赖 Vite/Webpack 等构建工具,所有资源用相对路径引用(`index.css`/`index.js`/`logo.png`),因此 ZTools 用 `file://` 加载 `index.html` 时所有资源正常解析。 -- 这规避了 Vite 构建产物用绝对 `/assets/...` 路径在插件环境下加载失败的问题。 +`preload.js` 加载时把 CCF 目录按 ISSN 关联到 JCR 期刊: + +- 关联成功(277/291 条 CCF 期刊,95.2%)→ CCF 字段挂到该 JCR 记录上,并把 CCF 缩写并入检索词表,**一张卡同时显示两套指标**。 +- 未关联(386 条会议 + 14 条未被 JCR 收录的期刊)→ 作为独立记录并入统一检索数组,只显示 CCF 等级。 + +CCF 中跨领域重复收录的刊物(如 TOMM 同时挂「网络」和「图形学」)会合并为一条,领域显示为 `网络 · 图形学`。 + +## 数据源 + +- **JCR**:`2026年度JCR期刊名单(完整版).xlsx` → 转换压缩为 `journals.json.gz`(短键名 + gzip -9,1.6 MB)。22,643 条,含 2025 JIF / 分区 / JCI 等字段。 +- **CCF**:《中国计算机学会推荐国际学术会议和期刊目录》第七版(2026 年 3 月发布)→ `ccf.json.gz`(19.9 KB)。677 条 = 291 期刊 + 386 会议,分 A/B/C 三级、10 个领域方向。 +- 两个数据集均为静态快照,更新时替换对应 `.gz` 文件即可(`pack.sh` 会自动打包)。 ## 在 ZTools 中使用 -1. ZTools → 插件管理 → 添加本地插件,选择 `micro-tools/` 目录。 +1. 插件管理 → 添加本地插件,选择 `micro-tools/` 目录(或导入 `micro-tools-vX.Y.Z.zpx`)。 2. 搜索框输入: - - `科研小盒` / `kydh` — 打开工具箱首页 - - `期刊 Nature` / `影响因子 JACS` / `journal nat rev mol cell bio` — 直接进入期刊查询 - - `数据库 BRCA1` / `ncbi genome TP53` — 进入数据库查询并带入关键词 -3. 或直接打开插件,从首页点卡片进入各功能。 + - `科研小盒` / `kyxh` — 打开工具箱首页 + - `期刊 Nature` / `影响因子 JACS` — 进入期刊·会议查询 + - `ccf CVPR` / `会议 NeurIPS` — 直接进入并切到「会议」筛选 + - `数据库 BRCA1` — 进入数据库查询 +3. 划词后通过 `type: over` 触发「查询期刊/会议」,自动带入选中文本。 -## 数据源 +## 打包 -- 影响因子来自 `2026年度JCR期刊名单(完整版).xlsx`(2025 JIF / JCR 分区 / JCI 等 33 个字段,22,643 条记录)。 -- 更新数据只需替换同目录下的 xlsx 文件,列名保持一致即可自动适配(见 preload.js 中 COLUMN_MAP)。 +```bash +bash pack.sh # 产出 ../micro-tools-.zpx(ASAR + brotli,自动排除 .git/.DS_Store) +``` ## 开发者 - **开发者**:Asa12138 - **邮箱**:bfzede@gmail.com - diff --git a/plugins/micro-tools/ccf.json.gz b/plugins/micro-tools/ccf.json.gz new file mode 100644 index 000000000..d8c527d19 Binary files /dev/null and b/plugins/micro-tools/ccf.json.gz differ diff --git a/plugins/micro-tools/index.css b/plugins/micro-tools/index.css index 8828d292e..3850addd3 100644 --- a/plugins/micro-tools/index.css +++ b/plugins/micro-tools/index.css @@ -13,6 +13,9 @@ --q3: #e0a73a; --q4: #d4685a; --qna: #9aa0b0; + --ccf-a: #6d28d9; + --ccf-b: #1e88ff; + --ccf-c: #8a90a2; } * { box-sizing: border-box; } @@ -207,11 +210,19 @@ html, body { display: flex; flex-direction: column; align-items: flex-end; - gap: 4px; + gap: 5px; flex-shrink: 0; /* 不被压缩 */ margin-left: auto; /* 始终靠右 */ } +.badge-row { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 5px; + flex-wrap: wrap; +} + .badge { flex: 0 0 auto; padding: 3px 9px; @@ -235,6 +246,22 @@ html, body { .badge--q4 { background: rgba(212,104,90,0.14); color: var(--q4); } .badge--qna { background: #eef0f4; color: var(--qna); } +/* CCF 等级徽标:用紫/蓝/灰三档,与 Q 分区在色系上区分开 */ +.badge--ccf-a { background: rgba(109,40,217,0.12); color: var(--ccf-a); } +.badge--ccf-b { background: rgba(30,136,255,0.12); color: var(--ccf-b); } +.badge--ccf-c { background: #eef0f4; color: var(--ccf-c); } +/* CCF 领域 / 变动备注:弱化,避免抢焦点 */ +.badge--cat { + background: var(--teal-soft); + color: var(--teal); + font-weight: 500; +} +.badge--note { + background: rgba(224,167,58,0.16); + color: #a6791b; + font-weight: 500; +} + .card__meta { margin-top: 6px; font-size: 12.5px; @@ -279,6 +306,14 @@ html, body { color: var(--muted); } +.card__ccf { + margin-top: 6px; + font-size: 11.5px; + color: var(--teal); + line-height: 1.5; + word-break: break-word; +} + .card__cat { margin-top: 8px; font-size: 11.5px; @@ -287,6 +322,32 @@ html, body { word-break: break-word; } +/* ---------------- 类型筛选条 ---------------- */ +.chips { + margin-top: 10px; + display: flex; + gap: 8px; +} + +.chip { + border: 1px solid var(--line); + background: var(--card); + color: var(--muted); + font-size: 12.5px; + padding: 5px 14px; + border-radius: 999px; + cursor: pointer; + transition: background .15s, color .15s, border-color .15s; +} + +.chip:hover { border-color: #cfd6e4; } + +.chip.is-on { + background: var(--brand); + border-color: var(--brand); + color: #fff; +} + .card__actions { margin-top: 10px; display: flex; gap: 8px; } .btn { diff --git a/plugins/micro-tools/index.html b/plugins/micro-tools/index.html index bb7afd4ba..7c27399c6 100644 --- a/plugins/micro-tools/index.html +++ b/plugins/micro-tools/index.html @@ -23,8 +23,8 @@
+ + +
+
正在加载期刊与 CCF 数据…
@@ -69,7 +74,7 @@ diff --git a/plugins/micro-tools/index.js b/plugins/micro-tools/index.js index d3c8e56da..e735a8274 100644 --- a/plugins/micro-tools/index.js +++ b/plugins/micro-tools/index.js @@ -1,6 +1,7 @@ // index.js —— 科研小盒 前端逻辑(多功能 SPA) -// 视图:home(启动器)/ journal(期刊查询)/ database(数据库查询) -// 数据检索由 preload 的 window.journalApi 提供;ztools API 做剪贴板/通知/打开外链。 +// 视图:home(启动器)/ journal(期刊·会议查询)/ database(数据库查询) +// 数据检索由 preload 的 window.journalApi 提供(JCR 期刊 + CCF 目录已合并); +// ztools API 做剪贴板/通知/打开外链。 const $ = (sel) => document.querySelector(sel) @@ -14,13 +15,15 @@ const els = { jSearch: $('#jSearch'), jHint: $('#jHint'), jResults: $('#jResults'), + jFilter: $('#jFilter'), dSearch: $('#dSearch'), dbGrid: $('#dbGrid') } -const VIEW_TITLE = { home: '科研小盒', journal: '期刊查询', database: '数据库查询' } +const VIEW_TITLE = { home: '科研小盒', journal: '期刊 · 会议查询', database: '数据库查询' } let currentView = 'home' let journalInited = false +let jFilter = 'all' // all | journal | conf /* ================= 视图路由 ================= */ function showView(view) { @@ -65,50 +68,102 @@ function openExternal(url) { } } -/* ================= ① 期刊查询 ================= */ +/* ================= ① 期刊 · 会议查询 ================= */ const QCLASS = { Q1: 'q1', Q2: 'q2', Q3: 'q3', Q4: 'q4' } +const CCFCLASS = { A: 'ccf-a', B: 'ccf-b', C: 'ccf-c' } -function journalCardHTML(j) { - const q = j.quartile || '' +// CCF 领域全称 → 短标签(卡片上显示,避免过长) +const CCF_CAT_SHORT = { + '计算机体系结构/并行与分布计算/存储系统': '体系结构', + '计算机网络': '网络', + '网络与信息安全': '安全', + '软件工程/系统软件/程序设计语言': '软件工程', + '数据库/数据挖掘/内容检索': '数据库', + '计算机科学理论': '理论', + '计算机图形学与多媒体': '图形学', + '人工智能': '人工智能', + '人机交互与普适计算': '人机交互', + '交叉/综合/新兴': '交叉' +} + +function shortCats(cat) { + if (!cat) return '' + return String(cat).split('·').map((s) => CCF_CAT_SHORT[s.trim()] || s.trim()).join(' · ') +} + +// CCF 变动备注 → 徽章文案 +const NOTE_LABEL = { '新增': '新增', '晋级': '晋级', '降级': '降级', '名称更新': '更名' } + +function noteBadge(note) { + if (!note) return '' + const key = Object.keys(NOTE_LABEL).find((k) => note.indexOf(k) !== -1) + if (!key) return '' + return `${esc(NOTE_LABEL[key])}` +} + +function journalCardHTML(r, i) { + const q = r.quartile || '' const qc = QCLASS[q] || 'qna' - const jif = j.jif === '' || j.jif == null ? '—' : j.jif + const hasIf = r.jif !== '' && r.jif != null + const jif = hasIf ? r.jif : '—' + const rank = r.ccfRank || '' + const cc = rank ? (CCFCLASS[rank] || 'ccf-c') : '' + const isConf = !!r._conf + + const metaBits = [] + if (r.issn) metaBits.push(`ISSN ${esc(r.issn)}`) + if (r.category && !isConf) metaBits.push(esc(r.category)) + if (isConf) metaBits.push('会议') + if (!metaBits.length) metaBits.push('—') + + const hasStats = hasIf || r.citations || r.jci || r.jifRank + return ` -
+
-

${esc(j.name)}

-
${esc(j.abbr || '—')} · ${esc(j.publisher || '—')}
+

${esc(r.name)}

+
${esc(r.abbr || r.ccfAbbr || '—')} · ${esc(r.publisher || '—')}
- IF ${esc(jif)} - ${q ? `${esc(q)}` : ''} +
+ ${hasIf ? `IF ${esc(jif)}` : ''} + ${q ? `${esc(q)}` : ''} +
+
+ ${rank ? `CCF ${esc(rank)}` : ''} + ${rank ? `${esc(shortCats(r.ccfCat))}` : ''} + ${noteBadge(r.ccfNotes)} +
- ISSN ${esc(j.issn || '—')} - · - ${esc(j.category || '—')} + ${metaBits.map((m, k) => (k ? `·${m}` : `${m}`)).join('')}
+ ${r.ccfCat ? `
CCF 领域:${esc(r.ccfCat)}
` : ''} + ${hasStats ? `
-
${esc(j.citations || '—')}总被引
-
${esc(j.jci || '—')}JCI
-
${esc(j.oa || '—')}${j.oa ? '%' : ''}金色OA
-
${esc(j.jifRank || '—')}JIF排名
-
- ${j.catDetail ? `
分区详情:${esc(j.catDetail)}
` : ''} +
${esc(r.citations || '—')}总被引
+
${esc(r.jci || '—')}JCI
+
${esc(r.oa || '—')}${r.oa ? '%' : ''}金色OA
+
${esc(r.jifRank || '—')}JIF排名
+ ` : ''} + ${r.catDetail ? `
分区详情:${esc(r.catDetail)}
` : ''}
- ${j.abbr ? `` : ''} + ${(!isConf && r.abbr) ? `` : ''} + ${r.ccfUrl ? `` : ''}
` } function renderJournal(list) { if (!list.length) { - els.jResults.innerHTML = '
未找到匹配的期刊,换个关键词试试。
' + const what = jFilter === 'conf' ? '会议' : jFilter === 'journal' ? '期刊' : '期刊或会议' + els.jResults.innerHTML = `
未找到匹配的${what},换个关键词或切换筛选试试。
` return } - els.jResults.innerHTML = list.map(journalCardHTML).join('') + els.jResults.innerHTML = list.map((r, i) => journalCardHTML(r, r._i == null ? i : r._i)).join('') } function doJournalSearch(q) { @@ -116,12 +171,14 @@ function doJournalSearch(q) { if (!api) return const query = (q || '').trim() if (query) { - const list = api.search(query) + const list = api.search(query, 60, jFilter) renderJournal(list) - els.jHint.innerHTML = `匹配到 ${list.length} 条结果(按影响因子排序,最多 60 条)` + els.jHint.innerHTML = `匹配到 ${list.length} 条${jFilter === 'conf' ? '会议' : jFilter === 'journal' ? '期刊' : '结果'}(按相关度、影响因子排序,最多 60 条)` } else { - renderJournal(api.top(20)) - els.jHint.innerHTML = `共收录 ${api.total} 条期刊数据(默认显示影响因子最高的 20 条)` + const list = api.top(20) + renderJournal(list) + const ccf = api.ccfTotal ? ` · CCF 目录 ${api.ccfTotal} 条独立条目` : '' + els.jHint.innerHTML = `共收录 ${api.total} 条 JCR 期刊${ccf}(默认显示影响因子最高的 20 条)` } } @@ -133,16 +190,18 @@ function initJournal() { els.jHint.textContent = 'preload 未加载,无法读取期刊数据。' return } - // 首次进入触发一次性加载(gzip 解压 + 解析约 <0.1 秒) - els.jHint.textContent = '正在加载期刊数据…' + els.jHint.textContent = '正在加载期刊与 CCF 数据…' setTimeout(() => { const info = api.ensureLoaded() if (!info.ok) { els.jHint.textContent = '数据加载失败:' + info.error - els.jResults.innerHTML = '
无法读取期刊数据,请确认 journals.json.gz 与插件在同一目录。
' + els.jResults.innerHTML = '
无法读取 journals.json.gz,请确认它与插件在同一目录。
' return } journalInited = true + if (info.ccfError) { + els.jHint.innerHTML = `JCR 加载正常,CCF 目录加载失败:${esc(info.ccfError)}` + } doJournalSearch(els.jSearch.value) }, 30) } @@ -153,40 +212,64 @@ els.jSearch.addEventListener('input', () => { jTimer = setTimeout(() => doJournalSearch(els.jSearch.value), 220) }) +els.jFilter.addEventListener('click', (e) => { + const chip = e.target.closest('.chip') + if (!chip) return + jFilter = chip.dataset.filter + els.jFilter.querySelectorAll('.chip').forEach((c) => c.classList.toggle('is-on', c === chip)) + doJournalSearch(els.jSearch.value) +}) + els.jResults.addEventListener('click', (e) => { - const btn = e.target.closest('[data-copy]') - if (btn) { - const name = btn.closest('.card').dataset.name - const j = window.journalApi && window.journalApi.search(name)[0] - if (!j) return - const text = - `期刊: ${j.name}\n缩写: ${j.abbr || '—'}\n出版商: ${j.publisher || '—'}\n` + - `ISSN: ${j.issn || '—'}\n学科: ${j.category || '—'}\n` + - `影响因子(JIF): ${j.jif || '—'} 分区: ${j.quartile || '—'}\n` + - `JCI: ${j.jci || '—'} 5年IF: ${j.fiveYearJif || '—'}\n` + - `总被引: ${j.citations || '—'} 金色OA: ${j.oa || '—'}%\n` + - `JIF排名: ${j.jifRank || '—'}\n分区详情: ${j.catDetail || '—'}\n数据年份: ${j.year || '—'}` - copyToClipboard(text, '已复制期刊信息') + const card = e.target.closest('.card') + if (!card) return + const rec = window.journalApi && window.journalApi.get(Number(card.dataset.idx)) + if (!rec) return + + if (e.target.closest('[data-copy]')) { + const lines = [ + `名称: ${rec.name}`, + `缩写: ${rec.abbr || rec.ccfAbbr || '—'}`, + `出版商/主办: ${rec.publisher || '—'}` + ] + if (rec.ccfRank) { + lines.push(`CCF: ${rec.ccfRank} 类(${rec.ccfType})`) + lines.push(`CCF 领域: ${rec.ccfCat || '—'}`) + if (rec.ccfNotes) lines.push(`CCF 变动: ${rec.ccfNotes}`) + } + if (rec.jif !== '' && rec.jif != null) { + lines.push( + `影响因子(JIF): ${rec.jif} 分区: ${rec.quartile || '—'}`, + `JCI: ${rec.jci || '—'} 5年IF: ${rec.fiveYearJif || '—'}`, + `总被引: ${rec.citations || '—'} 金色OA: ${rec.oa || '—'}%`, + `JIF排名: ${rec.jifRank || '—'}`, + `分区详情: ${rec.catDetail || '—'}`, + `数据年份: ${rec.year || '—'}` + ) + } else { + lines.push('影响因子: 未被 JCR 2026 收录') + } + if (rec.issn) lines.push(`ISSN: ${rec.issn}`) + copyToClipboard(lines.join('\n'), '已复制信息') return } - const jcrBtn = e.target.closest('[data-jcr]') - if (jcrBtn) { - const card = jcrBtn.closest('.card') - const name = card.dataset.name - const j = window.journalApi && window.journalApi.search(name)[0] - if (j && j.abbr) { + if (e.target.closest('[data-jcr]')) { + if (rec.abbr) { openExternal( 'https://jcr.clarivate.com/jcr-jp/journal-profile?journal=' + - encodeURIComponent(j.abbr) + '&year=2025&fromPage=%2Fjcr%2Fhome' + encodeURIComponent(rec.abbr) + '&year=2025&fromPage=%2Fjcr%2Fhome' ) } return } + + if (e.target.closest('[data-home]')) { + if (rec.ccfUrl) openExternal(rec.ccfUrl) + } }) /* ================= ② 数据库查询(体验版) ================= */ -// 通过检索词拼出各数据库的搜索链接,用系统浏览器打开。 const DATABASES = [ { name: 'NCBI Genome', desc: '基因组/物种参考序列', url: (q) => `https://www.ncbi.nlm.nih.gov/genome/?term=${q}` }, { name: 'NCBI Gene', desc: '基因功能与位点信息', url: (q) => `https://www.ncbi.nlm.nih.gov/gene/?term=${q}` }, @@ -220,7 +303,7 @@ els.dbGrid.addEventListener('click', (e) => { els.dSearch.addEventListener('keydown', (e) => { if (e.key === 'Enter') { const q = els.dSearch.value.trim() - if (q) openExternal(DATABASES[0].url(encodeURIComponent(q))) // 回车默认打开第一个(NCBI Genome) + if (q) openExternal(DATABASES[0].url(encodeURIComponent(q))) } }) @@ -236,14 +319,20 @@ if (window.ztools && window.ztools.onPluginEnter) { window.ztools.onPluginEnter((param) => { const code = param && param.code const payload = param && param.payload != null ? String(param.payload) : '' - if (code === 'journal') { + if (code === 'journal' || code === 'ccf') { showView('journal') + if (code === 'ccf' && !payload) { + jFilter = 'conf' + els.jFilter.querySelectorAll('.chip').forEach((c) => c.classList.toggle('is-on', c.dataset.filter === 'conf')) + } if (payload) { els.jSearch.value = payload - // 如果数据已加载完成,立即搜索;否则 initJournal 会在加载后自动触发 + // 数据已加载则立即搜索,否则 initJournal 会在加载完成后自动触发 if (journalInited && window.journalApi && window.journalApi.loaded) { doJournalSearch(payload) } + } else if (journalInited) { + doJournalSearch('') } } else if (code === 'database') { showView('database') diff --git a/plugins/micro-tools/pack.sh b/plugins/micro-tools/pack.sh new file mode 100755 index 000000000..7a06f3691 --- /dev/null +++ b/plugins/micro-tools/pack.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# ============================================================ +# 打包 科研小盒 (micro-tools) 为 ZTools 可安装的 .zpx +# +# .zpx 格式 = brotli 压缩的 Electron ASAR(ZTools ≥ 2.3.1 采用; +# ZTools 安装时先解压,再按 ASAR 读取 plugin.json) +# +# 用法: bash pack.sh (在插件项目根目录执行) +# 产物: ../micro-tools-v<版本>.zpx (生成在插件目录上一级,方便直接分发) +# +# 关键:只打包「git 跟踪的运行文件」,绝不包含 .git / .DS_Store / +# node_modules / 本脚本 / 未跟踪的临时文件。 +# ============================================================ +set -euo pipefail +cd "$(dirname "$0")" + +[ -f plugin.json ] || { echo "❌ 未找到 plugin.json,请在插件项目根目录运行。" >&2; exit 1; } + +# 从 plugin.json 读取版本号,用于产物命名(版本统一以 plugin.json 为准) +VERSION="$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' plugin.json | head -1 | sed -E 's/.*"([^"]+)"$/\1/')" +[[ -n "$VERSION" ]] || { echo "❌ 无法从 plugin.json 读取 version。" >&2; exit 1; } +OUT="../micro-tools-v${VERSION}.zpx" + +# 收集要打包的文件:git 跟踪文件(天然排除 .git / node_modules / 未跟踪文件), +# 再排除开发类文件:.gitignore、本打包脚本 +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + echo "❌ 不在 git 仓库内:请先 git init && git add . && git commit(依赖 git 跟踪列表排除 .git)。" >&2 + exit 1 +fi +git ls-files | grep -vE '^(\.gitignore|pack\.sh)$' > /tmp/mt_pack_files.txt +[ -s /tmp/mt_pack_files.txt ] || { echo "❌ 没有可打包的文件(git ls-files 为空),请先 git add 并提交。" >&2; exit 1; } + +# 把文件复制到临时干净目录(保持目录结构) +TMP_SRC="$(mktemp -d)" +TMP_ASAR="${TMP_SRC}/plugin.asar" +while IFS= read -r f; do + mkdir -p "${TMP_SRC}/$(dirname "$f")" + cp "$f" "${TMP_SRC}/$f" +done < /tmp/mt_pack_files.txt + +# 构造 ASAR 并 brotli 压缩为 .zpx(node 内置能力,无需安装 @electron/asar / 联网) +# +# ASAR 布局(与 @electron/asar 完全一致): +# [u32 4][u32 8+jsonLen][u32 4+jsonLen][u32 jsonLen][json][数据区] +# 文件元信息里 offset 为相对数据区起点的偏移(字符串形式) +node -e ' +const fs = require("fs"), path = require("path"), zlib = require("zlib"); +const SRC = process.argv[1], OUT = process.argv[2]; +const list = fs.readFileSync(process.argv[3], "utf8").trim().split("\n").filter(Boolean); +const chunks = []; let off = 0; const tree = {}; +for (const f of list) { + const buf = fs.readFileSync(path.join(SRC, f)); + tree[f] = { size: buf.length, offset: String(off) }; + off += buf.length; chunks.push(buf); +} +const json = Buffer.from(JSON.stringify({ files: tree }), "utf8"); +const head = Buffer.alloc(16); +head.writeUInt32LE(4, 0); +head.writeUInt32LE(8 + json.length, 4); +head.writeUInt32LE(4 + json.length, 8); +head.writeUInt32LE(json.length, 12); +const asar = Buffer.concat([head, json, ...chunks]); +fs.writeFileSync(OUT, zlib.brotliCompressSync(asar)); +console.log("✔ 已生成 " + OUT + " (" + fs.statSync(OUT).size + " B,包内 " + list.length + " 个文件)"); +' "$PWD" "$OUT" /tmp/mt_pack_files.txt + +# 校验:解回 ASAR 并列出包内文件(应只见运行文件,无 .git / .DS_Store) +echo "--- 包内文件 ---" +node -e ' +const fs = require("fs"), zlib = require("zlib"); +const b = zlib.brotliDecompressSync(fs.readFileSync(process.argv[1])); +const jl = b.readUInt32LE(12); +const h = JSON.parse(b.slice(16, 16 + jl).toString("utf8")); +for (const [n, m] of Object.entries(h.files)) console.log(" /" + n + " " + m.size + " B"); +' "$OUT" + +rm -rf "$TMP_SRC" /tmp/mt_pack_files.txt +echo "✅ 打包完成:${OUT}" diff --git a/plugins/micro-tools/plugin.json b/plugins/micro-tools/plugin.json index e6527425b..4e5ae220e 100644 --- a/plugins/micro-tools/plugin.json +++ b/plugins/micro-tools/plugin.json @@ -1,8 +1,8 @@ { "name": "micro-tools", "title": "科研小盒", - "description": "科研常用小工具集合。当前功能:① 期刊查询——基于 JCR 2026 名单查询影响因子(JIF)、分区、JCI、被引等;② 常用数据库查询——一键检索 NCBI Genome / PubMed / UniProt 等(占位,持续扩展)。纯 Preload 模式,无构建步骤,可直接被 ZTools 加载。", - "version": "1.3.0", + "description": "科研常用小工具集合。当前功能:① 期刊·会议查询——JCR 2026 影响因子/分区 + CCF 第七版(2026.3)A/B/C 等级,同一入口检索期刊与会议;② 常用数据库查询——一键检索 NCBI Genome / PubMed / UniProt 等(占位,持续扩展)。纯 Preload 模式,无构建步骤,可直接被 ZTools 加载。", + "version": "1.3.1", "author": "Asa12138", "email": "bfzede@gmail.com", "main": "index.html", @@ -16,7 +16,7 @@ }, { "code": "journal", - "explain": "查询期刊影响因子/分区等信息,例如 期刊 Nature / 影响因子 JACS", + "explain": "查询期刊/会议的影响因子、分区与 CCF 等级,例如 期刊 Nature / 影响因子 JACS", "cmds": [ "期刊", "影响因子", @@ -25,11 +25,21 @@ "jcr", { "type": "over", - "label": "查询期刊", + "label": "查询期刊/会议", "minLength": 1 } ] }, + { + "code": "ccf", + "explain": "查询 CCF 第七版推荐会议与期刊等级,例如 ccf CVPR / 会议 NeurIPS", + "cmds": [ + "ccf", + "会议", + "会议等级", + "conference" + ] + }, { "code": "database", "explain": "常用科研数据库查询,例如 数据库 BRCA1 / ncbi genome", diff --git a/plugins/micro-tools/preload.js b/plugins/micro-tools/preload.js index 13a7513c2..058dd609c 100644 --- a/plugins/micro-tools/preload.js +++ b/plugins/micro-tools/preload.js @@ -2,21 +2,27 @@ // 规则:遵循 CommonJS 规范,可 require Node.js / Electron 原生能力; // 本文件源码保持清晰可读,未压缩混淆,并与插件一起发布。 // -// 职责:读取并解压同目录下的 journals.json.gz(短键名压缩版), -// 映射回完整字段后供前端检索使用。 -// 数据源替换:重新生成 journals.json.gz 即可(字段映射见 KEY_MAP)。 +// 职责:读取并解压同目录下的两个数据集,映射回完整字段后供前端检索: +// 1. journals.json.gz —— JCR 2026 期刊(影响因子 / 分区 / JCI …) +// 2. ccf.json.gz —— CCF 第七版(2026.3)推荐国际学术会议与期刊目录 +// 两者在加载时按 ISSN 关联合并:同一本刊既显示影响因子,也显示 CCF 等级; +// 未被 JCR 收录的 CCF 条目(全部会议 + 少数期刊)作为独立记录并入检索。 const fs = require('fs') const path = require('path') const zlib = require('zlib') const DATA_FILE = 'journals.json.gz' +const CCF_FILE = 'ccf.json.gz' -let DATA = [] +let DATA = [] // JCR 期刊(合并后会挂上 CCF 字段) +let CCF = [] // 未关联 JCR 的独立 CCF 条目 +let ALL = [] // 统一检索数组 = DATA + CCF let LOADED = false let ERROR = null +let CCF_ERROR = null -// 短键名 → 完整字段名(与 Python 压缩脚本的 KEY_MAP 对应) +// 短键名 → 完整字段名(与生成脚本对应) const KEY_MAP = { n: 'name', a: 'abbr', i: 'issn', e: 'eissn', p: 'publisher', c: 'category', j: 'jif', q: 'quartile', @@ -30,8 +36,7 @@ function expand(rec) { for (var k in rec) { out[KEY_MAP[k] || k] = rec[k] } - // 补充计算字段 - var jifStr = String(out.jif || '') + var jifStr = String(out.jif == null ? '' : out.jif) var jifNum = -1 if (jifStr !== '') { var n = parseFloat(jifStr) @@ -41,86 +46,166 @@ function expand(rec) { return out } -// 解压 gzip → JSON → 展开键名 → 记录数组(懒加载) +/* ---------------- 检索辅助 ---------------- */ + +function isIssnLike(q) { + return /^\d{4}-?\d{3}[\dxX]$/.test(q.replace(/\s+/g, '')) +} + +// 归一化:转大写并去除所有非字母数字(消除空格/连字符/点/大小写差异) +function norm(s) { + return String(s == null ? '' : s).toUpperCase().replace(/[^A-Z0-9]/g, '') +} + +var STOP = { THE: 1, OF: 1, AND: 1, A: 1, AN: 1, VOL: 1, NO: 1 } +function tok(s) { + return String(s == null ? '' : s).toUpperCase().replace(/[^A-Z0-9 ]/g, ' ').split(/\s+/).filter(function (t) { return t && !STOP[t] }) +} + +/* ---------------- 加载 JCR ---------------- */ + +function loadJournals() { + var file = path.join(__dirname, DATA_FILE) + var raw = JSON.parse(zlib.gunzipSync(fs.readFileSync(file)).toString('utf-8')) + DATA = new Array(raw.length) + for (var i = 0; i < raw.length; i++) { + var j = expand(raw[i]) + j._tokensAll = tok((j.abbr || '') + ' ' + (j.name || '')) + DATA[i] = j + } +} + +/* ---------------- 加载 CCF 并合并 ---------------- */ + +function loadCcf() { + var file = path.join(__dirname, CCF_FILE) + var raw = JSON.parse(zlib.gunzipSync(fs.readFileSync(file)).toString('utf-8')) + + // ISSN/eISSN → JCR 记录(用于校验下标是否仍然有效) + var byIssn = {} + for (var i = 0; i < DATA.length; i++) { + var d = DATA[i] + if (d.issn) byIssn[d.issn] = d + if (d.eissn) byIssn[d.eissn] = d + } + + CCF = [] + var linked = 0 + + for (var k = 0; k < raw.length; k++) { + var o = raw[k] + var type = o.t === 1 ? '期刊' : '会议' + var info = { + ccfRank: o.r || '', + ccfCat: o.c || '', + ccfType: type, + ccfUrl: o.u || '', + ccfNotes: o.x || '', + ccfAbbr: o.a || '' + } + + var host = null + if (o.k != null) { + var cand = DATA[o.k] + if (cand && (!o.s || cand.issn === o.s || cand.eissn === o.s)) { + host = cand + } else if (o.s && byIssn[o.s]) { + host = byIssn[o.s] // 下标失效时退回 ISSN 查找 + } + } + + if (host) { + // 关联成功:把 CCF 信息挂到 JCR 期刊上,并把 CCF 缩写/中文领域并入检索 + host.ccfRank = info.ccfRank + host.ccfCat = info.ccfCat + host.ccfType = type + host.ccfUrl = info.ccfUrl + host.ccfNotes = info.ccfNotes + host.ccfAbbr = info.ccfAbbr + host._tokensAll = host._tokensAll.concat(tok((o.a || '') + ' ' + (o.n || '') + ' ' + (o.c || ''))) + linked++ + continue + } + + // 无 JCR 对应(会议,或已被 JCR 除名的期刊):独立成条 + var rec = { + name: o.n || '', + abbr: o.a || '', + publisher: o.p || '', + ccfRank: info.ccfRank, + ccfCat: info.ccfCat, + ccfType: type, + ccfUrl: info.ccfUrl, + ccfNotes: info.ccfNotes, + ccfAbbr: info.ccfAbbr, + // 与 JCR 记录保持同构,便于复用同一套评分/渲染 + issn: '', eissn: '', jif: '', jifNum: -1, quartile: '', + jci: '', citations: '', fiveYearJif: '', jifRank: '', oa: '', catDetail: '', + category: o.c || '', year: '', + _conf: type === '会议', + _tokensAll: tok((o.a || '') + ' ' + (o.n || '') + ' ' + (o.p || '') + ' ' + (o.c || '')) + } + CCF.push(rec) + } + + ALL = DATA.concat(CCF) + // 记录自身在统一数组中的下标,供渲染后按 data-idx 精确回查(避免按名字反查出错) + for (var z = 0; z < ALL.length; z++) ALL[z]._i = z + return { total: raw.length, linked: linked, standalone: CCF.length } +} + function ensureLoaded() { - if (LOADED) return { ok: !ERROR, total: DATA.length, error: ERROR } + if (LOADED) return { ok: !ERROR, total: DATA.length, error: ERROR, ccf: CCF.length, ccfError: CCF_ERROR } LOADED = true try { - var file = path.join(__dirname, DATA_FILE) - var gzBuf = fs.readFileSync(file) - var jsonBuf = zlib.gunzipSync(gzBuf) - var raw = JSON.parse(jsonBuf.toString('utf-8')) - - DATA = new Array(raw.length) - for (var i = 0; i < raw.length; i++) { - var j = expand(raw[i]) - // 构建检索用的小写拼接串(一次性) - j._hay = ((j.name || '') + ' ' + (j.abbr || '') + ' ' + - (j.issn || '') + ' ' + (j.eissn || '')).toLowerCase() - // 缩写前缀匹配用:同时覆盖“缩写 + 全称”的全部词(去停用词) - j._tokensAll = tok((j.abbr || '') + ' ' + (j.name || '')) - DATA[i] = j - } + loadJournals() ERROR = null } catch (e) { ERROR = String((e && e.message) || e) DATA = [] console.error('[科研小盒] 加载 journals.json.gz 失败:', ERROR) } - return { ok: !ERROR, total: DATA.length, error: ERROR } + try { + if (!ERROR) loadCcf() + } catch (e2) { + CCF_ERROR = String((e2 && e2.message) || e2) + CCF = [] + ALL = DATA + console.error('[科研小盒] 加载 ccf.json.gz 失败:', CCF_ERROR) + } + if (!ALL.length) ALL = DATA.concat(CCF) + return { ok: !ERROR, total: DATA.length, error: ERROR, ccf: CCF.length, ccfError: CCF_ERROR } } /* ---------------- 搜索相关性评分 ---------------- */ -function isIssnLike(q) { - return /^\d{4}-?\d{3}[\dxX]$/.test(q.replace(/\s+/g, '')) -} - /** - * 计算查询词与期刊的相关性得分(越高越相关) - * 3 = 精确匹配(ISSN / 期刊全名 / 缩写完全等于 query) + * 3 = 精确匹配(ISSN / 全称 / 缩写 / CCF 缩写 完全等于 query) * 2 = 词首/前缀匹配 - * 1 = 包含匹配 + * 1 = 包含匹配 / 全词命中 * 0 = 不匹配 */ -// 归一化:转大写并去除所有非字母数字(消除空格/连字符/点/大小写差异) -// 例: "Acta Derm Venereol" 与 "ACTA DERM-VENEREOL" 归一化后均为 "ACTADERMVENEREOL" -function norm(s) { - return String(s == null ? '' : s).toUpperCase().replace(/[^A-Z0-9]/g, '') -} - -// 分词 + 停用词(用于词级回退匹配) -var STOP = { THE: 1, OF: 1, AND: 1, A: 1, AN: 1, VOL: 1, NO: 1 } -function tok(s) { - return String(s == null ? '' : s).toUpperCase().replace(/[^A-Z0-9 ]/g, ' ').split(/\s+/).filter(function (t) { return t && !STOP[t] }) -} +function relevanceScore(r, q) { + // 中文查询(如"人工智能")归一化后会变成空串,需在此单独处理:只匹配 CCF 中文领域 + if (r.ccfCat && q && String(r.ccfCat).indexOf(q) !== -1) return 1 -function relevanceScore(journal, q) { var qn = norm(q) if (!qn) return 0 - // ISSN 精确匹配 - var issnNorm = norm(journal.issn || '') - var eissnNorm = norm(journal.eissn || '') - if (issnNorm === qn || eissnNorm === qn) return 3 + var issnNorm = norm(r.issn || '') + var eissnNorm = norm(r.eissn || '') + if ((issnNorm && issnNorm === qn) || (eissnNorm && eissnNorm === qn)) return 3 - var name = norm(journal.name || '') - var abbr = norm(journal.abbr || '') + var name = norm(r.name || '') + var abbr = norm(r.abbr || '') + var ccfA = norm(r.ccfAbbr || '') - // 精确全名/缩写(已归一化,自动容忍符号与大小写) - if (name === qn || abbr === qn) return 3 - - // 前缀匹配 - if (name.indexOf(qn) === 0 || abbr.indexOf(qn) === 0) return 2 - - // 子串包含 - if (name.indexOf(qn) !== -1 || abbr.indexOf(qn) !== -1) return 1 - - // 词级回退:查询的每个分词都能在刊名/缩写分词中找到,或作为某词前缀(如 N→NEW) - // 注意:必须用“原始带空格的 q”做分词,不能用归一化后的结果(会丢失词边界) + if (name === qn || abbr === qn || ccfA === qn) return 3 + if (name.indexOf(qn) === 0 || abbr.indexOf(qn) === 0 || ccfA.indexOf(qn) === 0) return 2 + if (name.indexOf(qn) !== -1 || abbr.indexOf(qn) !== -1 || ccfA.indexOf(qn) !== -1) return 1 var qt = tok(q) if (qt.length) { - var recTok = tok(journal.abbr || journal.name) + var recTok = tok((r.abbr || '') + ' ' + (r.name || '') + ' ' + (r.ccfAbbr || '')) if (recTok.length) { var hit = 0 for (var ti = 0; ti < qt.length; ti++) { @@ -134,17 +219,13 @@ function relevanceScore(journal, q) { } } - // 缩写前缀匹配:处理 PubMed/NLM 缩写与 JCR 全称/缩写差异。 - // query 每个词必须是候选某词的前缀或反之(双向),且全部词命中。 - // 例: "Comput Struct Biotechnol J" -> "Computational and Structural Biotechnology Journal" - // "Probiotics Antimicrob Proteins" -> "Probiotics and Antimicrobial Proteins" - // "Clinical, Cosmetic and Investigational Dermatology" -> "CLIN COSMET INV DERM" - if (qt.length >= 2 && journal._tokensAll && journal._tokensAll.length) { + // 缩写前缀双向匹配(处理 PubMed/NLM 缩写与全称差异) + if (qt.length >= 2 && r._tokensAll && r._tokensAll.length) { var allHit = true for (var ci = 0; ci < qt.length; ci++) { var qw = qt[ci], found = false - for (var di = 0; di < journal._tokensAll.length; di++) { - var cw = journal._tokensAll[di] + for (var di = 0; di < r._tokensAll.length; di++) { + var cw = r._tokensAll[di] if (cw.indexOf(qw) === 0 || qw.indexOf(cw) === 0) { found = true; break } } if (!found) { allHit = false; break } @@ -162,51 +243,61 @@ window.journalApi = { get loaded() { return LOADED && !ERROR }, get total() { return DATA.length }, get error() { return ERROR }, + get allTotal() { return ALL.length }, + get ccfTotal() { return CCF.length }, + get ccfError() { return CCF_ERROR }, + + /** 按下标取统一检索数组中的记录(渲染与操作复用,避免按名字反查出错) */ + get(i) { return ALL[i] }, /** - * 搜索期刊:按相关性得分排序(精确 > 前缀 > 包含),同分按 JIF 高→低 - * @param {string} query 查询词(期刊名 / 缩写 / ISSN) - * @param {number} limit 最大返回条数 + * 统一检索:JCR 期刊 + CCF 目录 + * @param {string} query + * @param {number} limit + * @param {string} filter 'all' | 'journal' | 'conf' */ - search(query, limit /* = 60 */) { + search(query, limit /* = 60 */, filter /* = 'all' */) { if (limit == null) limit = 60 + if (filter == null) filter = 'all' ensureLoaded() - var q = String(query == null ? '' : query).trim().toLowerCase() + var q = String(query == null ? '' : query).trim() if (!q) return [] + var out = [] + var wantConf = filter === 'conf' + var wantJour = filter === 'journal' + // ISSN 路径:精确匹配优先 if (isIssnLike(q)) { - var norm = q.replace(/\s+/g, '') - var results = [] - for (var i = 0; i < DATA.length; i++) { - var j = DATA[i] - if ((j.issn || '').replace(/\s+/g, '').toLowerCase() === norm || - (j.eissn || '').replace(/\s+/g, '').toLowerCase() === norm) { - results.push(j) - if (results.length >= limit) break - } + var nq = q.replace(/\s+/g, '').toLowerCase() + for (var i = 0; i < ALL.length && out.length < limit; i++) { + var r = ALL[i] + if (r._conf && wantJour) continue + if (!r._conf && wantConf) continue + if ((r.issn || '').replace(/\s+/g, '').toLowerCase() === nq || + (r.eissn || '').replace(/\s+/g, '').toLowerCase() === nq) out.push(r) } - return results + return out } - // 通用路径:评分 + 排序 + var ql = q.toLowerCase() var scored = [] - for (var k = 0; k < DATA.length; k++) { - var journal = DATA[k] - var s = relevanceScore(journal, q) - if (s > 0) scored.push({ j: journal, score: s }) + for (var k = 0; k < ALL.length; k++) { + var rec = ALL[k] + if (rec._conf && wantJour) continue + if (!rec._conf && wantConf) continue + var s = relevanceScore(rec, ql) + if (s > 0) scored.push({ r: rec, score: s, i: k }) } scored.sort(function (a, b) { if (b.score !== a.score) return b.score - a.score - return b.j.jifNum - a.j.jifNum + if (b.r.jifNum !== a.r.jifNum) return b.r.jifNum - a.r.jifNum + return a.i - b.i }) - var result = [] - for (var m = 0; m < scored.length && m < limit; m++) { - result.push(scored[m].j) - } - return result + for (var m = 0; m < scored.length && m < limit; m++) out.push(scored[m].r) + return out }, top(n /* = 20 */) {