chore: batch commit remaining changes
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s

Includes search engine improvements, Alembic migrations,
new services (pubmed_daily_update, query_expansion),
frontend updates, and documentation sync.
This commit is contained in:
34047007@qq.com
2026-07-27 08:35:12 +08:00
parent 35b0a5c565
commit 62ca8fa6b8
82 changed files with 314248 additions and 1519 deletions
@@ -3,7 +3,7 @@ import { computed } from 'vue'
import { NTag } from 'naive-ui'
import { computeStudyTypes } from '../../constants/studyTypes'
import type { StudyDesign, TrialReg, RctDetection } from '../../types'
import type { LiteratureItem } from '../../types'
import type { LiteratureItem, DisplaySettings } from '../../types'
import { useMessage } from 'naive-ui'
import { copyToClipboard } from '../../utils/clipboard'
@@ -13,11 +13,14 @@ const props = defineProps<{
showSave?: boolean
savedPmids?: Set<number>
searchQuery?: string
displaySettings?: DisplaySettings
}>()
const emit = defineEmits(['detail', 'preview', 'save', 'dismiss'])
const message = useMessage()
const ds = computed(() => props.displaySettings || {} as DisplaySettings)
function onSave(e: Event) { e.stopPropagation(); emit('save', props.item) }
function onDismiss(e: Event) { e.stopPropagation(); emit('dismiss', props.item) }
@@ -105,14 +108,30 @@ const nctId = computed(() => {
})
// ── 搜索关键词高亮 ──
// 从查询中提取纯文本词(去掉 PubMed 字段标签如 [TI]、[AB] 等)
function extractPlainText(q: string): string {
return q
.replace(/\[[\w-]+\]/g, '')
.replace(/"?\b(AND|OR|NOT)\b"?/gi, '')
.replace(/"/g, '')
.replace(/\s+/g, ' ')
.trim()
}
const highlightedTitle = computed(() => {
const title = props.item.title || ''
const q = props.searchQuery?.trim()
if (!q) return ''
const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const parts = title.split(new RegExp(`(${escaped})`, 'gi'))
const raw = props.searchQuery?.trim()
if (!raw) return ''
const plain = extractPlainText(raw)
if (!plain) return ''
// 拆分为独立词项,逐词高亮(多词查询不拼成一个连写短语)
const terms = plain.split(/\s+/).filter(t => t.length > 0)
if (terms.length === 0) return ''
const escaped = terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
// 单字符词添加 \b 词边界避免过匹配("A" 不匹配每个字母 "a"
const pattern = escaped.map(e => e.length === 1 ? `\\b${e}\\b` : e).join('|')
const parts = title.split(new RegExp(`(${pattern})`, 'gi'))
return parts.map((part: string) =>
part.toLowerCase() === escaped.toLowerCase()
terms.some(t => part.toLowerCase() === t.toLowerCase())
? `<mark style="background:#fff3b0;padding:0 2px;border-radius:2px">${escapeHtml(part)}</mark>`
: escapeHtml(part)
).join('')
@@ -127,67 +146,69 @@ function escapeHtml(s: string) {
<div class="lit-card" :class="{ compact, 'is-read': isRead }">
<!-- 标题徽章内联点击跳转详情 -->
<div class="lit-title lit-title-link" @click.stop="emit('detail', item)">
<template v-if="ds.showBadges">
<NTag v-if="isNew" size="tiny" type="info" :bordered="false" class="badge-inline">🆕</NTag>
<NTag v-if="isUpdated" size="tiny" type="success" :bordered="false" class="badge-inline">🔄</NTag>
<NTag v-if="item.retracted" size="tiny" type="error" :bordered="false" class="badge-inline">已撤稿</NTag>
<NTag v-if="item.is_negative_result && !item.retracted" size="tiny" type="warning" :bordered="false" class="badge-inline">阴性结果</NTag>
<NTag v-if="rctInfo?.confidence === 'confirmed'" size="tiny" type="error" :bordered="false" class="badge-inline">RCT </NTag>
<NTag v-else-if="rctInfo?.confidence === 'suspected'" size="tiny" type="info" :bordered="false" class="badge-inline">RCT ?</NTag>
</template>
<span v-if="highlightedTitle" v-html="highlightedTitle"></span>
<span v-else>{{ item.title || '' }}</span>
</div>
<!-- 元信息行 -->
<div class="lit-meta">
<span class="lit-authors">{{ item.first_author || '' }} et al.</span>
<span v-if="item.affiliation" class="affil">🏛 {{ item.affiliation }}</span>
<span v-if="item.journal" class="meta-sep">|</span>
<strong class="lit-journal">{{ item.journal }}</strong>
<NTag v-if="tierType" :type="tierType" size="tiny" :bordered="false" class="tier-tag">Q{{ item.journal_tier }}</NTag>
<span v-if="ds.showAuthors" class="lit-authors">{{ item.first_author || '' }} et al.</span>
<span v-if="ds.showAffiliation && item.affiliation" class="affil">🏛 {{ item.affiliation }}</span>
<span v-if="ds.showJournal && item.journal" class="meta-sep">|</span>
<strong v-if="ds.showJournal && item.journal" class="lit-journal">{{ item.journal }}</strong>
<NTag v-if="ds.showJournal && tierType" :type="tierType" size="tiny" :bordered="false" class="tier-tag">Q{{ item.journal_tier }}</NTag>
<span class="meta-sep">|</span>
<span v-if="displayDate" class="lit-date">{{ displayDate }}</span>
</div>
<!-- DOI / PMID / 研究类型 / OA / 引用 -->
<div class="lit-doi" v-if="studyTypes.length || item.doi || item.pmid || item.is_oa">
<template v-if="studyTypes.length">
<div class="lit-doi" v-if="(ds.showStudyTypes && (studyTypes.length || designLabel)) || (item.doi && ds.showDoi) || (item.pmid && ds.showPmid) || (item.is_oa && ds.showStudyTypes) || (item.cited_by_count && ds.showCitedBy) || (nctId && ds.showStudyTypes)">
<template v-if="ds.showStudyTypes && studyTypes.length">
<NTag v-for="st in studyTypes" :type="st.type" size="tiny" :bordered="false">{{ st.label }}</NTag>
<NTag v-if="designLabel" type="info" size="tiny" :bordered="false">{{ designLabel }}</NTag>
<span class="meta-sep-lit">|</span>
</template>
<template v-if="item.is_oa">
<template v-if="ds.showStudyTypes && item.is_oa">
<NTag type="success" size="tiny" :bordered="false">🔓 免费全文</NTag>
<span class="meta-sep-lit">|</span>
</template>
<template v-if="item.doi">
<template v-if="ds.showDoi && item.doi">
<span class="doi-label">DOI:</span>
<a :href="'https://doi.org/' + item.doi" target="_blank" rel="noopener" class="doi-link" @click.stop>{{ item.doi }}</a>
<span class="doi-copy-btn" @click.stop="copyDoi" title="复制 DOI">📋</span>
</template>
<template v-if="item.doi && item.pmid">
<template v-if="ds.showDoi && item.doi && ds.showPmid && item.pmid">
<span class="meta-sep-lit">|</span>
</template>
<template v-if="item.pmid">
<template v-if="ds.showPmid && item.pmid">
<span class="doi-label">PMID:</span>
<span class="pmid-text">{{ item.pmid }}</span>
<span class="doi-copy-btn" @click.stop="copyPmid" title="复制 PMID">📋</span>
</template>
<span v-if="item.cited_by_count" class="meta-sep-lit">|</span>
<span v-if="item.cited_by_count" class="cited-count">📊 被引 {{ item.cited_by_count }}</span>
<span v-if="nctId" class="meta-sep-lit">|</span>
<a v-if="nctId" :href="'https://clinicaltrials.gov/study/' + nctId" target="_blank" rel="noopener" class="nct-link" @click.stop>{{ nctId }}</a>
<span v-if="ds.showCitedBy && item.cited_by_count" class="meta-sep-lit">|</span>
<span v-if="ds.showCitedBy && item.cited_by_count" class="cited-count">📊 被引 {{ item.cited_by_count }}</span>
<span v-if="ds.showStudyTypes && nctId" class="meta-sep-lit">|</span>
<a v-if="ds.showStudyTypes && nctId" :href="'https://clinicaltrials.gov/study/' + nctId" target="_blank" rel="noopener" class="nct-link" @click.stop>{{ nctId }}</a>
</div>
<!-- 标签 -->
<div class="lit-tags" v-if="item.tags?.length">
<div class="lit-tags" v-if="ds.showTags && item.tags?.length">
<NTag v-for="t in item.tags.slice(0,6)" :key="t.id||t.name_zh" size="tiny" :bordered="false">{{ t.name_zh || t.name_en }}</NTag>
</div>
<!-- AI 摘要 -->
<div v-if="item.ai_summary && !compact" class="lit-ai">{{ item.ai_summary }}</div>
<div v-if="ds.showAiSummary && item.ai_summary && !compact" class="lit-ai">{{ item.ai_summary }}</div>
<!-- 操作按钮 -->
<div class="lit-actions" v-if="!compact">
<div class="lit-actions" v-if="ds.showActions && !compact">
<button class="action-btn expand-btn" @click.stop="emit('preview', item)" title="快速预览"></button>
<button class="action-btn save-btn" :class="{ saved: isSaved }" @click="onSave">
{{ isSaved ? '' : '' }} 收藏