fix: PubMed搜索合规 — 34项修复 + query_expansion UnboundLocalError + HomeView precision_mode残留
Batch1 — 解析器 (pubmed_query_parser.py) - P0-1: 未知字段标签降级为 WORD token 而非 ParseError - P2-1: 增加未消费 token 检查 - P2-2: PRISMA 字段标签正则 [\w:+] → [\w:]+ - P2-3: Unicode NFKC 规格化输入 - P2-4: re.ASCII 防止 Unicode 数字匹配 - P2-9: 移除重复 dataclass 字段 Batch2 — 搜索引擎 (search_engine.py) - P1-1: 批量 PMID 查询替代 N+1 循环 - P1-2: isdigit() → isdecimal() - P1-5: 移除 precision_mode 参数 - P2-5: 移除死代码 - P2-6: 统一 _CHINESE_RE 正则 Batch3 — ATM 引擎 (query_expansion.py) - P0-4: name_zh ILIKE 中文回退 + _find_mesh_tags 中文降级 - P1-3: name_en ILIKE 加 LIMIT 100 Batch4 — API 层 (features.py) - P1-5: 移除 precision_mode 请求字段 - P1-11: 增加 logging - P2-8: NLM_SUBSET_LABELS f-string 安全注释 - P2-12: split 校验器近似性注释 Batch5 — SearchView.vue - P0-5a/b/c: 日期修复(UTC 方法、互斥逻辑、restoreFromQuery 合并) - P2-10: 筛选模态关闭时重搜 - P3-1: 搜索框 aria-label Batch6 — HomeView.vue - P1-10: URL date → date_from/date_to - P2-11: clearSearch 清空 feedItems 并重加载 Batch7 — LiteratureCard.vue - P1-6: 字段标签正则 [\w-]+ → [\w:-]+ - P1-7: terms 切片限制 20 项防 ReDoS 后修复: - query_expansion.py _find_partial_mesh_tags UnboundLocalError(单非中文词未初始化 tag_ids) - HomeView.vue handleAdvancedSearch precision_mode 残留引用
This commit is contained in:
@@ -50,7 +50,6 @@ const hasFullText = ref(false)
|
||||
const hasAssociatedData = ref(false)
|
||||
const medlineOnly = ref(false)
|
||||
const excludePreprints = ref(false)
|
||||
const precisionMode = ref('majr') // 'majr' | 'mesh'
|
||||
|
||||
// ── 筛选选项数据 ──
|
||||
const filterOptions = ref<any>(null)
|
||||
@@ -294,17 +293,18 @@ const { page, total, goToPage } = usePagination({
|
||||
// cursor 不存在时保留 body.page,回退到 offset 分页
|
||||
}
|
||||
}
|
||||
// 年份 / 日期
|
||||
// 年份 / 日期(datePreset 与 year_* 互斥)
|
||||
const yf = Number(yearFromStr.value)
|
||||
const yt = Number(yearToStr.value)
|
||||
if (yearFromStr.value && !isNaN(yf) && yf >= 1900 && yf <= 2100) body.year_from = yf
|
||||
if (yearToStr.value && !isNaN(yt) && yt >= 1900 && yt <= 2100) body.year_to = yt
|
||||
if (!datePreset.value && yearFromStr.value && !isNaN(yf) && yf >= 1900 && yf <= 2100) body.year_from = yf
|
||||
if (!datePreset.value && yearToStr.value && !isNaN(yt) && yt >= 1900 && yt <= 2100) body.year_to = yt
|
||||
if (datePreset.value && datePreset.value !== 'custom') {
|
||||
body.date_to = new Date().toISOString().slice(0, 10)
|
||||
const d = new Date()
|
||||
if (datePreset.value === '1y') d.setFullYear(d.getFullYear() - 1)
|
||||
else if (datePreset.value === '5y') d.setFullYear(d.getFullYear() - 5)
|
||||
else if (datePreset.value === '10y') d.setFullYear(d.getFullYear() - 10)
|
||||
const now = new Date()
|
||||
body.date_to = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())).toISOString().slice(0, 10)
|
||||
const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))
|
||||
if (datePreset.value === '1y') d.setUTCFullYear(d.getUTCFullYear() - 1)
|
||||
else if (datePreset.value === '5y') d.setUTCFullYear(d.getUTCFullYear() - 5)
|
||||
else if (datePreset.value === '10y') d.setUTCFullYear(d.getUTCFullYear() - 10)
|
||||
body.date_from = d.toISOString().slice(0, 10)
|
||||
}
|
||||
// Text Availability
|
||||
@@ -369,15 +369,16 @@ function restoreFromQuery() {
|
||||
if (route.query.year_to) yearToStr.value = String(route.query.year_to)
|
||||
if (route.query.date_preset && ['1y','5y','10y'].includes(String(route.query.date_preset))) {
|
||||
datePreset.value = String(route.query.date_preset)
|
||||
} else if (route.query.date_from) {
|
||||
} else if (route.query.date_from || route.query.date_to) {
|
||||
datePreset.value = null
|
||||
const df = String(route.query.date_from)
|
||||
if (df.length >= 4) yearFromStr.value = df.slice(0, 4)
|
||||
}
|
||||
if (route.query.date_to) {
|
||||
datePreset.value = null
|
||||
const dt = String(route.query.date_to)
|
||||
if (dt.length >= 4) yearToStr.value = dt.slice(0, 4)
|
||||
if (route.query.date_from) {
|
||||
const df = String(route.query.date_from)
|
||||
if (df.length >= 4) yearFromStr.value = df.slice(0, 4)
|
||||
}
|
||||
if (route.query.date_to) {
|
||||
const dt = String(route.query.date_to)
|
||||
if (dt.length >= 4) yearToStr.value = dt.slice(0, 4)
|
||||
}
|
||||
}
|
||||
if (route.query.tag) selectedTags.value = String(route.query.tag).split(',')
|
||||
if (route.query.tier) selectedTiers.value = String(route.query.tier).split(',')
|
||||
@@ -395,7 +396,6 @@ function restoreFromQuery() {
|
||||
if (route.query.associated_data) hasAssociatedData.value = route.query.associated_data === 'true'
|
||||
if (route.query.medline_only) medlineOnly.value = route.query.medline_only === 'true'
|
||||
if (route.query.exclude_preprints) excludePreprints.value = route.query.exclude_preprints === 'true'
|
||||
if (route.query.precision) precisionMode.value = String(route.query.precision)
|
||||
if (route.query.p) restoredPage.value = parseInt(String(route.query.p)) || 1
|
||||
// keyset 游标不能从 URL 恢复 → page>1 回退到首页
|
||||
if (sort.value === 'date' && restoredPage.value > 1) restoredPage.value = 1
|
||||
@@ -410,6 +410,11 @@ watch(datePreset, (val) => {
|
||||
}
|
||||
})
|
||||
|
||||
// P2-10: 弹窗关闭时(mask-closable 或确定按钮)自动搜索
|
||||
watch(showPubTypeModal, (val) => { if (!val && searched.value) goToPage(1) })
|
||||
watch(showLangModal, (val) => { if (!val && searched.value) goToPage(1) })
|
||||
watch(showAgeModal, (val) => { if (!val && searched.value) goToPage(1) })
|
||||
|
||||
// pageSize 变化时持久化并重新搜索
|
||||
watch(pageSize, (v) => {
|
||||
localStorage.setItem('search:pageSize', String(v))
|
||||
@@ -451,17 +456,19 @@ function syncSearchToUrl() {
|
||||
if (query.value) q.q = query.value
|
||||
if (field.value !== 'all') q.field = field.value
|
||||
if (sort.value !== 'date') q.sort = sort.value
|
||||
if (yearFromStr.value) q.year_from = yearFromStr.value
|
||||
if (yearToStr.value) q.year_to = yearToStr.value
|
||||
if (datePreset.value && datePreset.value !== 'custom') {
|
||||
q.date_preset = datePreset.value
|
||||
// 同时保存计算出的 date_from/date_to,使分享的 URL 能直接恢复
|
||||
q.date_to = new Date().toISOString().slice(0, 10)
|
||||
const d = new Date()
|
||||
if (datePreset.value === '1y') d.setFullYear(d.getFullYear() - 1)
|
||||
else if (datePreset.value === '5y') d.setFullYear(d.getFullYear() - 5)
|
||||
else if (datePreset.value === '10y') d.setFullYear(d.getFullYear() - 10)
|
||||
const now = new Date()
|
||||
q.date_to = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())).toISOString().slice(0, 10)
|
||||
const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))
|
||||
if (datePreset.value === '1y') d.setUTCFullYear(d.getUTCFullYear() - 1)
|
||||
else if (datePreset.value === '5y') d.setUTCFullYear(d.getUTCFullYear() - 5)
|
||||
else if (datePreset.value === '10y') d.setUTCFullYear(d.getUTCFullYear() - 10)
|
||||
q.date_from = d.toISOString().slice(0, 10)
|
||||
} else {
|
||||
if (yearFromStr.value) q.year_from = yearFromStr.value
|
||||
if (yearToStr.value) q.year_to = yearToStr.value
|
||||
}
|
||||
if (selectedTags.value.length) q.tag = selectedTags.value.join(',')
|
||||
if (selectedTiers.value.length) q.tier = selectedTiers.value.join(',')
|
||||
@@ -479,7 +486,6 @@ function syncSearchToUrl() {
|
||||
if (hasAssociatedData.value) q.associated_data = 'true'
|
||||
if (medlineOnly.value) q.medline_only = 'true'
|
||||
if (excludePreprints.value) q.exclude_preprints = 'true'
|
||||
if (precisionMode.value !== 'majr') q.precision = precisionMode.value
|
||||
// 页码持久化到 URL(不同排序下使用各自的分页)
|
||||
const currentPage = sort.value === 'date' ? keysetPage.value : page.value
|
||||
if (currentPage > 1) q.p = String(currentPage)
|
||||
@@ -505,7 +511,6 @@ function resetAllFilters() {
|
||||
hasAssociatedData.value = false
|
||||
medlineOnly.value = false
|
||||
excludePreprints.value = false
|
||||
precisionMode.value = 'majr'
|
||||
query.value = ''
|
||||
field.value = 'all'
|
||||
sort.value = 'date'
|
||||
@@ -793,7 +798,7 @@ const specialTags = computed(() => filterOptions.value?.special_tags || {})
|
||||
<!-- ======== 右侧搜索结果 ======== -->
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="search-bar">
|
||||
<div class="search-input-wrap"><NInput v-model:value="query" placeholder='搜索标题、摘要、作者、MeSH词... 支持PubMed语法如 "lung cancer"[TI]' size="small" clearable @keyup.enter="goToPage(1)" /></div>
|
||||
<div class="search-input-wrap"><NInput v-model:value="query" placeholder='搜索标题、摘要、作者、MeSH词... 支持PubMed语法如 "lung cancer"[TI]' size="small" clearable aria-label="搜索文献" @keyup.enter="goToPage(1)" /></div>
|
||||
<NSelect v-model:value="field" :options="[{label:'全部',value:'all'},{label:'标题',value:'title'},{label:'摘要',value:'abstract'},{label:'作者',value:'author'},{label:'机构',value:'affiliation'},{label:'期刊',value:'journal'}]" size="small" />
|
||||
<NButton class="sky-btn" size="small" :loading="loading" :disabled="loading" @click="goToPage(1)"><template #icon><NIcon size="14"><SearchOutline /></NIcon></template>搜索</NButton>
|
||||
<NButton class="sky-btn" :ghost="showFilters" size="small" @click="showFilters=!showFilters"><template #icon><NIcon size="14"><component :is="showFilters ? EyeOffOutline : FilterOutline" /></NIcon></template>{{ showFilters?'隐藏筛选':'筛选' }}</NButton>
|
||||
|
||||
Reference in New Issue
Block a user