chore: batch commit remaining changes
Includes search engine improvements, Alembic migrations, new services (pubmed_daily_update, query_expansion), frontend updates, and documentation sync.
This commit is contained in:
@@ -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 ? '⭐' : '☆' }} 收藏
|
||||
|
||||
Reference in New Issue
Block a user